通信編程:Winsock 編寫 TCP 套接字


套接字編寫流程

以 TCP 套接字為例,由於 TCP 是面向連接的協議,所以基於 TCP 的套接字也需要有多個步驟。

套接字的創建

在進行網絡通信之前,都需要使用 socket() 函數創建一個套接字對象。

SOCKET
WSAAPI
socket(
    _In_ int af,
    _In_ int type,
    _In_ int protocol
    );
參數 說明
af socket 使用的地址格式
type 指定套接字的類型
protocol 指定使用的協議類型

其中 WinSock 中只支持 AF_INET 作為地址格式,一般 type 確定后 protocol 也會隨之確定。socket 的 type 可以是以下幾種類型:

type 類型 說明
SOCK STREAM 流套接字 使用TCP提供有連接的可靠的傳輸
SOCK DGRAM 數據報套接字 使用UDP提供無連接的不可靠的傳輸
SOCK RAW 原始套接字 不使用某種特定的協議去封裝它,而是由程序自行處理數據報以及協議首部

綁定 socket 和地址

創建了 socket 對象后,需要為該對象綁定 IP 地址和端口號,需要使用到 bind() 函數

bind(
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
    _In_ int namelen
    );
參數 說明
s 套接字句柄
name 要關聯的本地地址
namelen 地址長度

一般來說 s 就是剛剛創建的 socket 對象,name 可一個 sockaddr_in 結構,namelen 則直接對一個 sockaddr_in 結構用 sizeof() 運算即可。

進入監聽狀態

綁定地址后,socket 就可以進入監聽狀態,這個時候就可以接收傳來的鏈接信息了。為了進入監聽狀態,需要使用 listen() 函數

listen(
    _In_ SOCKET s,
    _In_ int backlog
    );
參數 說明
s 套接字句柄
backlog 監聽隊列的長度

接收連接請求

客戶端想要與服務器建立一條 TCP 連接,需要使用 connect() 函數

int
WSAAPI
connect(
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
    _In_ int namelen
    );

服務器使用 accept() 函數將在監聽隊列中,取出未處理連接中的第一個連接,然后為這個連接創建新的套接字,返回它的句柄。新創建的套接字是處理實際連接的套接字,它與 s 有相同的屬性。

accept(
    _In_ SOCKET s,
    _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
    _Inout_opt_ int FAR * addrlen
    );

connect() 函數和 accept() 函數的參數相同:

參數 說明
s 套接字句柄
name 要連接的設備的地址信息
namelen 地址長度

name 中的地址用來尋址遠程的 socket,一般來說監聽狀態下是一個循環等待的過程。此時程序默認工作在阻塞模式下,如果沒有未處理的連接存在,accept() 函數會一直等待下去,直到有新的連接發生才返回。

收發數據

對於流套接字來說,一般使用 send() 函數來發送緩沖區內的數據,返回發送數據的實際字節數。

send(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags
    );
參數 說明
s 套接字句柄
buf 要發送的數據
len 要發送的數據的長度
flags 調用方式,通常為 0

可以使用 recv() 函數從對方接收數據,並將其存儲到指定的緩沖區。

recv(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
    _In_ int len,
    _In_ int flags
    );
參數 說明
s 套接字句柄
buf 接收的數據要存儲的變量
len 能接收的數據的長度
flags 調用方式,通常為 0

在阻塞模式下,send 將會阻塞線程的執行直到所有的數據發送完畢(或者發生錯誤),而 recv 函數將返回盡可能多的當前可用信息,直到達到緩沖區指定的大小。

關閉套接字

當不使用 socket 創建的套接字時,應該調用 closesocket() 函數將它關閉。

int
WSAAPI
closesocket(
    _In_ SOCKET s
    );
參數 說明
s 套接字句柄

TCP 套接字樣例

功能設計

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

程序工作流程

由於前面的流程是對於單個客戶端或服務器的編碼流程,這里給出一組客戶端和服務器工作的流程。

編碼實現

注意無論是客戶端還是服務器,都需要包含頭文件 initsock.h 來載入 Winsock。

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 sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sListen == 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(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        cout << "Failed bind()" << endl;
        return 0;
    }

    // 進入監聽模式
    if (::listen(sListen, 2) == SOCKET_ERROR)
    {
        cout << "Failed listen()" << endl;
        return 0;
    }

    // 循環接受客戶的連接請求
    sockaddr_in remoteAddr;
    int nAddrLen = sizeof(remoteAddr);
    SOCKET sClient;
    char szText[] = "你好!";
    while (TRUE)
    {
        cout << "服務端已啟動,正在監聽!\n" << endl;

        // 接受一個新連接
        sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
        if (sClient == INVALID_SOCKET)
        {
            cout << "Failed accept()" << endl;
            continue;
        }

        cout << "與主機 " << inet_ntoa(remoteAddr.sin_addr) << "建立連接:" << endl;

        // 接收數據
        char buff[256];
        int nRecv = ::recv(sClient, buff, 256, 0);
        if (nRecv > 0)
        {
            buff[nRecv] = '\0';
            cout << "接收到數據:" << buff << endl;
        }

        // 向客戶端發送數據
        ::send(sClient, szText, strlen(szText), 0);
        // 關閉同客戶端的連接
        ::closesocket(sClient);
    }

    // 關閉監聽套接字
    ::closesocket(sListen);

    return 0;
}

客戶端

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

CInitSock initSock;     // 初始化Winsock庫

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

    // 也可以在這里調用bind函數綁定一個本地地址,否則系統將會自動安排
    // 填寫遠程地址信息
    sockaddr_in servAddr;
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(4567);
    // 填寫服務器程序(TCPServer程序)所在機器的IP地址
    char serverAddr[] = "127.0.0.1";
    servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);

    //與服務器建立連接
    if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        cout << " Failed connect()" << endl;
        return 0; 
    }

    cout << "與服務器 " << serverAddr << "建立連接" << endl;

    //向服務器發送數據
    char szText[] = "你好,服務器!";
    int slen = send(s, szText, 100, 0);
    if (slen > 0)
    {
        cout << "向服務器發送數據:" << szText << endl;
    } 

    // 接收數據
    char buff[256];
    int nRecv = ::recv(s, buff, 256, 0);
    if (nRecv > 0)
    {
        buff[nRecv] = '\0';
        cout << "接收到數據:" << buff << endl;
    }

    // 關閉套接字
    ::closesocket(s);
    return 0;
}

運行效果

參考資料

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


免責聲明!

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



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