解析C語言基於UDP協議進行Socket編程的要點_C 語言

來源:互聯網
上載者:User

兩種協議 TCP 和 UDP
前者可以理解為有保證的串連,後者是追求快速的串連。
當然最後一點有些 太過絕對 ,但是現在不需熬考慮太多,因為初入通訊端編程,一切從簡。
稍微試想便能夠大致理解, TCP 追求的是可靠的傳輸資料, UDP 追求的則是快速的傳輸資料。
前者有繁瑣的串連過程,後者則是根本不建立可靠串連(不是絕對),只是將資料發送而不考慮是否到達。
以下例子以 *nix 平台的便准為例,因為 Windows平台需要考慮額外的載入問題,稍作添加就能在 Windows 平台上運行UDP。

UDP

這是一個十分簡潔的串連方式,假設有兩台主機進行通訊,一台只發送,一台只接收。
接收端:

  int sock; /* 通訊端 */  socklen_t addr_len; /* 發送端的地址長度,用於 recvfrom */  char mess[15];  char get_mess[GET_MAX]; /* 後續版本使用 */  struct sockaddr_in recv_host, send_host;  /* 建立通訊端 */  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));  /* 進入接收資訊的狀態 */  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 (在作業系統實現中 PF_INET 和 AF_INET 的值一樣,但是還是要寫宏更好,而不應該直接用數字或者,混淆使用)
htonl 和 htons 兩個函數的使用涉及到 大端小端問題, 這裡不敘述,需要記住的是在網路編程時一定要使用這種函數將必要資訊轉為 大端標記法 。
(struct sockaddr *) 這個強制轉換是為了參數的必須,但不會出錯,因為 sizeof(struct sockaddr_in) == sizeof(struct sockaddr) 具體可以查詢相關資訊,之所以這麼做是為了方便編寫通訊端程式的程式員。
發送端:

  int sock;  const char* mess = "Hello Server!";  char get_mess[GET_MAX]; /* 後續版本使用 */  struct sockaddr_in recv_host;  socklen_t addr_len;  /* 建立通訊端 */  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);  /* 發送資訊 */  /* 在此處,發送端的IP地址和連接埠號碼等各類資訊,隨著這個函數的調用,自動綁定在了通訊端上 */  sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));  /* 完成,關閉 */  close(sock);

上述代碼是發送端。

代碼解釋:
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 協議的使用十分簡潔,幾乎就是 建立通訊端->準備資料->裝備通訊端->發送/接收->結束
其中,都沒有串連的操作,但是實際上這是為了方便 UDP 隨時和 不同的主機 進行通訊所預設的設定,如果需要和相同主機一直通訊呢?
此中的原由暫時不需要知道,記錄方法,即長時間使用 UDP 和同一主機通訊時,可以使用 connect 函數來進行最佳化自身。此時 假設兩台主機的實際功能一致,既接收也發送
發送端:

  /* 前方高度一致,將 bind函數替換為 */  connect(sock, (struct sockaddr *)&recv_host, sizeof(recv_host); // 將對方的 IP地址和 連接埠號碼資訊 註冊進UDP的通訊端中)  while(1) /* 迴圈的發送和接收資訊 */  {   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; 並添加迴圈,構造收發的現象*/    while(1)  {   size_t read_len = 0;   char sent_mess[15] = "Hello Sender!"; /* 用於發送的資訊 */   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,但是  * 一定要注意,只要有一點資訊變動,這個軟體就可能無法正常的收發資訊了。  */

代碼解釋
故而實際上,雖然前方的表格顯示,UDP 似乎並沒有 connect 的使用必要,但是實際上還是有用到的地方。
就 *nix 的 API 來說,sendto 和 write 的區別十分明顯,便是一個需要在參數中提供目標主機的各類資訊,而後者則不需要提供。同樣的道理recvfrom和read也是如此。
這個代碼只是做示範而已,所以將代碼置於無限迴圈當中,現實中可以自行定義出口條件。
以上是 UDP 的一些簡單說明,入門足矣,並未詳細敘述某些 函數 的具體用法,而是用實際例子來體現。 在 記錄 TCP 之前,還是需要講一個函數 shutdown
shutdown 與 close(closesocket)

首先要知道,網路通訊一般而言是雙方的共同進行的,換而言之就是雙向的,一個方向只用來發送訊息,一個方向只用來讀取訊息。
這就導致了,在結束通訊端通訊的時候,需要關閉兩個方向的通道(暫時叫它們通道),那同時關閉不行嗎?可以啊
close(sock); // closesocket(sock); FOR Windows PlatForm 就是這麼乾的,同時斷開兩個方向的串連。
簡單的通訊程式或者單向通訊程式這麼做的確無甚大礙,但是萬一在結束通訊的時候需要接收最後一個資訊那該怎麼辦?
假設通訊結束,用戶端向伺服器發送 "Thank you"
伺服器需要接收這個資訊,之後才能關閉通訊
問題就在這之間,伺服器並不知道用戶端會在通訊結束後的什麼時刻傳來資訊
所以我們選擇在通訊完成後先關閉 伺服器的 發送通道(寫流),等待用戶端發來訊息後,關閉剩下的 接收通道(讀流)
發送端:

  /* 假設有一個 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 伺服器端的需要,後續會記錄

代碼解釋
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; /* 發送端的地址長度,用於 recvfrom */  char mess[15];  char get_mess[GET_MAX]; /* 後續版本使用 */  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] 中存放字串形式的 連接埠號碼 */  /* 建立通訊端,此處會產生兩種寫法,但更保險,可靠的寫法是如此 */  /* 舊式方法  * 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; /* 如果能執行到此,證明建立通訊端成功,通訊端綁定成功,故不必再嘗試。 */  }  /* 進入接收資訊的狀態 */  //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 就是要建立的通訊端類型,這個必須明確聲明,系統沒法預判(日後人工智慧說不定呢?)
  • 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,也就是說,這裡可以直接填寫網域名稱,由作業系統來轉換成 IP 資訊,或者直接填寫IP亦可,是以字串的形式
