通信編程:UDP 套接字和廣播通信


UDP 套接字

工作流程

UDP 用戶數據報協議提供的是無連接的、不可靠的數據傳輸服務,但是不需要像 TCP 那樣建立連接,所以傳輸效率高。

數據收發

UDP 使用的發送數據的函數是 sendto(),比較不同的是發送數據時需要填寫接收方的地址。

int
WSAAPI
sendto(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags,
    _In_reads_bytes_(tolen) const struct sockaddr FAR * to,
    _In_ int tolen
    );

接收數據的函數是 recvfrom(),接收數據時需要把發送方的地址存出來。

int
WSAAPI
recvfrom(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
    _In_ int len,
    _In_ int flags,
    _Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from,
    _Inout_opt_ int FAR * fromlen
    );

sendto() 和 recvfrom() 的參數相似:

參數 說明
s 用來發送數據的套接字
buf 指向發送數據的緩沖區
len 要發送數據的長度
flags 一般指定為 0
to/from 指向一個包含目標地址和端口號的 sockaddr in 結構
tolen/fromlen sockaddr in 結構的大小

程序編寫

功能設計

模擬實現 TCP 協議通信過程,要求編程實現服務器端與客戶端之間雙向數據傳遞。也就是在一條 TCP 連接中,客戶端和服務器相互發送一條數據即可。

initsock.h

#include <winsock2.h>
#pragma comment(lib, "WS2_32")  // 鏈接到 WS2_32.lib

class CInitSock
{
public:
    /*CInitSock 的構造器*/
    CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
    {
        // 初始化WS2_32.dll
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(minorVer, majorVer);
        if (::WSAStartup(sockVersion, &wsaData) != 0)
        {
            exit(0);
        }
    }

    /*CInitSock 的析構器*/
    ~CInitSock()
    {
        ::WSACleanup();
    }
};

服務器

#include "initsock.h"
#include <iostream>
using namespace std;
CInitSock initSock;     // 初始化Winsock庫

int main()
{
    // 創建套接字
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (s == INVALID_SOCKET)
    {
        cout << "Failed socket()" << endl;
        return 0;
    }

    // 填充sockaddr_in結構
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(4567);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;

    // 綁定這個套接字到一個本地地址
    if (::bind(s, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        cout << "Failed bind()" << endl;
        return 0;
    }

    cout << "服務端已啟動!\n" << endl;

    // 進入循環
    char buff[1024];
    sockaddr_in addr;
    int nLen = sizeof(addr);
    while (TRUE)
    {
        //接收數據
        int nRecv = ::recvfrom(s, buff, 1024, 0, (sockaddr*)&addr, &nLen);
        if (nRecv > 0)
        {
            buff[nRecv] = '\0';
            cout << " 接收到" << ::inet_ntoa(addr.sin_addr) << "的數據:" << buff << endl;
        }

        // 發送數據
        char szText[] = "你好,客戶端!";
        ::sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr));
    }
    ::closesocket(s);
}

客戶端

#include "initsock.h"
#include <iostream>
using namespace std;

CInitSock initSock;     // 初始化Winsock庫

int main()
{
    // 創建套接字
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (s == INVALID_SOCKET)
    {
        printf("Failed socket() %d \n", ::WSAGetLastError());
        return 0;
    }

    // 也可以在這里調用bind函數綁定一個本地地址
    // 否則系統將會自動安排

    // 填寫遠程地址信息
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(4567);
    //填寫服務器程序所在機器的IP地址
    addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

    // 發送數據
    char szText[] = "你好,服務器!";
    ::sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr));
    
    char buff[1024];
    sockaddr_in client_addr;
    int nLen = sizeof(client_addr);
    int nRecv = ::recvfrom(s, buff, 1024, 0, (sockaddr*)&client_addr, &nLen);
    if (nRecv > 0)
    {
        buff[nRecv] = '\0';
        cout << "  接收到" << ::inet_ntoa(client_addr.sin_addr) << "的數據:" << buff << endl;
    }

    ::closesocket(s);
    return 0;
}

運行效果

廣播通信

廣播

利用廣播(broadcast)可以將數據發送給本地子網上的每個機器,同時需要有一些線程在機器上監聽到來的數據。廣播的缺點是如果多個進程都發送廣播數據,網絡就會阻塞,網絡性能便會受到影響。

廣播模式設置

套接字創建之后,可以使用套接字選項和 ioctl 命令操作它的屬性,以改變套接字的默認行為。I/O 控制命令縮寫為 ioctl,它也影響套接字的行為,獲取和設置套接字選項的函數分別是 getsockopt 和 setsockopt,函數調用出錯時返回 SOCKET ERROR。

