探索UDP套接字編程

来源:http://www.cnblogs.com/luoxn28/archive/2016/08/26/5811727.html
-Advertisement-
Play Games

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章節

  2、UDP接收端緩衝區和丟包問題


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、安裝JDK 1、用戶可以在Oracle JDK的官網下載相應版本的JDK,本例以JDK 1.6為例,官網地址為http://www.oracle.com/tech-network/java/javase/downloads/index.html。 2、配置環境變數,在/etc/profile增加 ...
  • 1、環境準備 1)準備6台ubuntu虛擬機 2)配置好IP(192.168.1.36,192.168.1.37,192.168.1.38, 192.168.1.40,192.168.1.41,192.168.1.42 三台master 三台slave) 3)安裝好openssh-server 2、 ...
  • 表大小 慢的sql select a.city, a.agent_id, a.username, a.real_name, phone, zgy_name, login_count, user_count, count(distinct b.invest_id) user_invested, sum ...
  • 一個學習性任務:每個人有不同次數的成績,統計出每個人的最高成績。 這個問題應該還是相對簡單,其實就用聚合函數就好了。 select id,name,max(score) from Student group by id,name order by name 上邊這種情況只適用id 和name是一一對 ...
  • 1.安裝MySql 目前MySQL有兩種形式的文件,一個是msi格式,一個是zip格式的。msi格式的直接點擊setup.exe就好,按照步驟進行。但是很多人下了zip格式的解壓發現沒有setup.exe,本人下載的也是這樣的,不知道怎麼安裝,點哪裡都沒有反應。只能尋求度娘幫助,然後才瞭解到,這種文 ...
  • 方法一: 直接(手動)去修改資料庫名稱,資料庫表名稱,資料庫列名稱、列屬性 方法二: 使用SQL語句去修改 ...
  • 彙編器構造 一、 彙編器簡介 前面介紹了編譯器構造和靜態鏈接器構造的具體方法,而且我們實現了一個將高級語言轉化為彙編語言的編譯器,同時又實現了一個將多個目標文件鏈接為一個可執行文件的鏈接器。現在需要一個連接這兩個模塊的功能模塊——彙編器,它能將一個單獨的彙編文件轉換為一個可重定位目標文件,如圖1-1 ...
  • file命令及其用法 file:查看文件內容 的類型 du命令及其用法 -s: -h: read命令及其用法 描述GPL,BSD,Apache三個開源協定的大體聯繫及區別 自由軟體 開源協定,版權描述 列出Linux的發行版,並說明其跟Linux內核的關係 Linux,GNU:GNU/Linux, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...