Winsock 編程詳解

  • 2020 年 10 月 9 日
  • 筆記

Winsock 編程

目錄

  1. 通用函數講解
    1. WSAStartup
    2. WSACleanup
    3. socket
    4. closesocket
  2. 面向連接的函數講解
    1. bind
    2. listen
    3. accept
    4. connect
    5. send
    6. recv
  3. 面向非連接的函數講解
    1. sendto
    2. recvfrom
  4. 位元組順序
  5. 獲取本地 ip 填充 sockaddr_in
    1. gethostname
    2. gethostbyname
    3. 獲取本地 ip 填充 sockaddr_in
  6. 入門 TCP/UDP 編程
    1. TCP 基本編程
    2. UDP 基本編程
    3. TCP 封裝
    4. UDP 封裝
  7. 藉助多線程實現 TCP 全雙工通信
  8. TCP 多連接多請求設計
  9. Winsock 其他函數
    1. getsockname
    2. setsockopt
    3. getsockopt
    4. shutdown
    5. ioctlsocket
    6. WSAAsyncSelect

一、通用函數講解

  • 摘要:WSAStartup / WSACleanup / socket / closesocket

1、WSAStartup

  • WSA:即 Windows Sockets Asynchronous,Windows 異步套接字

  • 作用:WSAStartup 必須是應用程序或 DLL 調用的第一個 Windows Sockets 函數。它允許應用程序或 DLL 指明Windows Sockets API 的版本號及獲得特定 Windows Sockets 實現的細節。應用程序或 DLL 只能在一次成功的 WSAStartup() 調用之後才能調用進一步的 Windows Sockets API 函數

  • 函數原型:

    int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
    
  • 參數:

    • wVersionRequested:一個 WORD(雙位元組)型數值,在最高版本的 Windows Sockets 支持調用者使用,高階位元組指定小版本(修訂本)號,低位位元組指定主版本號。當前常用的 Winsock sockets 的版本號為 2.2,用到的 DLL 是 ws2_32.dll,使用 MAKEWORD(a, b) 生成

    • lpWSAData:指向 WSADATA 數據結構的指針,用來接收 Windows Sockets 實現的細節

  • 返回值:

    • 0 成功
    • 否則返回相關錯誤代碼
    • 所有的 Winsock 函數出錯後,都可以調用 WSAGetLastError() 函數得到錯誤碼,但是 WSAStartup()
      不能通過 WSAGetLastError() 得到錯誤碼,因為 WSAStartup() 未調用成功,不能調用 WSAGetLastError() 函數
  • 一些錯誤代碼:

    • WSAVERNOTSUPPORTED:值為 10092,表示所需的 Windows Sockets API 的版本未由特定的Windows Sockets 實現提供
    • WSANOTINITIALISED:值為 10093,在使用此 API 之前應首先成功地調用 WSAStartup()
    • WSAEPROTONOSUPPORT:值為 10043,不支持指定的協議
    • 這裡沒法展示所有錯誤碼,通過 WSAGetLastError() 獲得錯誤碼,通過任意錯誤碼可轉到源碼中查看相關錯誤碼代表內容(如 VS 中右擊 WSANOTINITIALISED,速覽定義或轉到定義可查看源碼)
  • 對於 WSAStartup() / WSACleanup() 和 socket() / closesocket() 這樣的函數,最好保持成對出現

  • 測試:

    WSADATA wsaData;
    cout << WSAStartup(MAKEWORD(2, 2), &wsaData) << endl;		// 0
    cout << WSAStartup(MAKEWORD(512, 512), &wsaData) << endl;	// 10092
    

2、WSACleanup

  • 作用:用於終止 ws2_32.dll 的使用

  • 函數原型:

    int WSACleanup();
    
  • 返回值:

    • 0 成功
    • 否則返回值為 SOCKET_ERROR,可以通過調用 WSAGetLastError() 獲取錯誤代碼
    • SOCKET_ERROR 值為 -1
  • 測試:

    int main()
    {
    	cout << WSACleanup() << endl;		// -1
    	cout << WSAGetLastError() << endl;	// 10093
    	return 0;
    }
    
    int main()
    {
    	WSADATA wsaData;
    	cout << WSAStartup(MAKEWORD(2, 2), &wsaData) << endl;	// 0
    	cout << WSACleanup() << endl;		// 0
    	cout << WSAGetLastError() << endl;	// 0
    	return 0;
    }
    

3、socket

  • 作用:socket() 函數用於根據指定的地址族、數據類型和協議來分配一個套接口的描述字及其所用的資源,創建一個套接口

  • 函數原型:

    SOCKET socket(int af, int type, int protocol);
    
  • 參數:

    • af:一個地址描述,指明地址簇類型,在 Windows 下可以使用的參數值有多個,但是真正可以使用的只有兩個:AF_INET、PF_INET,這兩個宏在 Winsock2.h 下的定義是相同的;AF 表示地址族(Address Family),PF 表示協議族(Protocol Family);一般情況下,在調用 socket() 函數時應該使用 PF_INET,而在設置地址時使用 AF_INET,調用 socket()函數時,應該使用 PF_INET 宏,而盡量避免或不去使用 AF_INET 宏

      #define AF_INET  2	// internetwork: UDP, TCP, etc.
      
      /*
       * Protocol families, same as address families for now.
       */
      #define PF_INET  AF_INET
      
    • type:指定新套接字描述符的類型(指定 socket 類型)。常用的有:

      /*
       * Types
       */
      #define SOCK_STREAM     1		/* stream socket */
      #define SOCK_DGRAM      2		/* datagram socket */
      #define SOCK_RAW        3		/* raw-protocol interface */
      #define SOCK_RDM        4		/* reliably-delivered message */
      #define SOCK_SEQPACKET  5		/* sequenced packet stream */
      
      • SOCK_STREAM:流套接字,用於 TCP 協議

      • SOCK_DGRAM:數據報套接字,用於 UDP 協議

      • SOCK_RAW:原始套接字

    • protocol:指定應用程序所使用的通信協議,該參數取決於 af 和 type 參數的類型。常用的有:

      • IPPROTO_TCP:指明 TCP 協議,af = PF_INET,type = SOCK_STREAM
      • IPPROTO_UDP:指明 UDP 協議,af = PF_INET,type = SOCK_DGRAM
    • 返回值:

      • 若無錯誤發生,socket() 返回引用新套接口的描述字

      • 否則的話,返回 INVALID_SOCKET 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼

      • 測試:

        SOCKET socketServer;
        socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        cout << (socketServer == INVALID_SOCKET) << endl;	// 0
        cout << WSAGetLastError() << endl;					// 0
        
        SOCKET socketServer;
        socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_UDP);
        cout << (socketServer == INVALID_SOCKET) << endl;	// 1
        cout << WSAGetLastError() << endl;					// 10043
        

