解析C語言基于UDP協議進行Socket編程的要點
兩種協議 TCP 和 UDP
前者可以理解為有保證的連接,后者是追求快速的連接。
當然最后一點有些 太過絕對 ,但是現在不需熬考慮太多,因為初入套接字編程,一切從簡。
稍微試想便能夠大致理解, TCP 追求的是可靠的傳輸數據, UDP 追求的則是快速的傳輸數據。
前者有繁瑣的連接過程,后者則是根本不建立可靠連接(不是絕對),只是將數據發(fā)送而不考慮是否到達。
以下例子以 *nix 平臺的便準為例,因為 Windows平臺需要考慮額外的加載問題,稍作添加就能在 Windows 平臺上運行UDP。
UDP
這是一個十分簡潔的連接方式,假設有兩臺主機進行通信,一臺只發(fā)送,一臺只接收。
接收端:
int sock; /* 套接字 */ socklen_t addr_len; /* 發(fā)送端的地址長度,用于 recvfrom */ char mess[15]; char get_mess[GET_MAX]; /* 后續(xù)版本使用 */ struct sockaddr_in recv_host, send_host; /* 創(chuàng)建套接字 */ sock = socket(PF_INET, SOCK_DGRAM, 0); /* 把IP 和 端口號信息綁定在套接字上 */ memset(&recv_host, 0, sizeof(recv_host)); recv_host.sin_family = AF_INET; recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */ recv_host.sin_port = htons(6000); /* 使用6000 端口號 */ bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host)); /* 進入接收信息的狀態(tài) */ recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len); /* 接收完成,關閉套接字 */ close(sock);
上述代碼省略了許多必要的 錯誤檢查 ,在實際編寫時要添加
代碼解釋:
PF_INET 代表協議的類型,此處代表 IPv4 網絡協議族, 同樣 PF_INET6 代表 IPv6 網絡協議族,這個范圍在后方單獨記錄,不與IPv4混在一起(并不意味著更復雜,實際上更簡便)。
AF_INET 代表地址的類型,此處代表 IPv4 網絡協議使用的地址族, 同樣有 AF_INET6 (在操作系統(tǒng)實現中 PF_INET 和 AF_INET 的值一樣,但是還是要寫宏更好,而不應該直接用數字或者,混淆使用)
htonl 和 htons 兩個函數的使用涉及到 大端小端問題, 這里不敘述,需要記住的是在網絡編程時一定要使用這種函數將必要信息轉為 大端表示法 。
(struct sockaddr *) 這個強制轉換是為了參數的必須,但不會出錯,因為 sizeof(struct sockaddr_in) == sizeof(struct sockaddr) 具體可以查詢相關信息,之所以這么做是為了方便編寫套接字程序的程序員。
發(fā)送端:
  int sock;
  const char* mess = "Hello Server!";
  char get_mess[GET_MAX]; /* 后續(xù)版本使用 */
  struct sockaddr_in recv_host;
  socklen_t addr_len;
  /* 創(chuàng)建套接字 */
  sock = socket(PF_INET, SOCK_DGRAM, 0);
  /* 綁定 */
  memset(&recv_host, 0, sizeof(recv_host));
  recv_host.sin_family = AF_INET;
  recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
  recv_host.sin_port = htons(6000);
  /* 發(fā)送信息 */
  /* 在此處,發(fā)送端的IP地址和端口號等各類信息,隨著這個函數的調用,自動綁定在了套接字上 */
  sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
  /* 完成,關閉 */
  close(sock);
