Linux下的socket編程實踐(十)基本UDP編程細節

來源:互聯網
上載者:User

Linux下的socket編程實踐(十)基本UDP編程細節
在我的這兩篇部落格中,簡單介紹並實現了基於UDP(TCP)的windows(UNIX下流程基本一致)下的服務端和用戶端的程式,本文繼續探討關於UDP編程的一些細節。
是一個簡單的UDP客戶/伺服器模型:

我在這裡也實現了一個簡單的UDP回射伺服器/用戶端:



/**實踐: 實現一個基於UDP的echo回聲server/client**/  //server端代碼  void echoServer(int sockfd);  int main()  {      int sockfd = socket(AF_INET, SOCK_DGRAM, 0);      if (sockfd == -1)          err_exit("socket error");        struct sockaddr_in servAddr;      servAddr.sin_family = AF_INET;      servAddr.sin_addr.s_addr = htonl(INADDR_ANY);      servAddr.sin_port = htons(8001);      if (bind(sockfd, (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)          err_exit("bind error");        echoServer(sockfd);  }  void echoServer(int sockfd)  {      char buf[BUFSIZ];      ssize_t recvBytes = 0;      struct sockaddr_in clientAddr;      socklen_t addrLen;      while (true)      {          memset(buf, 0, sizeof(buf));          addrLen = sizeof(clientAddr);          memset(&clientAddr, 0, addrLen);          recvBytes = recvfrom(sockfd, buf, sizeof(buf), 0,                               (struct sockaddr *)&clientAddr, &addrLen);          //如果recvBytes=0, 並不代表對端串連關閉, 因為UDP是不需連線的          if (recvBytes < 0)          {              if (errno == EINTR)                  continue;              else                  err_exit("recvfrom error");          }            cout << buf ;          if (sendto(sockfd, buf, recvBytes, 0,                     (const struct sockaddr *)&clientAddr, addrLen) == -1)              err_exit("sendto error");      }  }  
/**client端代碼**/  void echoClient(int sockfd);  int main()  {      int sockfd = socket(AF_INET, SOCK_DGRAM, 0);      if (sockfd == -1)          err_exit("socket error");      echoClient(sockfd);      cout << "Client exiting..." << endl;  }  void echoClient(int sockfd)  {      struct sockaddr_in servAddr;      servAddr.sin_family = AF_INET;      servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");      servAddr.sin_port = htons(8001);      char buf[BUFSIZ] = {0};      while (fgets(buf, sizeof(buf), stdin) != NULL)      {          if (sendto(sockfd, buf, strlen(buf), 0,                     (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)              err_exit("sendto error");          memset(buf, 0, sizeof(buf));          int recvBytes = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);          if (recvBytes == -1)          {              if (errno == EINTR)                  continue;              else                  err_exit("recvfrom error");          }          cout << buf ;          memset(buf, 0, sizeof(buf));      }  }  
UDP協議並不是像TCP一樣是一對一的通訊,UDP可以實現廣播通訊,並且由於是不需連線的,只要知道對等方地址(ip和port) 都可以主動發資料。關閉server,再串連上,還可以進行通訊。


UDP編程的注意事項和細節:
1.UDP不存在粘包問題,因為不是基於流的傳輸(基於訊息)。
2.UDP報文可能會丟失、重複、亂序問題。處理丟失可以採用逾時處理機制,計時器逾時重傳;處理重複、亂序可以靠維護資料報之間的序號解決。
3.UDP缺乏流量控制:當緩衝區寫滿以後,由於UDP沒有流量控制機制,因此會覆蓋緩衝區。可以通過類比TCP的滑動視窗協議解決。
4.資料報截斷:如果對端發送的UDP資料報大於本地接收緩衝區,報文可能被截斷,後面的部分會丟失(而不是像我們想象的下一次能夠接收到);並且如果我們使用sendto發送"ABCD"4個位元組,接受的recv函數採用迴圈的方式每次接收一個字元,然而結果卻是我們只能接受到A。剩下的不會存在於緩衝區,這也可以形成判斷依據: 會丟失報式套介面(UDP)不是流式套介面(TCP)。
5.recvfrom返回0:不代表串連關閉,因為UDP是不需連線的。
6.ICMP非同步錯誤(重點)
伺服器或者用戶端只有一方開啟的時候,會發生這個錯誤,並且這是TCP/IP協議棧產生的一個ICMP應答錯誤,recv的時候才能收到。因為根本得不到通知,所以稱作非同步,不會返回給未串連的通訊端。
解決方案:UDP調用connect
當增加上connect後,send之後,如果對方處於未開啟的話,那麼recv會接受到ICMP錯誤。UDP的connect沒有三向交握,僅僅是維護了一個資訊,一個狀態,並且為這個通訊端不能發送給其他地址。connect後不指定sendto的目的地址也可以,因為串連的時候已經指定了,並且還可以用send,write的方法。
進一步說明:


1)UDP發送報文的時,只把資料copy到發送緩衝區。在伺服器沒有起來的情況下,可以發送成功。
2)所謂ICMP非同步錯誤是指:發送的報文的時候,沒有錯誤,接受報文recvfrom的時候,回收到ICMP應答.
3)非同步錯誤,無法返回未串連的通訊端, 因此如果上例我們調用了connect, 是可以收到該非同步ICMP報文的;