4、closesocket

  • 作用:用於關閉一個套接口。更確切地說,它釋放套接口描述字 s,以後對 s 的訪問均以 WSAENOTSOCK 錯誤返回

  • 函數原型:

    int closesocket(SOCKET s);
    
  • 參數:

    • s:待關閉的套接字
  • 返回值:

    • 0 成功
    • 否則的話,返回 SOCKET_ERROR 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 測試:

    cout << closesocket(socketServer) << endl;	// 0
    cout << closesocket(socketServer) << endl;	// -1
    

二、面向連接的函數講解

  • 摘要:bind / listen / accept / connect / send / recv

1、bind

  • 作用:bind 將一本地地址與一套接口捆綁。適用於未連接的數據報或流類套接口,在 connect() 或 listen() 調用前使用。當用 socket() 創建套接口後,它便存在於一個名字空間(地址族)中,但並未賦名。bind() 函數通過給一個未命名套接口分配一個本地名字來為套接口建立本地捆綁(主機地址/端口號)

  • 函數原型:

    int bind(SOCKET s, const struct sockaddr* name, int namelen);
    
  • 參數:

    • s:指定一個待綁定的 socket,由 socket() 函數創建
    • name:是一個指向 sockaddr 結構體的指針,稍後進行講解
    • namelen:參數 name 指向對象的位元組數,可通過 sizeof() 得到
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 再看參數 name:

    • 結構體 sockaddr 共 16 位元組:

      struct sockaddr
      {
      	u_short		sa_family;
      	CHAR		sa_data[14];
      };
      
    • 在該結構體之前所使用的結構體為 sockaddr_in:

      struct sockaddr_in
      {
      	short		sin_family;		// 地址族,常為 AF_INET
      	USHORT		sin_port;		// 端口號,要求大端模式
      	IN_ADDR		sin_addr;		// ip 地址
      	CHAR		sin_zero[8];
      };
      
    • sockaddr 結構體是為了保持各個特定協議之間的結構體兼容性而設計的。為 bind() 函數指定地址和端口時,應向 sockaddr_in 結構體填充相應的內容,而調用函數時應該使用 sockaddr 結構體

    • 在 sockaddr_in 結構體中,還有一個結構體 in_addr,用來表示 IP 地址:

      typedef struct in_addr {
      		union {
      			struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
      			struct { USHORT s_w1,s_w2; } S_un_w;
      			ULONG	S_addr;
      		} S_un;
      #define s_addr	S_un.S_addr	/* can be used for most tcp & ip code */
      #define s_host	S_un.S_un_b.s_b2	// host on imp
      #define s_net	S_un.S_un_b.s_b1	// network
      #define s_imp	S_un.S_un_w.s_w2	// imp
      #define s_impno	S_un.S_un_b.s_b4	// imp #
      #define s_lh	S_un.S_un_b.s_b3	// logical host
      } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
      
      • 該結構體中是一個共用體 S_un,包含兩個結構體變量和 1 個 u_long 類型變量,

      • 一般使用的 IP 地址是使用點分十進制表示的,而 in_addr 結構體中卻沒有提供用來保存點分十進制
        表示 IP 地址的數據類型,這時需要使用轉換函數,把點分十進制表示的 IP 地址轉換成 in_addr 結構體可以接受的類型。這裡使用的轉換函數是 inet_addr():

        unsigned long inet_addr(const char* cp);
        
        // 上面函數可能被否定,推薦使用 inet_pton() 函數
        // 頭文件 <WS2tcpip.h>
        INT inet_pton(
        	INT Family,				// 地址族,常為 AF_INET
        	PCSTR pszAddrString,	// 點分 ip 地址字符串,如 "127.0.0.1"
        	PVOID pAddrBuf			// 指向 sockaddr_in::sin_addr
        );
        // 返回 1 成功,0 失敗
        
      • 該函數是將點分十進制表示 IP 地址轉換成 unsigned long 類型的數值。該函數的參數 cp 是指向點分十進制 IP 地址的字符指針。同時該函數有一個逆函數,是將 unsigned long 型的數值型 IP 地址轉換為點分十進制的 IP 地址字符串,該函數的定義如下:

        char* inet_ntoa(in_addr in);
        
        // 上面函數可能被否定,推薦使用 inet_pton() 函數
        // 頭文件 <WS2tcpip.h>
        PCSTR inet_ntop(
        	INT Family,				// 地址族,常為 AF_INET
        	const VOID* pAddr,		// 指向 sockaddr_in::sin_addr
        	PSTR pStringBuf,		// 用於存轉換後的點分 ip 地址字符串,如 "127.0.0.1"
        	size_t StringBufSize	// 緩衝空間大小
        );
        // 返迴轉換後的點分 ip 地址字符串
        
    • sockaddr_in 結構體中的 sin_port 表示端口,這個端口需要使用大端模式位元組序存儲(之後再介紹大端模式和小端模式位元組順序),在 Intel X86 架構下,數值存儲方式默認都是小端模式位元組序,而 TCP/IP 的數值存儲方式都是大端模式的位元組序,為了實現方便的轉換,WinSock2.h 中提供了方便的函數,即 htons() 和 htonl() 兩個函數,並且提供了它們的逆函數 ntohs() 和 ntohl():

      // htons()
      u_short htons(u_short hostshort);
      // htonl()
      u_long htons(u_long hostshort);
      // ntohs
      u_short ntohs(u_short hostshort);
      // ntohl()
      u_long ntohl(u_long hostshort);
      
    • ntoh 指 network to host,hton 指 host to network

  • 具體 bind 的使用方法:

    sockaddr_in serverAddr;				// sockaddr_in 結構
    serverAddr.sin_family = AF_INET;	// 地址族
    serverAddr.sin_port = htons(8082);	// 端口號,使用 htons 轉換到大端模式
    serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");	// 設置 ip
    // 綁定
    bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    
    // 安全版本
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8082);
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);	// inet_pton()
    bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    