上述代碼是發(fā)送端。
代碼解釋:
inet_addr 函數是用于將字符串格式的 IP地址 轉換為 大端表示法的 地址類型,即 s_addr 的類型 in_addr_t
與之相反,同樣也有功能相反的函數 inet_ntoa 用于將 in_addr_t 類型轉為字符串,但是使用時一定要記住及時拷貝返回值 char addr[16]; recv_host.sin_addr.s_addr = inet_addr("127.0.0.1"); strcpy(addr, inet_ntoa(recv_host.sin_addr.s_addr));
從上述代碼看出, UDP 協議的使用十分簡潔,幾乎就是 創(chuàng)建套接字->準備數據->裝備套接字->發(fā)送/接收->結束
其中,都沒有連接的操作,但是實際上這是為了方便 UDP 隨時和 不同的主機 進行通信所默認的設置,如果需要和相同主機一直通信呢?
此中的原由暫時不需要知道,記錄方法,即長時間使用 UDP 和同一主機通信時,可以使用 connect 函數來進行優(yōu)化自身。此時 假設兩臺主機的實際功能一致,既接收也發(fā)送
發(fā)送端:
  /* 前方高度一致,將 bind函數替換為 */
  connect(sock, (struct sockaddr *)&recv_host, sizeof(recv_host); // 將對方的 IP地址和 端口號信息 注冊進UDP的套接字中)
  while(1) /* 循環(huán)的發(fā)送和接收信息 */
  {
   size_t read_len = 0;
   /* 原先使用的 sendto 函數,先擇改為使用 write 函數, Windows平臺為 send 函數 */
   write(sock, mess, strlen(mess));      /* send(sock, mess, strlen(mess), 0) FOR Windows Platform */
   read_len = read(sock, get_mess, GET_MAX-1); /* recv(sock, mess, strlen(mess)-1, 0) FOR Windows Platform */
   get_mess[read_len-1] = '\0';
   printf("In Client like Host Recvive From Other Host : %s\n", get_mess);
  }
  /* 后方高度一致 */
接收端:
  /* 前方一致, 添加額外的 struct sockaddr_in send_host; 并添加循環(huán),構造收發(fā)的現象*/
    while(1)
  {
   size_t read_len = 0;
   char sent_mess[15] = "Hello Sender!"; /* 用于發(fā)送的信息 */
   sendto(sock, mess, strlen(sent_mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
   read_len = recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len)
   mess[read_len-1] = '\0';
   printf("In Sever like Host Recvive From other Host : %s\n", mess);
  }
  /* 后方高度一致 */
  /*
  * 之所以只在接收端使用 connect 的原因,便在于我們模擬的是 客戶端-服務器 的模型,而服務器的各項信息是不會隨意變更的
  * 但是 客戶端就不同了,可能由于 ISP(Internet Server Provider) 的原因,你的IP地址不可能總是固定的,所以只能
  * 保證 在客戶端 部分注冊了 服務器 的各類信息,而不能在 服務器端 注冊 客戶端 的信息。
  * 當然也有例外,例如你就想這個軟件作為私密軟件,僅供兩個人使用, 且你有固定的 IP地址,那么你可以兩邊都connect,但是
  * 一定要注意,只要有一點信息變動,這個軟件就可能無法正常的收發(fā)信息了。
  */
代碼解釋
故而實際上,雖然前方的表格顯示,UDP 似乎并沒有 connect 的使用必要,但是實際上還是有用到的地方。
就 *nix 的 API 來說,sendto 和 write 的區(qū)別十分明顯,便是一個需要在參數中提供目標主機的各類信息,而后者則不需要提供。同樣的道理recvfrom和read也是如此。
這個代碼只是做演示而已,所以將代碼置于無限循環(huán)當中,現實中可以自行定義出口條件。
以上是 UDP 的一些簡單說明,入門足矣,并未詳細敘述某些 函數 的具體用法,而是用實際例子來體現。 在 記錄 TCP 之前,還是需要講一個函數 shutdown
shutdown 與 close(closesocket)
首先要知道,網絡通信一般而言是雙方的共同進行的,換而言之就是雙向的,一個方向只用來發(fā)送消息,一個方向只用來讀取消息。
這就導致了,在結束套接字通信的時候,需要關閉兩個方向的通道(暫時叫它們通道),那同時關閉不行嗎?可以啊
close(sock); // closesocket(sock); FOR Windows PlatForm 就是這么干的,同時斷開兩個方向的連接。
簡單的通信程序或者單向通信程序這么做的確無甚大礙,但是萬一在結束通信的時候需要接收最后一個信息那該怎么辦?
假設通信結束,客戶端向服務器發(fā)送 "Thank you"
服務器需要接收這個信息,之后才能關閉通信
問題就在這之間,服務器并不知道客戶端會在通信結束后的什么時刻傳來信息
所以我們選擇在通信完成后先關閉 服務器的 發(fā)送通道(寫流),等待客戶端發(fā)來消息后,關閉剩下的 接收通道(讀流)
發(fā)送端:
/* 假設有一個 TCP 的連接,此為客戶端 */ write(sock, "Thank you", 10); close(sock); // 寫完直接關閉通信
接收端:
  /* 此為服務器 */
  /* 首先關閉寫流 */
  shutdown(sock_c, SHUT_WR);
  read(sock_c, get_mess, GET_MAX);
  printf("Message : %s\n", get_mess);
  close(sock_c);
  close(sock_s); // 關閉兩個套接字是因為 TCP 服務器端的需要,后續(xù)會記錄
代碼解釋
shutdown 函數的作用就是 可選擇的關閉那個方向的輸出
int shutdown(int sock, int howto);
sock 代表要操作的套接字
howto有幾個選擇
- * nix ** : SHUT_RD SHUT_WR SHUT_RDWR
 - Windows : SD_RECEIVE SD_SEND SD_BOTH
 
下面,有幾個結構體,以及一個接口十分重要及常用:
- struct sockaddr_in6 : 代表的是 IPv6 的地址信息
 - struct addrinfo : 這是一個通用的結構體,里面可以存儲 IPv4 或 IPv6 類型地址的信息
 - getaddrinfo : 這是一個十分方便的接口,在上述 UDP 程序中許多手動填寫的部分,都能夠省去,有該函數替我們完成
 
改寫一下上方的例子:
接收端:
  int sock; /* 套接字 */
  socklen_t addr_len; /* 發(fā)送端的地址長度,用于 recvfrom */
  char mess[15];
  char get_mess[GET_MAX]; /* 后續(xù)版本使用 */
  struct sockaddr_in host_v4; /* IPv4 地址 */
  struct sockaddr_in6 host_v6; /* IPv6 地址 */
  struct addrinfo easy_to_use; /* 用于設定要獲取的信息以及如何獲取信息 */
  struct addrinfo *result;  /* 用于存儲得到的信息(需要注意內存泄露) */
  struct addrinfo * p;
  /* 準備信息 */
  memset(&easy_to_use, 0, sizeof easy_to_use);
  easy_to_use.ai_family = AF_UNSPEC; /* 告訴接口,我現在還不知道地址類型 */
  easy_to_use.ai_flags = AI_PASSIVE; /* 告訴接口,稍后“你”幫我填寫我沒明確指定的信息 */
  easy_to_use.ai_socktype = SOCK_DGRAM; /* UDP 的套接字 */
  /* 其余位都為 0 */
  /* 使用 getaddrinfo 接口 */
  getaddrinfo(NULL, argv[1], &easy_to_use, &result); /* argv[1] 中存放字符串形式的 端口號 */
  /* 創(chuàng)建套接字,此處會產生兩種寫法,但更保險,可靠的寫法是如此 */
  /* 舊式方法
  * sock = socket(PF_INET, SOCK_DGRAM, 0);
  */
  /* 把IP 和 端口號信息綁定在套接字上 */
  /* 舊式方法
  * memset(&recv_host, 0, sizeof(recv_host));
  * recv_host.sin_family = AF_INET;
  * recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
  * recv_host.sin_port = htons(6000); /* 使用6000 端口號 */
  * bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
  */
  for(p = result; p != NULL; p = p->ai_next) /* 該語法需要開啟 -std=gnu99 標準*/
  {
    sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if(sock == -1)
     continue;
    if(bind(sock, p->ai_addr, p->ai_addrlen) == -1)
    {
     close(sock);
     continue;
    }
    break; /* 如果能執(zhí)行到此,證明建立套接字成功,套接字綁定成功,故不必再嘗試。 */
  }
  /* 進入接收信息的狀態(tài) */
  //recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
  switch(p->ai_socktype)
  {
   case AF_INET :
    addr_len = sizeof host_v4;
    recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v4, &addr_len);
    break;
    case AF_INET6:
     addr_len = sizeof host_v6
     recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v6, &addr_len);
     break;
    default:
     break;
  }
  freeaddrinfo(result); /* 釋放這個空間,由getaddrinfo分配的 */
  /* 接收完成,關閉套接字 */
  close(sock);
