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; } }