2、listen

  • 作用:當套接字與地址端口信息綁定以後,就需要讓端口進行監聽,當端口處於監聽狀態以後就可以接受其他主機的連接了。監聽端口和接受連接請求的函數分別為 listen() 和 accept()

  • 函數原型:

    int listen(SOCKET s, int backlog);
    
  • 參數:

    • s:指定要監聽的套接字描述符

    • backlog:允許進入請求連接隊列的個數,backlog 的最大值由系統指定,在 winsock2.h 中,其最大值
      由 SOMAXCONN 表示:

      #define SOMAXCONN 0x7fffffff
      
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼

3、accept

  • 作用:該函數從連接請求隊列中獲得連接信息,創建新的套接字描述符,獲取客戶端地址。新創建的套接字用於和客戶端進行通信,在服務器和客戶端通信完成後,該套接字也需要使用 closesocket() 函數進行關閉,以釋放相應的資源

  • 函數原型:

    SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
    
  • 參數:

    • s:處於監聽的套接字描述符
    • addr:指向 sockaddr 結構體的指針,用來返回客戶端的地址信息
    • addrlen:指向 int 型的指針變量,用來傳入 sockaddr 結構體的大小
  • 返回值:

    • 如果不發生錯誤,accept 將返回一個新的 SOCKET 描述符,即新建連接的 socket 句柄
    • 否則,將返回 INVALID_SOCKET,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 測試:

    sockaddr_in clientAddr;
    int nSize = sizeof(clientAddr);
    SOCKET socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
    cout << (socketClient == INVALID_SOCKET) << endl;	// 0
    
    sockaddr_in clientAddr;
    int nSize = 1;
    SOCKET socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
    cout << (socketClient == INVALID_SOCKET) << endl;	// 1
    

4、connect

  • 客戶端請求服務端連接

  • 函數原型:

    int connect(SOCKET s, const struct sockaddr* name, int namelen);
    
  • 參數:

    • s:待連接套接字,指服務端套接字
    • name:指明連接地址,指向服務端 sockaddr
    • namelen:指定 sockaddr 結構體的長度,可通過 sizeof() 得到
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 該函數類似 bind

5、send

  • 作用:發送數據

  • 函數原型:

    int send(SOCKET s, const char* buf, int len, int flags);
    
  • 參數:

    • s:套接字描述符,該套接字描述符對於服務器端而言,使用的是 accept() 函數返回的套接字描述符(客戶端套接字),對於客戶端而言,使用的是 socket() 函數創建的套接字描述符(服務端套接字)
    • buf:發送消息的緩衝區
    • len:緩衝區的長度
    • flags:通常賦為 0 值
  • 返回值:

    • 無錯:返回發送位元組數,可能小於 len
    • 出錯:返回 SOCKET_ERROR,可通過 WSAGetLastError() 獲取錯誤碼

6、recv

  • 作用:接收數據

  • 函數原型:

    int recv(SOCKET s, char* buf, int len, int flags);
    
  • 參數:

    • s:套接字描述符,該套接字描述符對於服務器端而言,使用的是 accept() 函數返回的套接字描述符(客戶端套接字),對於客戶端而言,使用的是 socket() 函數創建的套接字描述符(服務端套接字)
    • buf:發送消息的緩衝區
    • len:緩衝區的長度
    • flags:通常賦為 0 值
  • 返回值:

    • 正常:返回已接收數據長度
    • 出錯:返回 SOCKET_ERROR,可通過 WSAGetLastError() 獲取錯誤碼
    • 如果接收緩衝區沒有數據可接收,則 recv 將阻塞直至有數據到達

三、面向非連接的函數講解

  • 摘要:sendto / recvfrom
  • 面向非連接的協議 UDP 服務端不需要 listen & accept,客戶端不需要 connet

1、sendto

  • 作用:指向一指定目的地發送數據,sendto() 適用於發送未建立連接的 UDP 數據包

  • 函數原型:

    int sendto(
    	SOCKET s,
    	const char* buf,
    	int len,
    	int flags,
    	const struct sockaddr* to,
    	int tolen
    );
    
  • 參數:

    • s:自身套接字
    • buf:發送消息的緩衝區
    • len:緩衝區的長度
    • flags:通常賦為 0 值
    • to:指向 sockaddr 結構體的指針,對服務端而言是客戶端地址,對客戶端而言是服務端地址
    • tolen:sockaddr 結構體大小,可通過 sizeof() 獲得
  • 返回值:

    • 成功則返回實際傳送出去的字符數
    • 失敗返回 SOCKET_ERROR,可通過 WSAGetLastError() 獲取錯誤碼

2、recvfrom

  • 作用:接收一個數據報並保存源地址

  • 函數原型:

    int recvfrom(
    	SOCKET s,
    	char* buf,
    	int len,
    	int flags,
    	struct sockaddr* from,
    	int* fromlen
    );
    
  • 參數:

    • s:自身套接字
    • buf:接收消息的緩衝區
    • len:緩衝區的長度
    • flags:通常賦為 0 值
    • from:指向 sockaddr 結構體的指針,對服務端而言是客戶端地址,對客戶端而言是服務端地址
    • fromlen:指向 sockaddr 結構體大小的整形指針
  • 返回值:

    • 正常:返回已接收數據長度
    • 出錯:返回 SOCKET_ERROR,可通過 WSAGetLastError() 獲取錯誤碼
    • 如果接收緩衝區沒有數據可接收,則 recv 將阻塞直至有數據到達

四、位元組順序

  • 通常情況下,數值在內存中存儲的方式有兩種,一種是大端方式(大端方式字序就是網絡位元組序),另一種是小端方式

  • 大端方式:高位地址存放低位數據,低位地址存放高位數據

  • 小端方式:高位地址存放高位數據,低位地址存放低位數據

  • 示例(驗證本機的位元組順序):

    int val = 0x01020304;
    const char* p = reinterpret_cast<char*>(&val);
    for (int i = 0; i < 4; ++i)
    {
    	cout << "0x" << (void*)(p + i) << " -- " << static_cast<int>(p[i]) << endl;
    }
    // 輸出
    0x0093FD1C -- 4
    0x0093FD1D -- 3
    0x0093FD1E -- 2
    0x0093FD1F -- 1
    
    • 可見,高位地址存放高位數據,低位地址存放低位數據,該機器是小端方式位元組順序
  • 網絡位元組序是大端方式

五、獲取本地 ip 填充 sockaddr_in

  • 摘要:gethostname / gethostbyname

1、gethostname

  • 作用:該函數把本地主機名存放入由 name 參數指定的緩衝區中。返回的主機名是一個以 NULL 結束的字符串。主機名的形式取決於 Windows Sockets 實現:它可能是一個簡單的主機名,或者是一個域名。然而,返回的名字必定可以在 gethostbyname() 和 WSAAsyncGetHostByName() 中使用

  • 函數原型:

    int gethostname(char* name, int namelen);
    
  • 參數:

    • name:接收緩衝區
    • namelen:緩衝區大小
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR,應用程序可以通過 WSAGetLastError() 來得到一個特定的錯誤代碼

