歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux下的socket編程實踐(十)基本UDP編程細節

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

日期:2017/3/1 12:22:26   编辑:關於Linux
在我的這兩篇博客中,簡單介紹並實現了基於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;  
    }  
}  

Copyright © Linux教程網 All Rights Reserved