service 便是連接埠號碼的意思,也是字串形式
hints 通俗的來說就是告訴介面,我需要你反饋哪些資訊給我(第四個參數),並將這些資訊填寫到第四個參數裡。
res 便是儲存結果的地方,需要注意的是,這個結果在API內部是動態分配記憶體了,所以使用完之後需要調用另一個介面(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 就可以,使用時直接強制轉換類型即可

改寫上方 接收端 例子中,進入接收資訊的狀態部分

  /* 首先將多於的變數化簡 */  // - struct sockaddr_in host_v4; /* IPv4 地址 */  // - struct sockaddr_in6 host_v6; /* IPv6 地址  struct sockaddr_storage host_ver_any; /* + 任意類型的 IP 位址 */  ...  /* 進入接收資訊的狀態部分 */  recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_ver_any, &addr_len); /* 像是又回到了只有 IPv4 的年代*/

補充完整上方對應的 發送端 代碼

  int sock;  const char* mess = "Hello Server!";  char get_mess[GET_MAX]; /* 後續版本使用 */  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] 代表對端的 連接埠號碼 */  /* 建立通訊端 */  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 函數,因為作為發送端不需要講對端的資訊 綁定 到建立的通訊端上。 */    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);  */  /* 發送資訊 */  /* 在此處,發送端的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 平台,前者你可以選擇隨意監聽一個機器,不多時就能看見 ARP 協議的使用,因為它使用的太頻繁了。
對於 ARP 協議而言,首先對於一台機器 A,想與 機器B 通訊,(假設此時 機器A 的快取區(作業系統一定時間更新一次)中 沒有 機器B的緩衝),
那麼機器A就向廣播位址發出 ARP請求,如果 機器B 收到了這個請求,就將自己的資訊(IP地址,MAC地址)填入 ARP應答 中,再發送回去就行。
上述中, ARP請求 和 ARP應答 是一種報文形式的資訊,是 ARP協議 所附帶的實現產品,也是用於兩台主機之間進行通訊。
這是當 機器A 和 機器B 同處於一個網路的情況下,可以藉由本網路段的廣播位址 發送請求報文。
對於不同網路段的 機器A 與 機器B 而言,想要通過 ARP協議 擷取 MAC地址 ,就需要藉助路由器的協助了,可以想象一下,路由器(可以不止一個)在中間,機器A 和 機器B 分別在這些路由器的兩邊(即在不同子網)
由於 A 和 B 不在同一個子網內,所以沒辦法通過通過直接通過廣播到達,但是有了路由器,就能進行 ARP代理 的操作,大概就是將路由器當成機器B, A向自己的本地路由器發送 ARP請求
之後路由器判斷出是發送給B的ARP請求,又正好 B 在自己的管轄範圍之內,就把自己的硬體地址 寫入 ARP應答 中發回去,之後再有A向B 的資料,就都是A先發送給路由器,再經由路由器發往B了

ICMP協議

這個協議比較重要。
請求應答報文 和 差錯報文 ,重點在於差錯報文。
請求應答報文在 ICMP 的應用中可以拿來查詢原生子網路遮罩之類的資訊,大致通過向本子網內的所有主機發送該請求報文(包括自己,實際上就是廣播),後接收應答,得到資訊
差錯報文在後續中會有提到,這裡需要科普一二。
首先對於差錯報文的一大部分是關於 xxx不可達 的類型,例如主機不可達,連接埠不可達等等,每次出現錯誤的時候,ICMP報文總是第一時間返回給對端,(它一次只會出現一份,否則會造成網路風暴),但是對端是否能夠接收到,就不是發送端的問題了。
這點上 通訊端的類型 有著一定的聯絡,例如 UDP 在 unconnected 狀態下是會忽略 ICMP報文的。而 TCP 因為總是 connected 的,所以對於 ICMP報文能很好的捕捉。
ICMP差錯報文中總是帶著 出錯資料報中的一部分真實資料,用於配對。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.