2、gethostbyname

  • gethostbyname() 返回對應於給定主機名的包含主機名字和地址信息的 hostent 結構的指針

  • 函數原型:

    struct hostent* gethostbyname(const char* name);
    
  • 參數:

    • name:指向主機名的指針,可由 gethostname 獲得
  • 返回值:

    • 如果沒有錯誤發生,gethostbyname() 返回如上所述的一個指向 hostent 結構的指針
    • 否則,返回一個空指針。應用程序可以通過 WSAGetLastError() 來得到一個特定的錯誤代碼
  • hostent:

    struct hostent {
    char*   h_name;					/* official name of host */
    char**  h_aliases;				/* alias list */
    short   h_addrtype;				/* host address type */
    short   h_length;				/* length of address */
    char**  h_addr_list;			/* list of addresses */
    #define h_addr h_addr_list[0]	/* address, for backward compat */
    };
    

3、獲取本地 ip 填充 sockaddr_in

  • gethostbyname 可能被否定,使用宏消除警告:#define _WINSOCK_DEPRECATED_NO_WARNINGS

    char hostname[MAXBYTE];
    gethostname(hostname, MAXBYTE);
    
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8082);
    serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;
    

六、入門 TCP/UDP 編程

1、TCP 基本編程

/*
 * TCP 服務端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET socketServer;
	socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (socketServer == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

	if (bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) return WSAGetLastError();
	if (listen(socketServer, 1) != 0) return WSAGetLastError();

	sockaddr_in clientAddr;
	int nSize = sizeof(clientAddr);
	SOCKET socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
	if (socketClient == INVALID_SOCKET) return WSAGetLastError();

	char ipbuf[20];
	cout << "客戶端成功接入,ip - port: " << inet_ntop(AF_INET, &clientAddr.sin_addr, ipbuf, 20) << " - " << ntohs(clientAddr.sin_port) << endl;

	cout << "send: server send" << endl;
	cout << "send ret: " << send(socketClient, "server send", strlen("server send") + 1, 0) << endl;
	char buf[256];
	cout << "recv ret: " << recv(socketClient, buf, 256, 0) << endl;
	cout << "recv: " << buf << endl;

	closesocket(socketClient);
	closesocket(socketServer);
	WSACleanup();
	return 0;
}


/*
 * TCP 客戶端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->connet()->send()/recv()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET socketServer;
	socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (socketServer == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

	if (connect(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) return WSAGetLastError();

	char buf[256];
	cout << "recv ret: " << recv(socketServer, buf, 256, 0) << endl;
	cout << "recv: " << buf << endl;
	cout << "send: client send" << endl;
	cout << "send ret: " << send(socketServer, "client send", strlen("client send") + 1, 0) << endl;

	closesocket(socketServer);
	WSACleanup();
	return 0;
}
/*
 * TCP 服務端程序輸出
*/
客戶端成功接入,ip - port: 127.0.0.1 - 49291
send: server send
send ret: 12
recv ret: 12
recv: client send

/*
 * TCP 客戶端程序輸出
*/
recv ret: 12
recv: server send
send: client send
send ret: 12

2、UDP 基本編程

/*
 * UDP 服務端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->bind()->sendto()/recvfrom()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET mySocket;
	mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (mySocket == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

	if (bind(mySocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) return WSAGetLastError();

	sockaddr_in clientAddr;
	int nSize = sizeof(clientAddr);

	char buf[256];
	cout << "recvfrom ret: " << recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&clientAddr, &nSize) << endl;
	cout << "recvfrom: " << buf << endl;

	char ipbuf[20];
	cout << "客戶端成功接入,ip - port: " << inet_ntop(AF_INET, &clientAddr.sin_addr, ipbuf, 20) << " - " << ntohs(clientAddr.sin_port) << endl;

	cout << "sendto: server send" << endl;
	cout << "sendto ret: " << sendto(mySocket, "server send", strlen("server send") + 1, 0, (SOCKADDR*)&clientAddr, sizeof(clientAddr)) << endl;

	closesocket(mySocket);
	WSACleanup();
	return 0;
}


/*
 * UDP 客戶端程序
*/
#include <iostream>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")

/* WSAStartup()->socket()->sendto()/recvfrom()->closesocket()->WSACleanup() */

int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return WSAGetLastError();

	SOCKET mySocket;
	mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (mySocket == INVALID_SOCKET) return WSAGetLastError();

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8082);
	inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	int nSize = sizeof(serverAddr);

	cout << "sendto: server send" << endl;
	cout << "sendto ret: " << sendto(mySocket, "server send", strlen("server send") + 1, 0, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) << endl;
	char buf[256];
	cout << "recvfrom ret: " << recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&serverAddr, &nSize) << endl;
	cout << "recvfrom: " << buf << endl;

	closesocket(mySocket);
	WSACleanup();
	return 0;
}
/*
 * UDP 服務端程序輸出
*/
recvfrom ret: 12
recvfrom: server send
客戶端成功接入,ip - port: 127.0.0.1 - 59939
sendto: server send
sendto ret: 12

/*
 * UDP 客戶端程序輸出
*/
sendto: server send
sendto ret: 12
recvfrom ret: 12
recvfrom: server send

3、TCP 封裝

/*
 * TCP 服務端程序
*/
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */

class TCPServer
{
public:
	TCPServer()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socketServer == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		char hostname[MAXBYTE];
		gethostname(hostname, MAXBYTE);
		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;

		if (bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) clear_and_exit();
		if (listen(socketServer, 1) != 0) clear_and_exit();
	}

	~TCPServer()
	{
		closesocket(socketClient);
		closesocket(socketServer);
		WSACleanup();
	}

	void Accept()
	{
		int nSize = sizeof(clientAddr);
		socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
		if (socketClient == INVALID_SOCKET) clear_and_exit();
	}

	auto Send(std::string toSend)
	{
		return send(socketClient, toSend.c_str(), strlen(toSend.c_str()) + 1, 0);
	}

	auto Recv()
	{
		char buf[MAXBYTE];
		recv(socketClient, buf, MAXBYTE, 0);
		return std::string(buf);
	}
	auto Recv(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recv(socketClient, buf, MAXBYTE, 0);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp(bool client = true)
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &(client ? clientAddr : serverAddr).sin_addr, ipBuf, 20));
	}
	auto getPort(bool client = true)
	{
		return ntohs((client ? clientAddr : serverAddr).sin_port);
	}
	auto getIpPort(bool client = true)
	{
		return getIp(client) + " - " + std::to_string(getPort(client));
	}