代碼解釋:
首先解釋幾個新的結構體
struct addrinfo 這個結構體的內部順序對于 *nix 和 Windows 稍有不同,以 *nix 為例
 struct addrinfo{
  int ai_flags;
  int ai_family;
  int ai_socktype;
  int ai_protocol;
  socklen_t ai_addrlen;
  struct sockaddr * ai_addr; /* 存放結果地址的地方 */
  char * ai_canonname; /* 忽略它吧,很長一段時間你無須關注它 */
  struct addrinfo * ai_next; /* 一個域名/IP地址可能解析出多個不同的 IP */
 };
ai_family 如果設定為 AF_UNSPEC 那么在調用 getaddrinfo 時,會自動幫你確定,傳入的地址是什么類型的
ai_flags 如果設定為 AI_PASSIVE 那么調用 getaddrinfo 且向其第一個參數傳入 NULL 時會自動綁定自身 IP,相當于設定 INADDR_ANY
- ai_socktype 就是要創(chuàng)建的套接字類型,這個必須明確聲明,系統(tǒng)沒法預判(日后人工智能說不定呢?)
 - ai_protocol 一般情況下我們設置為 0,含義可以自行查找,例如 MSDN 或者 UNP
 - ai_addr 這里保存著結果,可以通過 調用getaddrinfo之后 的第四個參數獲得。
 - ai_addrlen 同上
 - ai_next 同上
 