對1)的進一步分析:
如果伺服器沒啟動,用戶端sendto發送一個資料,結果會是什麼呢?
結論是,sendto成功返回,如果用戶端還同時調用了recvfrom,則將永遠堵塞在recvfrom函數(當然可以設逾時),同時通過tcpdump還可以看到,服務端返回ICMP port unreachable錯誤訊息,但是這個訊息並沒有通過sendto和recvfrom函數返回給使用者進程,換句話說,使用者並不知道服務端返回了ICMP錯誤。怎麼辦呢,udp的connet函數可以搞定這些。
對於udp socket調用connect,稱之為已串連UDP socket,其與未串連UDP socket區別如下:
不使用sendto,而使用write或send,因為在connect中,已經指定目的端IP地址;不應用recvfrom,而使用read或recv或recvmsg,注意,如果源地址不是connect串連的目的地址,是不會回饋到該通訊端的,這正好由核心幫我們完成了驗證接收到的響應;由已串連的UDP通訊端引發的非同步錯誤,會返回給他們所在的進程,這樣就很好的解決了上述問題;在調用connect,確定目的IP和port之外,同時還會通過目的地址,尋找路由表,確定本地地址,connect之後調用getsockname可以擷取到。

總結:UDP客戶或服務進程,僅在使用自己的UDP通訊端與確定的唯一對端進行通訊時,才會調用connect,當然,一般UDP用戶端會用connect多一點。同時如果採用connect,其效能也會得到提升,因為對於sendto來講,其發送資料前和後,需要串連通訊端、斷開通訊端,如果connect之後,這兩步就省下了。

 

7.UDP外出介面的確定:

假設用戶端有多個IP地址,由connect /sendto 函數提供的遠程地址的參數,系統會選擇一個合適的出口,比如Server的IP是192.168.2.10, 而用戶端現在的IP有 192.168.1.32 和 192.168.2.75 那麼會自動選擇192.168.2.75 這個IP出去。(具體演算法先略過...)


最後附上資料報截斷的一個樣本:

 

int main()  {      int sockfd = socket(AF_INET, SOCK_DGRAM, 0);      if (sockfd == -1)          err_exit("socket error");        struct sockaddr_in servAddr;      servAddr.sin_family = AF_INET;      servAddr.sin_addr.s_addr = htonl(INADDR_ANY);      servAddr.sin_port = htons(8001);      if (bind(sockfd, (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)          err_exit("bind error");      //給自己發送資料      if (sendto(sockfd, "ABCDE", 5, 0,                 (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)          err_exit("sendto error");        for (int i = 0; i < 5; ++i)      {          char ch;          int recvBytes =  recvfrom(sockfd, &ch, 1, MSG_DONTWAIT, NULL, NULL);          if (recvBytes == -1)          {              if (errno == EINTR)                  continue;              else if (errno == EAGAIN)                  err_exit("recvfrom error");          }          else              cout << "char = " << ch << ", recvBytes = " << recvBytes << endl;      }  }  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.