private:
	void clear_and_exit()
	{
		auto code = WSAGetLastError();
		closesocket(socketServer);
		WSACleanup();
		exit(auto code = WSAGetLastError(););
	}

private:
	WSADATA wsaData;
	SOCKET socketServer;
	sockaddr_in serverAddr;
	SOCKET socketClient;
	sockaddr_in clientAddr;
};

int main()
{
	TCPServer server;
	cout << "local ip - port: " << server.getIpPort(false) << endl;

	server.Accept();
	cout << "客戶端成功接入 ip - port: " << server.getIpPort() << endl;

	cout << "Send: " << "Hello Client" << endl;
	server.Send("Hello Client");
	cout << "Recv: " << server.Recv() << endl;

	return 0;
}


/*
 * TCP 客戶端程序
*/
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->connet()->send()/recv()->closesocket()->WSACleanup() */

class TCPClient
{
public:
	TCPClient()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socketServer == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		inet_pton(AF_INET, "192.168.43.5", &serverAddr.sin_addr);
	}

	~TCPClient()
	{
		closesocket(socketServer);
		WSACleanup();
	}

	void Connect()
	{
		if (connect(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0)
		{
			auto code = WSAGetLastError();
			closesocket(socketServer);
			WSACleanup();
			exit(code);
		}
	}

	auto Send(std::string toSend)
	{
		return send(socketServer, toSend.c_str(), strlen(toSend.c_str()) + 1, 0);
	}

	auto Recv()
	{
		char buf[MAXBYTE];
		recv(socketServer, buf, MAXBYTE, 0);
		return std::string(buf);
	}
	auto Recv(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recv(socketServer, buf, MAXBYTE, 0);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp()
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &serverAddr.sin_addr, ipBuf, 20));
	}
	auto getPort()
	{
		return ntohs(serverAddr.sin_port);
	}
	auto getIpPort()
	{
		return getIp() + " - " + std::to_string(getPort());
	}

private:
	WSADATA wsaData;
	SOCKET socketServer;
	sockaddr_in serverAddr;
};

int main()
{
	TCPClient client;

	client.Connect();
    
	cout << "Recv: " << client.Recv() << endl;
	cout << "Send: " << "Hello Server" << endl;
	client.Send("Hello Server");

	return 0;
}
/*
 * TCP 服務端程序輸出
*/
local ip - port: 192.168.43.5 - 8082
客戶端成功接入 ip - port: 192.168.43.5 - 51249
Send: Hello Client
Recv: Hello Server

/*
 * TCP 客戶端程序輸出
*/
Recv: Hello Client
Send: Hello Server

4、UDP 封裝

/*
 * UDP 服務端程序
*/
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->bind()->sendto()/recvfrom()->closesocket()->WSACleanup() */

class UDPServer
{
public:
	UDPServer()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if (mySocket == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		char hostname[MAXBYTE];
		gethostname(hostname, MAXBYTE);
		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;

		if (bind(mySocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0)
		{
			auto code = WSAGetLastError();
			closesocket(mySocket);
			WSACleanup();
			exit(code);
		}
	}

	~UDPServer()
	{
		closesocket(mySocket);
		WSACleanup();
	}

	auto Sendto(std::string toSend)
	{
		return sendto(mySocket, toSend.c_str(), strlen(toSend.c_str()) + 1, 0, (SOCKADDR*)&clientAddr, sizeof(clientAddr));
	}

	auto Recvfrom()
	{
		char buf[MAXBYTE];
		recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&clientAddr, &nSize);
		return std::string(buf);
	}
	auto Recv(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recvfrom(mySocket, buf, 256, 0, (SOCKADDR*)&clientAddr, &nSize);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp(bool client = true)
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &(client ? clientAddr : serverAddr).sin_addr, ipBuf, 20));
	}
	auto getPort(bool client = true)
	{
		return ntohs((client ? clientAddr : serverAddr).sin_port);
	}
	auto getIpPort(bool client = true)
	{
		return getIp(client) + " - " + std::to_string(getPort(client));
	}

private:
	WSADATA wsaData;
	SOCKET mySocket;
	sockaddr_in serverAddr;
	sockaddr_in clientAddr;
	int nSize = sizeof(clientAddr);
};

int main()
{
	UDPServer server;
	cout << "local ip - port: " << server.getIpPort(false) << endl;

	cout << "Recv: " << server.Recvfrom() << endl;
	cout << "客戶端成功接入 ip - port: " << server.getIpPort() << endl;
	cout << "Send: " << "Hello Client" << endl;
	server.Sendto("Hello Client");

	return 0;
}


/*
 * UDP 客戶端程序
*/
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>

/* WSAStartup()->socket()->sendto()/recvfrom()->closesocket()->WSACleanup() */

class UDPClient
{
public:
	UDPClient()
	{
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);

		mySocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if (mySocket == INVALID_SOCKET)
		{
			auto code = WSAGetLastError();
			WSACleanup();
			exit(code);
		}

		serverAddr.sin_family = AF_INET;
		serverAddr.sin_port = htons(8082);
		inet_pton(AF_INET, "192.168.43.5", &serverAddr.sin_addr);
	}

	~UDPClient()
	{
		closesocket(mySocket);
		WSACleanup();
	}

	auto Sendto(std::string toSend)
	{
		return sendto(mySocket, toSend.c_str(), strlen(toSend.c_str()) + 1, 0, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
	}

	auto Recvfrom()
	{
		char buf[MAXBYTE];
		recvfrom(mySocket, buf, MAXBYTE, 0, (SOCKADDR*)&serverAddr, &nSize);
		return std::string(buf);
	}
	auto Recvfrom(std::string& toRecv, bool append = false)
	{
		char buf[MAXBYTE];
		auto ret = recvfrom(mySocket, buf, MAXBYTE, 0, (SOCKADDR*)&serverAddr, &nSize);
		if (!append) toRecv = std::string(buf);
		else toRecv.append(buf);
		return ret;
	}

	auto getIp()
	{
		char ipBuf[20];
		return std::string(inet_ntop(AF_INET, &serverAddr.sin_addr, ipBuf, 20));
	}
	auto getPort()
	{
		return ntohs(serverAddr.sin_port);
	}
	auto getIpPort()
	{
		char ipBuf[20];
		return getIp() + " - " + std::to_string(getPort());
	}

private:
	WSADATA wsaData;
	SOCKET mySocket;
	sockaddr_in serverAddr;
	int nSize = sizeof(serverAddr);
};

int main()
{
	UDPClient client;

	cout << "Send: " << "Hello Server" << endl;
	client.Sendto("Hello Server");
	cout << "Recv: " << client.Recvfrom() << endl;

	return 0;
}
/*
 * UDP 服務端程序輸出
*/
local ip - port: 192.168.43.5 - 8082
Recv: Hello Server
客戶端成功接入 ip - port: 192.168.43.5 - 54075
Send: Hello Client

/*
 * UDP 客戶端程序輸出
*/
Send: Hello Server
Recv: Hello Client

七、藉助多線程實現 TCP 全雙工通信

// 為避免標準輸出的競爭,發送消息通過檢測按鍵,將相應值拷貝四次後進行發送
// 如:按下 1,發送為 11111

/*
 * TCP 服務端程序
 * TCPServer 類見 [六-3] TCP 封裝
*/
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>
#include <thread>
#include <atomic>
#include <conio.h>

/* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */

// 同 [六-3] TCP 封裝
class TCPServer;

int main()
{
	TCPServer server;
	cout << "local ip - port: " << server.getIpPort(false) << endl << endl;
	cout << "等待客戶端接入..." << endl;

	std::atomic_bool disconnect = false;
	std::atomic_flag ostream_spin_lock = ATOMIC_FLAG_INIT;

	while (true)
	{
		server.Accept();
		cout << "客戶端成功接入, ip - port: " << server.getIpPort() << endl;
		disconnect = false;

		std::thread thread_recv(
			[&] {
				while (true)
				{
					std::string recvMsg;
					auto recvRet = server.Recv(recvMsg);
					if (recvRet == SOCKET_ERROR)
					{
						disconnect.store(true);
						return;
					}
					while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
					cout << "Recv:\n\t" << recvMsg << endl;
					ostream_spin_lock.clear();
				}
			}
		);
		thread_recv.detach();

		while (!disconnect.load() && !(GetAsyncKeyState(VK_ESCAPE) & 0x8000))
		{
			if (_kbhit())
			{
				std::string sendMsg(5, static_cast<char>(_getch()));
				server.Send(sendMsg);
				while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
				cout << "Send:\n\t" << sendMsg << endl;
				ostream_spin_lock.clear();
			}
			std::this_thread::sleep_for(std::chrono::milliseconds(10));
		}

		cout << "客戶端斷開連接,等待重新接入..." << endl << endl;
	}

	return 0;
}


/*
 * TCP 客戶端程序
 * TCPClient 類見 [六-3] TCP 封裝
*/
#include <iostream>

using std::cout;
using std::endl;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment (lib, "ws2_32")
#include <string>
#include <thread>
#include <atomic>
#include <conio.h>

/* WSAStartup()->socket()->connet()->send()/recv()->closesocket()->WSACleanup() */

// 同 [六-3] TCP 封裝
class TCPClient;

int main()
{
	TCPClient client;

	std::atomic_bool disconnect = false;
	std::atomic_flag ostream_spin_lock = ATOMIC_FLAG_INIT;

	client.Connect();
	cout << "成功接入服務端,ip - port: " << client.getIpPort() << endl;
	disconnect = false;

	std::thread thread_recv(
		[&] {
			while (true)
			{
				std::string recvMsg;
				auto recvRet = client.Recv(recvMsg);
				if (recvRet == SOCKET_ERROR)
				{
					disconnect.store(true);
					return;
				}
				while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
				cout << "Recv:\n\t" << recvMsg << endl;
				ostream_spin_lock.clear();
			}
		}
	);
	thread_recv.detach();

	while (!disconnect.load() && !(GetAsyncKeyState(VK_ESCAPE) & 0x8000))
	{
		if (_kbhit())
		{
			std::string sendMsg(5, static_cast<char>(_getch()));
			client.Send(sendMsg);
			while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
			cout << "Send:\n\t" << sendMsg << endl;
			ostream_spin_lock.clear();
		}
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}

	cout << "服務端斷開連接..." << endl;

	return 0;
}
// 客戶端發送 1,服務端發送 2,客戶端發送 3
// 此後客戶端斷開重連後重複上面操作

/*
 * TCP 服務端程序輸出
*/
local ip - port: 192.168.43.5 - 8082

等待客戶端接入...
客戶端成功接入, ip - port: 192.168.43.5 - 53231
Recv:
	11111
Send:
	22222
Recv:
	33333
客戶端斷開連接,等待重新接入...

客戶端成功接入, ip - port: 192.168.43.5 - 53233
Recv:
	11111
Send:
	22222
Recv:
	33333

/*
 * TCP 客戶端程序輸出
*/
成功接入服務端,ip - port: 192.168.43.5 - 8082
Send:
	11111
Recv:
	22222
Send:
	33333
// 斷開重連
成功接入服務端,ip - port: 192.168.43.5 - 8082
Send:
	11111
Recv:
	22222
Send:
	33333