getaddrinfo 強大的接口函數
  int getaddrinfo(const char * node, const char * service,
                    const struct addrinfo * hints, struct addrinfo ** res);
通俗的說這幾個參數的作用
node 便是待獲取或者待綁定的 域名 或是 IP,也就是說,這里可以直接填寫域名,由操作系統(tǒng)來轉換成 IP 信息,或者直接填寫IP亦可,是以字符串的形式
service 便是端口號的意思,也是字符串形式
hints 通俗的來說就是告訴接口,我需要你反饋哪些信息給我(第四個參數),并將這些信息填寫到第四個參數里。
res 便是保存結果的地方,需要注意的是,這個結果在API內部是動態(tài)分配內存了,所以使用完之后需要調用另一個接口(freeaddrinfo)將其釋放
實際上對于現代的 套接字編程 而言,多了幾個新的存儲 IP 信息的結構體,例如 struct sockaddr_in6 和 struct sockaddr_storage 等。
其中,前者是后者的大小上的子集,即一個 struct storage 一定能夠裝下一個 struct sockaddr_in6,具體(實際上根本看不到有意義的實現)
  struct sockaddr_in6{
   u_int16_t sin6_family;
   u_int16_t sin6_port;
   u_int32_t sin6_flowinfo; /* 暫時忽略它 */
   struct in6_addr sin6_addr; /* IPv6 的地址存放在此結構體中 */
   u_int32_t sin_scope_id; /* 暫時忽略它 */
  };
  struct in6_addr{
   unsigned char s6_addr[16];
  }
  ------------------------------------------------------------
  struct sockaddr_storage{
   sa_family_t ss_family; /* 地址的種類 */
   char __ss_pad1[_SS_PAD1SIZE]; /* 從此處開始,不是實現者幾乎是沒辦法理解 */
   int64_t __ss_align;      /* 從名字上可以看出大概是為了兼容兩個不同 IP 類型而做出的妥協 */
   char __ss_pad2[_SS_PAD2SIZE]; /* 隱藏了實際內容,除了 IP 的種類以外,無法直接獲取其他的任何信息。 */
   /* 在各個*nix 的具體實現中, 可能有不同的實現,例如 `__ss_pad1` , `__ss_pad2` , 可能合并成一個 `pad` 。 */
  };
在實際中,我們往往不需要為不同的IP類型聲明不同的存儲類型,直接使用 struct sockaddr_storage 就可以,使用時直接強制轉換類型即可
改寫上方 接收端 例子中,進入接收信息的狀態(tài)部分
/* 首先將多于的變量化簡 */ // - struct sockaddr_in host_v4; /* IPv4 地址 */ // - struct sockaddr_in6 host_v6; /* IPv6 地址 struct sockaddr_storage host_ver_any; /* + 任意類型的 IP 地址 */ ... /* 進入接收信息的狀態(tài)部分 */ recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_ver_any, &addr_len); /* 像是又回到了只有 IPv4 的年代*/
補充完整上方對應的 發(fā)送端 代碼
  int sock;
  const char* mess = "Hello Server!";
  char get_mess[GET_MAX]; /* 后續(xù)版本使用 */
  struct sockaddr_storage recv_host; /* - struct sockaddr_in recv_host; */
  struct addrinfo tmp, *result;
  struct addrinfo *p;
  socklen_t addr_len;
  /* 獲取對端的信息 */
  memset(&tmp, 0, sizeof tmp);
  tmp.ai_family = AF_UNSPEC;
  tmp.ai_flags = AI_PASSIVE;
  tmp.ai_socktype = SOCK_DGRAM;
  getaddrinfo(argv[1], argv[2], &tmp, &result); /* argv[1] 代表對端的 IP地址, argv[2] 代表對端的 端口號 */
  /* 創(chuàng)建套接字 */
  for(p = result; p != NULL; p = p->ai_next)
  {
   sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol); /* - sock = socket(PF_INET, SOCK_DGRAM, 0); */
   if(sock == -1)
    continue;
   /* 此處少了綁定 bind 函數,因為作為發(fā)送端不需要講對端的信息 綁定 到創(chuàng)建的套接字上。 */ 
   break; /* 找到就可以退出了,當然也有可能沒找到,那么此時 p 的值一定是 NULL */
  }
  if(p == NULL)
  {
   /* 錯誤處理 */
  }
  /* -// 設定對端信息
  memset(&recv_host, 0, sizeof(recv_host));
  recv_host.sin_family = AF_INET;
  recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
  recv_host.sin_port = htons(6000);
  */
  /* 發(fā)送信息 */
  /* 在此處,發(fā)送端的IP地址和端口號等各類信息,隨著這個函數的調用,自動綁定在了套接字上 */
  sendto(sock, mess, strlen(mess), 0, p->ai_addr, p->ai_addrlen);
  /* 完成,關閉 */
  freeaddrinfo(result); /* 實際上這個函數應該在使用完 result 的地方就予以調用 */
  close(sock);        