int
WSAAPI
setsockopt(
    _In_ SOCKET s,
    _In_ int level,
    _In_ int optname,
    _In_reads_bytes_opt_(optlen) const char FAR * optval,
    _In_ int optlen
    );

int WSAAPI getsockopt(_In_ SOCKET s, _In_ int level, _In_ int optname, _In_reads_bytes_opt_(optlen) const char FAR * optval, _In_ int optlen);
參數 說明
s 套接字句柄
level 指定此選項被定義在哪個級別,如 SOL SOCKET、IPPROTO_TCP、IPPROTO_IP 等
optname 套接字選項名稱,如 SO ACCEPTCONN
optval 指定一個緩沖區,所請求的選項的值將會被返回到這里
optlen 指定上面 optval 所指緩沖區的大小,返回所需的大小
SOBROADCAST 選項設置套接字傳輸和接收廣播消息,如果給定套接字已經被設置為接收或者發送廣播數據,查詢此套接字選項將返回TRUE,此選項對於那些不是SOCK_STREAM類型的套接字有效。
::setsockopt(s, SOL_SOCKET, SO_BROADCAST, (char*)&bBroadcast, sizeof(BOOL));

代碼編寫

功能設計

實現服務器端與客戶端之間廣播數據傳遞,服務器端向所有客戶端發送“今天是個好日子!”廣播消息,客戶端收到並在本地顯示。

initsock.h

#include <winsock2.h>
#pragma comment(lib, "WS2_32")  // 鏈接到 WS2_32.lib

class CInitSock
{
public:
    /*CInitSock 的構造器*/
    CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
    {
        // 初始化WS2_32.dll
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(minorVer, majorVer);
        if (::WSAStartup(sockVersion, &wsaData) != 0)
        {
            exit(0);
        }
    }

    /*CInitSock 的析構器*/
    ~CInitSock()
    {
        ::WSACleanup();
    }
};

Sender

對於 UDP 來說存在一個特定的廣播地址 255.255.255.255,廣播數據都應該發送到這里。發送方程序在創建套接字后使用 setsockopt 函數打開 SO_BROADCAST選項,然后設置廣播地址 255.255.255.255,使用 sendto 函數向端口號 54321 不斷發送廣播數據。

#include "initsock.h"
#include <iostream>
#include <windows.h>
using namespace std;

CInitSock theSock;

int main()
{
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, 0);
    // 有效SO_BROADCAST選項
    BOOL bBroadcast = TRUE;
    ::setsockopt(s, SOL_SOCKET, SO_BROADCAST, (char*)&bBroadcast, sizeof(BOOL));

    // 設置廣播地址和廣播端口號
    SOCKADDR_IN bcast;
    bcast.sin_family = AF_INET;
    bcast.sin_port = htons(54321);
    bcast.sin_addr.s_addr = INADDR_BROADCAST;

    // 發送廣播
    cout << "  開始向端口發送廣播數據... \n" << endl;
    char sz[] = "今天是個好日子! \r\n";
    while (TRUE)
    {
        time_t now = time(0);    // 基於當前系統的當前日期/時間
        char* dt = ctime(&now);
        ::sendto(s, sz, strlen(sz), 0, (sockaddr*)&bcast, sizeof(bcast));
        cout << dt << "發送廣播:" << sz << endl;
        ::Sleep(5000);
    }
    return 0;
}

Recver

服務器需要使用 recvfrom 函數,向端口號 54321 接收廣播數據。

#include "initsock.h"
#include <iostream>
#include <windows.h>
using namespace std;

CInitSock theSock;

int main()
{
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, 0);

    // 首先要綁定一個本地地址,指明廣播端口號
    SOCKADDR_IN sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(54321);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    if (::bind(s, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        cout << " bind() failed!" << endl;
        return 0;
    }

    // 接收廣播
    cout << "  開始在端口接收廣播數據... \n" << endl;
    SOCKADDR_IN addrRemote;
    int nLen = sizeof(addrRemote);
    char sz[256];
    while (TRUE)
    {
        int nRet = ::recvfrom(s, sz, 256, 0, (sockaddr*)&addrRemote, &nLen);
        if (nRet > 0)
        {
            sz[nRet] = '\0';
            time_t now = time(0);    // 基於當前系統的當前日期/時間
            char* dt = ctime(&now);
            cout << dt << "接收到廣播:" << sz << endl;
        }
    }
    return 0;
}

運行效果


參考資料

《Windows 網絡與通信編程》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM