UDP和TCP處於同一層網路模型中,也就是運輸層,基於二者之上的應用有很多,常見的基於TCP的有HTTP、Telnet等,基於UDP有DNS、NFS、SNMP等。UDP是無連接,不可靠的數據協議服務,而TCP提供面向流、提供可靠數據服務。註意,UDP和TCP沒有好壞之分,只是二者的適用場景不同罷了。 ...
UDP和TCP處於同一層網路模型中,也就是運輸層,基於二者之上的應用有很多,常見的基於TCP的有HTTP、Telnet等,基於UDP有DNS、NFS、SNMP等。UDP是無連接,不可靠的數據協議服務,而TCP提供面向流、提供可靠數據服務。註意,UDP和TCP沒有好壞之分,只是二者的適用場景不同罷了。
典型的UDP套接字編程模型是客戶端不予服務端建立連接,而只是調用sendto函數來向服務端發送數據,其中必須要指定服務端的信息,包括IP和埠等;服務端不接收來自客戶端的連接,而只是調用recvfrom函數,來等待某個客戶端的數據到達。
UDP編程模型
在UDP套接字中,有2個函數最常用,也就是sendto和recvfrom,二者的聲明如下:
#include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen); ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);
recvfrom和snedto的前3個參數和read/write的前3個參數一樣。flags表示設置的標誌值,簡單的UDP程式可以直接設置為0,最後兩個參數表示服務端地址(對於sendto來說)或者是對端地址(對於recvfrom來說)。如果不關心對端的地址,則設置為NULL,此時addrlen也可以設置為NULL了。
註意:recvfrom和sendto也可以應用於TCP編程,不過一般不這樣用。
簡單的echo UDP伺服器和客戶端程式
/** * UDP epollc測試 server端 */ #include <iostream> #include <sys/socket.h> #include <sys/epoll.h> #include <cstring> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> using namespace std; int main(int argc, char **argv) { int listenFd = -1; int connFd = -1; int epollFd = -1; struct sockaddr_in servAddr; struct epoll_event epollEvent; struct epoll_event epollEvents[16]; int nEvent = 0; listenFd = socket(AF_INET, SOCK_DGRAM, 0); memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_port = htons(6060); servAddr.sin_addr.s_addr = INADDR_ANY; bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr)); listen(listenFd, 5); epollFd = epoll_create(5); memset(&epollEvent, 0, sizeof(epollEvent)); epollEvent.data.fd = listenFd; epollEvent.events = EPOLLIN; epoll_ctl(epollFd, EPOLL_CTL_ADD, listenFd, &epollEvent); for (; ;) { nEvent = epoll_wait(epollFd, epollEvents, 16, -1); if (nEvent <= 0) { continue; } for (int i = 0; i < nEvent; i++) { int fd = epollEvents[i].data.fd; int event = epollEvents[i].events; if (event & EPOLLIN) { struct sockaddr_in clientAddr; socklen_t clientLen = sizeof(clientAddr); int recvLen = 0; char buff[256]; recvLen = recvfrom(fd, buff, sizeof(buff), 0, (struct sockaddr *)&clientAddr, &clientLen); buff[recvLen] = '\0'; cout << "-------------------------" << endl; cout << ntohs(clientAddr.sin_port) << endl; cout << inet_ntoa(clientAddr.sin_addr) << endl; cout << buff << endl; cout << "-------------------------" << endl; sendto(fd, buff, strlen(buff), 0, (struct sockaddr *)&clientAddr, clientLen); } if (event & (EPOLLHUP | EPOLLERR)) { epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, NULL); close(fd); cout << "client shutdown" << endl; } } } return 0; }
/** * UDP 客戶端 */ #include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; int main(int argc, char **argv) { int connFd = -1; struct sockaddr_in servAddr; char buff[64] = "hi, udp server"; char buffRecv[64]; connFd = socket(AF_INET, SOCK_DGRAM, 0); memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_port = htons(6060); servAddr.sin_addr.s_addr = inet_addr("192.168.1.100"); for (int i = 0; i < 2; i++) { sendto(connFd, buff, sizeof(buff), 0, (struct sockaddr *)&servAddr, sizeof(servAddr)); int recvLen = recvfrom(connFd, buffRecv, sizeof(buffRecv), 0, NULL, NULL); buffRecv[recvLen] = '\0'; cout << buffRecv << endl; } close(connFd); return 0; }
註意:代碼中為了方便沒有處理函數的返回值 :(
UDP編程會有數據包的丟失問題,因為UDP是不可靠的,如果一個客戶的數據包丟失,客戶端將永遠阻塞在recvfrom函數調用;類似的,如果客戶數據到達了服務端,然後響應數據包丟失了,則客戶永遠阻塞在recvfrom調用。為了防止這樣的問題出現,一般可以給recvfrom設置一個超時時間。
一個有意思的小問題
當UDP服務端未運行時,UDP客戶端發送給服務端數據包後就阻塞在了recvfrom調用了,等待一個永遠也不可能的服務端應答。但是通過抓包分析,伺服器主機響應了一個ICMP埠不可達的消息,但是這個ICMP錯誤不返回給客戶進程。
這裡測試用的是Socket Tool工具,在192.168.1.100(window主機)上往192.168.1.150(linux主機)上發送UDP包的測試結果。
這個ICMP錯誤是非同步錯誤,該錯誤是由sendto引起,但是sendto本身成功返回,從UDP輸出操作成功僅僅表示數據包已加入到數據鏈路層的輸出隊列了。而該ICMP報文是後來才接收來的。因此,對於一個UDP套接字來說,由它引發的非同步錯誤並不返回給它,除非它已經建立連接。那麼,這個問題如何解決呢,請搬個小板凳,拿著滑鼠,繼續翻滾 :)
UDP可以使用connect函數嗎
UDP是可以調用connect函數的,但是UDP的connect函數和TCP的connect函數調用確是大相徑庭的,這裡沒有三次握手過程。內核只是檢查是否存在立即可知的錯誤(比如目的地址不可達),記錄對端的IP和埠號,然後立即返回調用進程。
使用了connect的UDP編程就可不必使用sendto函數了,直接使用write/read即可。以下代碼使用了connect函數,然後往未運行UDP服務的主機發送數據:
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstring> #include <unistd.h> using namespace std; int main(int argc, char **argv) { int connFd = -1; struct sockaddr_in servAddr; char buff[64] = "hi, udp server"; char buffRecv[64]; connFd = socket(AF_INET, SOCK_DGRAM, 0); memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_port = htons(60); servAddr.sin_addr.s_addr = inet_addr("192.168.1.150"); connect(connFd, (struct sockaddr *) &servAddr, sizeof(servAddr)); write(connFd, buff, sizeof(buff)); int recvLen = read(connFd, buffRecv, sizeof(buffRecv)); if (recvLen < 0) { perror("read error"); } close(connFd); return 0; }
輸出結果為:
這裡把測試代碼中的read函數替換成以下代碼輸出結果也是一樣的。
int recvLen = recvfrom(connFd, buffRecv, sizeof(buffRecv), 0, NULL, NULL);
UDP缺乏流量控制
每個TCP套介面都有一個發送緩衝區,我們可以用SO_SNDBUF套介面選項來改變這個緩衝區的大小。當應用程式調用write時,內核從應用進程的緩衝區中拷貝所有數據到套介面的發送緩衝區。如果套介面發送緩衝區容不下應用程式所有的程式(或者應用程式的緩衝區大於套介面發送緩衝區,或者是套介面發送緩衝區還有其他數據),應用進程將被掛起,這裡假設write是阻塞的。內核將不從write系統調用返回,直到將應用進程緩衝區的所有數據都拷貝到套介面發送緩衝區。因此從寫一個TCP套介面的write調用成功返回僅僅代表發送的數據到達應用進程的緩衝區。它並不告訴我們對端TCP或者應用進程已經接收到數據。
UDP套介面有發送緩衝區大小(SO_SNDBUF修改),不過它僅僅是寫到套介面的UDP數據報的大小上限,註意,實際上UDP發送緩衝區並不存在。如果一個應用程式寫一個大於套介面發送緩衝區大小的數據報,內核將返回EMSGSIZE錯誤。既然UDP不可靠,它不必保存應用程式的數據拷貝,因此無需真正的發送緩衝區(應用進程的數據在沿協議棧往下傳遞,以某種形式拷貝到內核緩衝區,然而數據鏈路層在送出數據之後將丟棄該拷貝)。
從UDP套介面write成功返回僅僅表示用戶寫入的數據報或者所有片段已經加入到數據鏈路層的輸出隊列。如果該隊列沒有足夠的空間存放該數據包或者它的某個片段,內核通常返回給應用進程一個ENOBUFS錯誤。
TCP和UDP都擁有套介面接收緩衝區。TCP套介面接收緩衝區不可能溢出,因為TCP具有流量控制,然而對於UCP來說,當接收到的數據報裝不進套介面接收緩衝區時,該數據報就丟棄。UDP是沒有流量控制的:較快的發送端可以很容易淹沒較慢的接收端,導致接收端的UDP丟棄數據報。
註意,正是由於UDP沒有流量控制,所以其接收緩衝區滿後就開始丟棄新到來的數據包,測試如下,UDP服務端程式實時列印出接收到的數據包個數:
服務端顯示收到了94個UDP數據包,也就是說丟失了6個,如果一次發送的UDP數據量更大的話,丟包現象更嚴重。
參考資料
1、《Unix網路編程 捲一 套接字編程》UDP章節