到了此處,實際上是開了網絡編程的一個初始,解除了現代的 UDP 最簡單的用法(甚至還算不上完整的使用),但是確實是進行了交互。
介紹 UDP 并不是因為它簡單,而是因為他簡潔,也不是因為它不重要,相反他其實很強大。
永遠不要小看一個簡潔的東西,就像 C語言
ARP 協議
最簡便的方法就是找一個有 WireShark 軟件或者 tcpdump 的 *nix 平臺,前者你可以選擇隨意監(jiān)聽一個機器,不多時就能看見 ARP 協議的使用,因為它使用的太頻繁了。
對于 ARP 協議而言,首先對于一臺機器 A,想與 機器B 通信,(假設此時 機器A 的高速緩存區(qū)(操作系統(tǒng)一定時間更新一次)中 沒有 機器B的緩存),
那么機器A就向廣播地址發(fā)出 ARP請求,如果 機器B 收到了這個請求,就將自己的信息(IP地址,MAC地址)填入 ARP應答 中,再發(fā)送回去就行。
上述中, ARP請求 和 ARP應答 是一種報文形式的信息,是 ARP協議 所附帶的實現產品,也是用于兩臺主機之間進行通信。
這是當 機器A 和 機器B 同處于一個網絡的情況下,可以借由本網絡段的廣播地址 發(fā)送請求報文。
對于不同網絡段的 機器A 與 機器B 而言,想要通過 ARP協議 獲取 MAC地址 ,就需要借助路由器的幫助了,可以想象一下,路由器(可以不止一個)在中間,機器A 和 機器B 分別在這些路由器的兩邊(即在不同子網)
由于 A 和 B 不在同一個子網內,所以沒辦法通過通過直接通過廣播到達,但是有了路由器,就能進行 ARP代理 的操作,大概就是將路由器當成機器B, A向自己的本地路由器發(fā)送 ARP請求
之后路由器判斷出是發(fā)送給B的ARP請求,又正好 B 在自己的管轄范圍之內,就把自己的硬件地址 寫入 ARP應答 中發(fā)回去,之后再有A向B 的數據,就都是A先發(fā)送給路由器,再經由路由器發(fā)往B了
ICMP協議
這個協議比較重要。
請求應答報文 和 差錯報文 ,重點在于差錯報文。
請求應答報文在 ICMP 的應用中可以拿來查詢本機的子網掩碼之類的信息,大致通過向本子網內的所有主機發(fā)送該請求報文(包括自己,實際上就是廣播),后接收應答,得到信息
差錯報文在后續(xù)中會有提到,這里需要科普一二。
首先對于差錯報文的一大部分是關于 xxx不可達 的類型,例如主機不可達,端口不可達等等,每次出現錯誤的時候,ICMP報文總是第一時間返回給對端,(它一次只會出現一份,否則會造成網絡風暴),但是對端是否能夠接收到,就不是發(fā)送端的問題了。
這點上 套接字的類型 有著一定的聯系,例如 UDP 在 unconnected 狀態(tài)下是會忽略 ICMP報文的。而 TCP 因為總是 connected 的,所以對于 ICMP報文能很好的捕捉。
ICMP差錯報文中總是帶著 出錯數據報中的一部分真實數據,用于配對。
上一篇:淺談Windows系統(tǒng)下C語言編程中Glib庫的使用
欄 目:C語言
下一篇:使用C++遞歸求解跳臺階問題
本文標題:解析C語言基于UDP協議進行Socket編程的要點
本文地址:http://www.jygsgssxh.com/a1/Cyuyan/2478.html
您可能感興趣的文章
- 04-02c語言函數調用后清空內存 c語言調用函數刪除字符
 - 04-02c語言的正則匹配函數 c語言正則表達式函數庫
 - 04-02func函數+在C語言 func函數在c語言中
 - 04-02c語言中對數函數的表達式 c語言中對數怎么表達
 - 04-02c語言用函數寫分段 用c語言表示分段函數
 - 04-02c語言編寫函數冒泡排序 c語言冒泡排序法函數
 - 04-02c語言沒有round函數 round c語言
 - 04-02c語言分段函數怎么求 用c語言求分段函數
 - 04-02C語言中怎么打出三角函數 c語言中怎么打出三角函數的值
 - 04-02c語言調用函數求fibo C語言調用函數求階乘
 


閱讀排行
本欄相關
- 04-02c語言函數調用后清空內存 c語言調用
 - 04-02func函數+在C語言 func函數在c語言中
 - 04-02c語言的正則匹配函數 c語言正則表達
 - 04-02c語言用函數寫分段 用c語言表示分段
 - 04-02c語言中對數函數的表達式 c語言中對
 - 04-02c語言編寫函數冒泡排序 c語言冒泡排
 - 04-02c語言沒有round函數 round c語言
 - 04-02c語言分段函數怎么求 用c語言求分段
 - 04-02C語言中怎么打出三角函數 c語言中怎
 - 04-02c語言調用函數求fibo C語言調用函數求
 
隨機閱讀
- 08-05dedecms(織夢)副欄目數量限制代碼修改
 - 04-02jquery與jsp,用jquery
 - 01-10使用C語言求解撲克牌的順子及n個骰子
 - 01-11ajax實現頁面的局部加載
 - 08-05織夢dedecms什么時候用欄目交叉功能?
 - 08-05DEDE織夢data目錄下的sessions文件夾有什
 - 01-10delphi制作wav文件的方法
 - 01-10C#中split用法實例總結
 - 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
 - 01-10SublimeText編譯C開發(fā)環(huán)境設置
 