八、TCP 多連接多請求設計

  1. 試想,有許多客戶端向服務端發起請求,服務端需要自動返回相關內容怎麼辦?

  2. 思考幾個問題:

    • 如何處理眾多客戶端的連接請求,也就是如何使用阻塞模式的 accept
    • 如何接收眾多客戶端的消息,並自動返回返回內容,注意 recv 是阻塞的
    • 用什麼數據結構存儲眾多客戶端的 SOCKET 以及 sockaddr_in
    • 某個客戶端斷開連接後通過什麼途徑釋放資源,也就是如何使用 closesocket
  3. 先假設一個通信場景:

    • 為避免競爭 IO 流,直接處理客戶端按鍵消息,如:按下 1 則發送 11111
    • 服務器將信息接收消息追加返回,如收到 “11111”,返回 “1111111111”
  4. 一個解決方案:

    • 用一個線程 thread1 循環執行 accept
    • thread1 線程執行 accept 成功後打印新連接信息,並生成一個線程 threadx 循環執行 recv,threadx 根據 recv 內容進行返回內容,同時 thread1 將線程 threadx 的 id 與得到的 SOCKET 和 sockaddr_in 進行綁定放入哈希表,key 為 thread_id,value 為 pair 對象,存儲 SOCKET 和 sockaddr_in
    • 線程 threadx 執行 recv 及 send 時的參數可通過自身 thread_id 檢索哈希表得到,當 recv 返回 SOCKET_ERROR 認定對方斷開,打印連接結束信息,並調用 closesocket,然後刪除哈希表中的記錄,最後結束線程自身
  5. 上述方案存在的問題:

    • 輸出流競爭:這裡採用自旋鎖解決

    • 多線程共享哈希表存在競爭,如線信息存入哈希表時,可能存在其他線程從哈希表刪除內容,若 thread_id 哈希值相同,則存在同時修改同一個位置的情況,因此需要設計一個安全的哈希表

    • 一個簡單的並發哈希表設計:這裡為了簡單,將插入哈希表操作、從哈希表刪除操作和讀操作進行串行化,即三個操作共用一把互斥鎖(也可用自旋鎖,兩種鎖的區別可自行學習)。帶來的問題是對並發的不友好:當 thread_id 的哈希值不同時,多個線程插入、刪除和讀操作不會發生競爭(除開擴容操作),但任然被串行化

  6. 代碼演示:

    /*
     * TCP 服務端程序
    */
    #define _WINSOCK_DEPRECATED_NO_WARNINGS
    #include <iostream>
    
    using std::cout;
    using std::endl;
    
    #include <WinSock2.h>
    #include <WS2tcpip.h>
    #pragma comment (lib, "ws2_32")
    #include <string>
    #include <thread>
    #include <mutex>
    #include <atomic>
    #include <conio.h>
    #include <unordered_map>
    
    /* WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->WSACleanup() */
    
    class TCPServer
    {
    public:
    	TCPServer()
    	{
    		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) exit(EXIT_FAILURE);
    
    		socketServer = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    		if (socketServer == INVALID_SOCKET)
    		{
    			auto code = WSAGetLastError();
    			WSACleanup();
    			exit(code);
    		}
    
    		char hostname[MAXBYTE];
    		gethostname(hostname, MAXBYTE);
    		serverAddr.sin_family = AF_INET;
    		serverAddr.sin_port = htons(8082);
    		serverAddr.sin_addr.S_un.S_addr = ((struct in_addr*)gethostbyname(hostname)->h_addr)->s_addr;
    
    		if (bind(socketServer, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) != 0) clear_and_exit();
    		if (listen(socketServer, SOMAXCONN) != 0) clear_and_exit();
    	}
    
    	~TCPServer()
    	{
    		closesocket(socketServer);
    		WSACleanup();
    	}
    
    	void Accept(SOCKET& socketClient, sockaddr_in& clientAddr)
    	{
    		int nSize = sizeof(clientAddr);
    		socketClient = accept(socketServer, (SOCKADDR*)&clientAddr, &nSize);
    		if (socketClient == INVALID_SOCKET) clear_and_exit();
    	}
    
    	auto Send(std::string toSend, SOCKET socketClient)
    	{
    		return send(socketClient, toSend.c_str(), strlen(toSend.c_str()) + 1, 0);
    	}
    
    	auto Recv(SOCKET socketClient)
    	{
    		char buf[MAXBYTE];
    		recv(socketClient, buf, MAXBYTE, 0);
    		return std::string(buf);
    	}
    	auto Recv(std::string& toRecv, SOCKET socketClient, bool append = false)
    	{
    		char buf[MAXBYTE];
    		auto ret = recv(socketClient, buf, MAXBYTE, 0);
    		if (!append) toRecv = std::string(buf);
    		else toRecv.append(buf);
    		return ret;
    	}
    
    	auto getIp(const sockaddr_in& clientAddr)
    	{
    		char ipBuf[20];
    		return std::string(inet_ntop(AF_INET, &clientAddr.sin_addr, ipBuf, 20));
    	}
    	auto getPort(const sockaddr_in& clientAddr)
    	{
    		return ntohs(clientAddr.sin_port);
    	}
    	auto getIpPort(const sockaddr_in& clientAddr)
    	{
    		return getIp(clientAddr) + " - " + std::to_string(getPort(clientAddr));
    	}
    
    	void insertToMap(std::thread::id id, SOCKET s, sockaddr_in addr)
    	{
    		std::lock_guard<std::mutex> auto_lock(mtx);
    		mp.insert({ id, std::make_pair(s,addr) });
    	}
    	void eraseFromMap(std::thread::id id)
    	{
    		std::lock_guard<std::mutex> auto_lock(mtx);
    		mp.erase(id);
    	}
    	auto getFromMap(std::thread::id id)
    	{
    		std::lock_guard<std::mutex> auto_lock(mtx);
    		return mp[id];
    	}
    
    private:
    	void clear_and_exit()
    	{
    		auto code = WSAGetLastError();
    		closesocket(socketServer);
    		WSACleanup();
    		exit(code);
    	}
    
    private:
    	WSADATA wsaData;
    	SOCKET socketServer;
    	sockaddr_in serverAddr;
    	std::unordered_map<std::thread::id, std::pair<SOCKET, sockaddr_in>> mp;
    	std::mutex mtx;
    };
    
    int main()
    {
    	TCPServer server;
    
    	std::atomic_flag ostream_spin_lock = ATOMIC_FLAG_INIT;
    
    	auto recv_send = [&]
    	{
    		while (true)
    		{
    			std::string recvMsg;
    			auto clientMsg = server.getFromMap(std::this_thread::get_id());
    			auto recvRet = server.Recv(recvMsg, clientMsg.first);
    			if (recvRet == SOCKET_ERROR)
    			{
    				while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
    				cout << "連接結束: " << server.getIpPort(clientMsg.second) << endl;
    				ostream_spin_lock.clear();
    				closesocket(clientMsg.first);
    				server.eraseFromMap(std::this_thread::get_id());
    				return;
    			}
    			else server.Send(recvMsg + recvMsg, clientMsg.first);
    		}
    	};
    
    	while (true)
    	{
    		SOCKET socketClient;
    		sockaddr_in clientAddr;
    		server.Accept(socketClient, clientAddr);
    
    		while (!ostream_spin_lock.test_and_set()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
    		cout << "新連接接入: " << server.getIpPort(clientAddr) << endl;
    		ostream_spin_lock.clear();
    
    		std::thread newThread(recv_send);
    		server.insertToMap(newThread.get_id(), socketClient, clientAddr);
    
    		newThread.detach();
    	}
    
    	return 0;
    }
    
    /*
     * TCP 客戶端端程序見 [六-3] TCP 封裝
    */
    
  7. 輸出演示:運行服務端,然後依次開啟三個客戶端,然後三個客戶端依次按下 1,2,3,最後逆序關閉三個客戶端

    /*
     * 客戶端 1 輸出
    */
    成功接入服務端,ip - port: 192.168.43.5 - 8082
    Send:
    	11111
    Recv:
    	1111111111
    /*
     * 客戶端 2 輸出
    */
    成功接入服務端,ip - port: 192.168.43.5 - 8082
    Send:
    	22222
    Recv:
    	2222222222
    /*
     * 客戶端 3 輸出
    */
    成功接入服務端,ip - port: 192.168.43.5 - 8082
    Send:
    	33333
    Recv:
    	3333333333
    /*
     * 服務端輸出
    */
    新連接接入: 192.168.43.5 - 56168
    新連接接入: 192.168.43.5 - 56170
    新連接接入: 192.168.43.5 - 56172
    連接結束: 192.168.43.5 - 56172
    連接結束: 192.168.43.5 - 56170
    連接結束: 192.168.43.5 - 56168
    

九、Winsock 其他函數

  • 摘要:getsockname / shutdown / setsockopt / getsockopt / ioctlsocket / WSAAsyncSelect

1、getsockname

  • 作用:getsockname() 函數用於獲取一個套接字的名字。它用於一個已捆綁或已連接套接字 s,本地地址將被返回。本調用特別適用於如下情況:未調用 bind() 就調用了 connect(),這時唯有 getsockname() 調用可以獲知系統內定的本地地址。在返回時,namelen 參數包含了名字的實際位元組數

  • 函數原型:

    int getsockname(SOCKET s, struct sockaddr* name, int* namelen);
    
  • 參數:

    • s:標識一個已捆綁套接口的描述字
    • name:接收套接口的地址
    • namelen:名字緩衝區長度
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 使用:返回本地 ip-port,如在 TCP 客戶端 connect 後調用

    auto getLocalMsg()
    {
    	sockaddr_in localAddr;
    	int len = sizeof(localAddr);
    	if (getsockname(socketServer, (SOCKADDR*)&localAddr, &len) == 0)
    	{
    		char buf[20];
    		return std::string(inet_ntop(AF_INET, &localAddr.sin_addr, buf, 20)) + " - " + std::to_string(ntohs(localAddr.sin_port));
    	}
    	else return std::string();
    }
    

2、shutdown

  • 作用:禁止在一個套接口上進行數據的接收與發送

  • 函數原型:

    int shutdown(SOCKET s, int how);
    
  • 參數:

    • s:用於標識一個套接口的描述字
    • how:標誌,用於描述禁止哪些操作。取值及其意義如下:
      • SD_RECEIVE(0):關閉 s 上的讀操作,後續接收操作將被禁止。這對於低層協議無影響。對於 TCP 協議,TCP 窗口不改變並接收前來的數據(但不確認)直至窗口滿。對於 UDP 協議,接收並排隊前來的數據。任何情況下都不會產生 ICMP 錯誤包
      • SD_SEND(1):關閉 s 上的寫操作,禁止後續發送操作。對於TCP,將發送FIN
      • SD_BOTH(2):關閉 s 上的讀寫操作,同時禁止收和發
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼

3、setsockopt

  • 作用:用於任意類型、任意狀態套接口的設置選項值。儘管在不同協議層上存在選項,但本函數僅定義了最高的 「套接口」 層次上的選項

  • 函數原型:

    int setsockopt(
    	SOCKET s,
    	int level,
    	int optname,
    	const char* optval,
    	int optlen
    );
    
  • 參數:

    • s:用於標識一個套接口的描述字
    • level:選項定義的層次。目前僅支持 SOL_SOCKET 和 IPPROTO_TCP 層次。分別表示 Socket 屬性和 TCP 屬性
    • optname:需設置的選項
    • optval:指向存放選項值的緩衝區
    • optlen:指向 optval 緩衝區的長度值
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 更詳細的內容請參考:百度詞條 setsockopt

  • 這裡說一個應用場景:單片機與 TCP 後端通信,當單片機斷電後,後端並不能收到任何消息,若單片機會定時發送數據給後端,則可通過 setsockopt 設置接收超時返回,而不是一直阻塞在 recv() 處,如下:

    int nNetTimeout = 1000;		// 1 秒
    setsockopt(socketServer, SOL_SOCKET, SO_RCVTIMEO, (char*)&nNetTimeout, sizeof(int));
    

4、getsockopt

  • 作用:用於獲取任意類型、任意狀態套接口的選項當前值,並把結果存入 optval

  • 函數原型:

    int getsockopt(
    	SOCKET s,
    	int level,
    	int optname,
    	char* optval,
    	int* optlen
    );
    
  • 參數:

    • s:用於標識一個套接口的描述字
    • level:選項定義的層次。目前僅支持 SOL_SOCKET 和 IPPROTO_TCP 層次。分別表示 Socket 屬性和 TCP 屬性
    • optname:需獲取的套接口選項
    • optval:指向存放所獲得選項值的緩衝區
    • optlen:指向 optval 緩衝區的長度值
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 更詳細的內容請參考:百度詞條 getsockopt

5、ioctlsocket

  • 作用:控制套接口的模式。可用於任一狀態的任一套接口。它用於獲取與套接口相關的操作參數,而與具體協議或通訊子系統無關

  • 函數原型:

    int ioctlsocket(SOCKET s, long cmd, u_long* argp);
    
  • 參數:

    • s:一個標識套接口的描述字
    • cmd:對套接口 s 的操作命令
    • argp:指向 cmd 命令所帶參數的指針
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 更詳細的內容請參考:百度詞條 ioctlsocket

6、WSAAsyncSelect

  • 作用:用來請求 Windows Sockets DLL 為窗口句柄發一條消息:無論它何時檢測到由 lEvent 參數指明的網絡事件,要發送的消息由 wMsg 參數標明,被通知的套接口由 s 標識

  • 函數原型:

    int WSAAsyncSelect(
    	SOCKET s,
    	HWND hWnd,
    	u_int wMsg,
    	long lEvent
    );
    
  • 參數:

    • s:標識一個需要事件通知的套接口的描述符
    • hWnd:標識一個在網絡事件發生時需要接收消息的窗口句柄
    • wMsg:在網絡事件發生時要接收的消息
    • lEvent:位屏蔽碼,用於指明應用程序感興趣的網絡事件集合
  • 返回值:

    • 0 成功
    • 否則返回 SOCKET_ERROR 錯誤,應用程序可通過 WSAGetLastError() 獲取相應錯誤代碼
  • 更詳細的內容請參考:百度詞條 WSAAsyncSelect