Bootstrap

Windows C/C++ Socket 编程

承接上文:socket 编程

在进行 socket 编程之前,你要有一些计算机网络的知识,了解 TCP/UDP 、客户端服务器模型。

Windows Client 端

Windows socket 编程 client 端 大致如下:

char buffer[buffer_size] = { 0 };      //接收数据的缓存

WSADATA wsaData;
//初始化 Winsock
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {...}

SOCKET sock;
//创建 Socket
if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {...}

struct sockaddr_in server;
// 配置服务器地址结构
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
in_addr serverIP;
if (inet_pton(AF_INT, "IPv4 Address", &server.sin_addr) < 0) {...}
memcpy(&(serverAddr.sin_addr), &serverIP, sizeof(serverIP));

//连接服务器
if (connet(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {...}

// 发送数据
const char* message = "message";
send(sock, message, strlen(message), 0);

// 接收数据
char buffer[BUFFER_SIZE] = { 0 };
int recvResult = recv(sock, buffer, BUFFER_SIZE - 1, 0);

if (recvResult > 0) { //数据接受成功 } 
else if ( recvResult == 0 ) { //连接关闭} 
else { //数据接受失败 }

closesocket(sock);
WSACleanup();

WSADATA 结构体

该结构体定义了与 Winsock 库相关的一些信息。在 Windows 上使用 Socket 编程时,必须先调用 WSAStartup() 函数初始化 Winsock 库,而 WSADATA 结构体则是这个初始化过程的一部分。

WSADATA 结构体在 winsock2.h 头文件中定义,通常包含以下信息:

typedef struct _WSADATA {
    WORD wVersion;         // 使用的 Winsock 版本
    WORD wHighVersion;     // 支持的最高版本
    char szDescription[256]; // 描述字符串
    char szSystemStatus[128]; // 系统状态字符串
    unsigned short iMaxSockets; // 最大套接字数
    unsigned short iMaxUdpDg;   // 最大 UDP 数据报文长度
    char *lpVendorInfo;    // 供应商特定的附加信息
} WSADATA;

WSAStartup() 函数

用于初始化 Winsock 库并准备网络通信的环境。在 Windows 上进行网络编程时,必须先调用 WSAStartup() 才能使用任何与网络相关的功能(如套接字、连接、数据传输等)。

int WSAStartup(
  WORD wVersionRequested,  // 请求的 Winsock 版本
  LPWSADATA lpWSAData      // 指向 WSADATA 结构体的指针
);

WSAStartup() 为我们应用程序提供了使用 Windows 套接字的能力,会为应用程序提供所需的 Winsock 资源,并允许操作系统在程序退出时释放这些资源(通过 WSACleanup())。

返回值:
如果函数调用成功,返回值为 0。
如果函数调用失败,返回值是一个错误代码,表示初始化失败的原因。可以通过调用 WSAGetLastError() 获取详细的错误信息。(WSAGetLastError() 用于获取上一次 Winsock 操作的错误码。)

使用示例:

int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
    std::cerr << "WSAStartup failed: " << iResult << std::endl;
    return 1;
}

MAKEWORD(2, 2) 是一个宏,用于将两个 BYTE(字节)组合成一个 WORD(字,通常是 16 位的整数)。在 Windows 编程中,MAKEWORD 常用于将两个字节值合并为一个 16 位的数值,通常用于指定版本号或类似的参数。

  • 在 MAKEWORD(2, 2) 中,它将 2 和 2 合并,得到表示 “2.2” 版本号的 WORD 值。
  • 在 WSAStartup() 中,MAKEWORD(2, 2) 表示请求使用 Winsock 2.2 版本。

MAKEWORD 宏定义:

#define MAKEWORD(a, b)      ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))

// WORD:
typedef unsigned short      WORD;      // 无符号短整型 2字节
// BYTE:
typedef unsigned char       BYTE;      // 无符号字符型 1字节-8位
  • low:低字节(低 8 位),表示低位。
  • high:高字节(高 8 位),表示高位。

MAKEWORD 的位运算 (了解,不重要可跳过)

  1. (DWORD_PTR)(a)(DWORD_PTR)(b)

    • 这部分将 ab 强制转换为 DWORD_PTR 类型(通常是 unsigned int,在 64 位系统上为 unsigned long long,在 32 位系统上为 unsigned int)。
    • 这个转换的目的是确保 ab 被当作一个整数来处理,方便进行位操作。
  2. (DWORD_PTR)(a) & 0xff(DWORD_PTR)(b) & 0xff

    • 这里使用了 按位与运算符 &
      • 0xff 是一个 8 位的掩码,二进制表示为 11111111
      • 按位与运算 & 用于保留 ab 中的最低 8 位(即低字节)。即使 ab 是更大的类型(比如 DWORD_PTR,它的大小可能是 32 位或 64 位),通过 & 0xff 运算后,它们会被限制在 8 位范围内。
      • 举个例子,如果 a 的值是 0x12345678(32 位整数),a & 0xff 会返回 0x78,即最后一个字节的值。
  3. (BYTE)(((DWORD_PTR)(a)) & 0xff)(BYTE)(((DWORD_PTR)(b)) & 0xff)

    • 这两部分将前面的结果转换为 BYTE 类型。BYTE 是 8 位的类型,确保结果是 8 位整数,去除多余的位数。
  4. (WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff)) << 8

    • b 被处理成一个字节后,通过 左移操作 << 8 将它移到高字节的位置(16 位整数的高 8 位)。
    • 具体来说,左移 8 位的效果是将字节 b 提高 8 位,使其成为 16 位整数的高字节。例如,如果 b = 0x34,那么左移后就是 0x3400
  5. ((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8

    • 按位或运算符 | 用于将 ab 合并成一个 16 位的整数。
      • a 是低字节,b << 8 是高字节,二者按位或运算合并成一个 16 位整数。
      • 举个例子:如果 a = 0x12b = 0x34,那么 a | (b << 8) 的结果是 0x3412
  6. (WORD)

    • 最后,通过强制类型转换将结果转换为 WORD 类型。WORD 是 16 位的整数类型,确保最终结果符合预期的类型。

SOCKET 以及 socket() 函数

SOCKET 是 Windows 套接字编程中用于表示一个网络套接字的类型。它是一个句柄(或标识符),用来标识一个网络连接或通信通道。

  • SOCKET 本质上是一个整数类型(通常是 unsigned int),用于表示一个网络连接。

socket() 函数是创建一个套接字(socket)的函数。它通过指定地址族、套接字类型和协议类型来创建一个网络通信的通道。

SOCKET socket(int af, int type, int protocol);

af (地址族):指定套接字所使用的地址族,决定了套接字能够处理的数据类型(如 IPv4、IPv6 等)。

常用的值:

  • AF_INET:IPv4 地址族,表示使用 IPv4 地址进行通信。
  • AF_INET6:IPv6 地址族,表示使用 IPv6 地址进行通信。
  • AF_UNIX:Unix 域套接字,通常用于同一台机器上的进程间通信。

type (套接字类型):指定套接字的类型,决定了数据的传输方式。

常用的值:

  • SOCK_STREAM:流套接字,表示面向连接的 TCP 协议。适用于可靠的、连接导向的通信(如 HTTP、FTP)。
  • SOCK_DGRAM:数据报套接字,表示无连接的 UDP 协议。适用于不保证可靠性的、无连接的通信(如 DNS 查询、视频流)。
  • SOCK_RAW:原始套接字,允许直接访问网络层协议(通常是 ICMP 或自定义协议)。这种套接字用于较底层的网络操作,常见于网络工具。

protocol (协议):指定所用的协议,通常与 type 配合使用,决定了套接字的协议标准。

常用的值:

  • IPPROTO_TCP:表示 TCP 协议。一般与 SOCK_STREAM 搭配使用。
  • IPPROTO_UDP:表示 UDP 协议。一般与 SOCK_DGRAM 搭配使用。
  • IPPROTO_IP:表示通用的 IP 协议,通常可以与 SOCK_RAW 配合使用。

socket() 返回值

  • 成功时返回:如果套接字创建成功,socket() 函数返回一个 SOCKET 类型的值,这个值是一个非负整数,表示新创建的套接字的句柄。
  • 失败时返回:如果创建套接字失败,返回值是 INVALID_SOCKET,这通常是一个特殊值(在 Windows 中通常为 -1)。(#define INVALID_SOCKET (SOCKET)(~0)

sockaddr_in

sockaddr_in 是一个结构体,用于存储 IPv4 地址和端口信息,通常用于 TCP 或 UDP 套接字编程中。它是 sockaddr 结构的一个变种,专门用于 IPv4。

//
// IPv4 Socket address, Internet style
//

typedef struct sockaddr_in {

#if(_WIN32_WINNT < 0x0600)
    short   sin_family;
#else //(_WIN32_WINNT < 0x0600)
    ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)

    USHORT sin_port;
    IN_ADDR sin_addr;
    CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;
serverAddr.sin_family = AF_INET;      // internetwork: UDP, TCP, etc.
serverAddr.sin_port = htons(PORT);       // htons() 将主机字节序的 IP 端口转换成 网络字节序。
// The htons function can be used to convert an IP port number in host byte order to the IP port number in network byte order.

inet_pton() 函数

该函数用于将标准文本类型的 IPv4 & IPv6 地址转换成二进制形式。

in_addr struct

in_addr 结构体代表了一个 IPv4 地址

in_addr serverIP;
// in_addr 定义:
typedef struct in_addr {
  union {
    struct {
      UCHAR s_b1;
      UCHAR s_b2;
      UCHAR s_b3;
      UCHAR s_b4;
    } S_un_b;
    struct {
      USHORT s_w1;
      USHORT s_w2;
    } S_un_w;
    ULONG S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

memcpy()

memcpy(&(serverAddr.sin_addr), &serverIP, sizeof(serverIP)); 这行代码使用memcpy函数将serverIP的内容复制到serverAddr.sin_addr中。memcpy函数接受三个参数:目标地址、源地址、以及要复制的字节数。

connect() 函数

connect() 函数是用来建立与远程服务器连接的标准函数,通常在客户端程序中使用。它的作用是向指定的服务器地址发起连接请求,并完成三次握手过程。如果连接成功,connect() 函数返回值为 0,否则返回一个错误代码。

connect() 是一个阻塞函数(在默认情况下),即在连接建立之前,它会阻塞调用线程直到连接成功或失败(返回错误)。

    iResult = connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    if (iResult == SOCKET_ERROR) { ... // 错误处理 }

(SOCKADDR*)&serverAddr:这是一个类型转换,将serverAddr的地址转换为一个指向SOCKADDR结构体的指针。serverAddr是一个sockaddr_in类型的变量,它包含了服务器的地址信息(如IP地址和端口号)。sockaddr_in是sockaddr的一个特定于IPv4的实现,这种转换是必要的,因为connect()函数的第二个参数是一个指向SOCKADDR的指针,而SOCKADDR是一个通用的地址结构体,sockaddr_in是它的一个具体实现。

send() 函数

int send(
  SOCKET s,                // 套接字描述符
  const char *buf,         // 数据缓冲区,发送数据的指针
  int len,                 // 数据长度(字节数)
  int flags                // 发送标志
);

flags: 控制发送行为的标志。通常是以下几种:

  • 0: 默认行为,数据将尽可能一次性发送。
  • MSG_OOB: 发送带外数据。
  • MSG_DONTROUTE: 不使用路由表(直接发送到目标地址)。
  • MSG_MORE: 发送更多数据,通常与大数据流一起使用。
  • MSG_NOSIGNAL: 不触发 SIGPIPE 信号(适用于一些 Unix 系统,Windows 中该标志没有实际影响)。

除了 send() 函数用来发送数据以外,还有 sendto() UDP 的发送数据,以及 WSASend() 函数, 是 send() 的扩展(异步)。

recv() 函数

int recv(
    SOCKET s,        // 套接字
    char *buf,       // 数据接收缓冲区
    int len,         // 缓冲区的大小(字节数)
    int flags        // 控制接收行为的标志
);

int iRecvResult = recv(clientSocket, recvbuf, BUFFER_SIZE - 1, 0);
BUFFER_SIZE - 1 是为了确保缓冲区末尾有足够的空间来存储 空字符(\0),以确保接收到的数据能够作为一个有效的 C 字符串使用。recv() 不会自动为接收到的数据添加 \0(空字符),因此,如果期望将接收到的数据当作字符串处理,就必须在缓冲区的末尾手动添加 \0。如果只是把接收的数据当作二进制数据处理,那么 BUFFER_SIZE - 1 大可不必。

返回值

  • recvResult > 0:表示成功接收了数据,返回的是接收到的字节数。

  • recvResult == 0:表示连接已关闭。通常,这意味着对方已经关闭了连接,不能再发送数据。如果服务器关闭了连接,客户端调用 recv() 时会返回 0,表示连接被关闭。

  • recvResult == SOCKET_ERROR:表示发生了错误,调用 recv() 函数失败。

长期不调用 recv() 函数可能会导致数据丢失

每个套接字(无论是发送端还是接收端)都有 发送缓冲区接收缓冲区。这两个缓冲区由操作系统的 TCP/IP 协议栈管理。

  • 接收缓冲区:用于临时存储接收到的网络数据,直到应用程序调用 recv() 将数据读取出来。
  • 发送缓冲区:用于存储待发送的数据,直到 TCP 协议栈将其传输到网络。

如果应用程序不及时调用 recv() 来读取数据,接收缓冲区中的数据就会积压。

  • 接收缓冲区满:当接收缓冲区满了,TCP 会根据流量控制(Flow Control)机制采取措施,确保不会接收到过多的数据,导致丢失。

  • 流量控制:TCP 会通过接收方的 接收窗口(receive window) 通知发送方接收缓冲区的剩余空间。如果接收方的接收缓冲区已满,接收窗口大小会变为 0,这就意味着发送方无法继续发送数据,直到接收方的缓冲区有足够的空间来接收新数据。

这时,发送方会减缓发送速率,甚至暂停发送,直到接收方的接收缓冲区有空闲空间为止。

如果应用程序 长时间不调用 recv() 来读取接收到的数据,并且接收缓冲区已满:

  • TCP 保证可靠交付 的情况下,TCP 协议栈会通过调整接收窗口来控制发送方的发送速率。如果缓冲区满了,发送方会暂停发送,直到接收方读取部分数据并释放缓冲区空间。
  • 然而,如果 接收缓冲区仍然没有被及时读取,并且长时间保持满的状态,某些系统会发生 缓冲区溢出 或者 丢失数据。具体取决于操作系统的实现。

Windows Server 端

//server 端
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>  // 包含 InetNtop() 函数的头文件
#include <stdlib.h>

using namespace std;

#pragma comment(lib, "ws2_32.lib")  // 链接 Winsock 库

#define SERVER_PORT 12345  // 设置监听端口
#define BUFFER_SIZE 1024  // 缓冲区大小

int main() {
    system("chcp 65001");  // 设置命令行 utf-8 字符集

    WSADATA wsaData;
    SOCKET serverSocket, clientSocket;
    struct sockaddr_in serverAddr, clientAddr;
    int clientAddrSize = sizeof(clientAddr);
    char buffer[BUFFER_SIZE];
    int recvResult;
    char clientIP[INET_ADDRSTRLEN];  // 存储客户端 IP 地址字符串

    // 1. 初始化 Winsock
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        cout << u8"Winsock 初始化失败:" << WSAGetLastError();
        return 1;
    }

    // 2. 创建 TCP 套接字
    serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (serverSocket == INVALID_SOCKET) {
        cout << u8"创建套接字失败:" << WSAGetLastError();
        WSACleanup();
        return 1;
    }

    // 3. 设置服务器地址信息
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有本地地址
    serverAddr.sin_port = htons(SERVER_PORT); // 设置监听端口

    // 4. 绑定套接字
    if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
        cout << u8"绑定套接字失败:" << WSAGetLastError();
        closesocket(serverSocket);
        WSACleanup();
        return 1;
    }

    // 5. 开始监听
    if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) {
        cout << u8"监听失败:" << WSAGetLastError();
        closesocket(serverSocket);
        WSACleanup();
        return 1;
    }

    cout << u8"服务器正在监听端口:" << SERVER_PORT;

    // 6. 接受客户端连接
    clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize);
    if (clientSocket == INVALID_SOCKET) {
        cout << u8"接受连接失败:" << WSAGetLastError();
        closesocket(serverSocket);
        WSACleanup();
        return 1;
    }

    // 使用 inet_ntop 替代 inet_ntoa
    inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP));
    cout << u8"客户端已连接,IP 地址:" << clientIP;

    // 7. 与客户端进行数据交换
    while (1) {
        recvResult = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
        if (recvResult > 0) {
            buffer[recvResult] = '\0';  // 确保数据为字符串
            cout << u8"接收到的数据:" << buffer << endl;

            // 向客户端发送回应
            const char* response = u8"Hello from server. 你好我是服务器端。";
            int sendResult = send(clientSocket, response, strlen(response), 0);
            if (sendResult == SOCKET_ERROR) {
                cout << u8"发送数据失败:" << WSAGetLastError();
                break;
            }
        }
        else if (recvResult == 0) {
            // 客户端正常关闭连接
            cout << u8"客户端关闭了连接.\n";
            break;
        }
        else {
            // 发生错误
            cout << u8"接收数据失败: %d\n" << WSAGetLastError();
            break;
        }
    }

    // 8. 关闭套接字和清理
    closesocket(clientSocket);
    closesocket(serverSocket);
    WSACleanup();

    return 0;
}

server 端和 client 端大致相同,但也有些许差别。

server 端要有一个一直打开的欢迎套接字(serverSocket),等待来自 client 端的连接。server 端比 client 多了绑定套接字 bind() 和 监听操作 listen()。client 使用 connect() 来连接 server 端的欢迎套接字,server 端使用 accept() 接受来自 client 端的连接,并返回一个新的套接字,用来和该 client 端交换数据。bind() 函数和 accept() 函数的参数在代码中都很简单明了,这里就不多介绍。

bind() 操作:将一个本地的套接字地址(包括 IP 地址和端口号)与一个套接字绑定,确保服务器能够在一个特定的端口上监听连接请求。服务器需要绑定到某个具体的端口号,以便客户端能够通过该端口访问服务器。绑定后,操作系统会知道要将来自客户端的请求通过该端口路由到服务器。(在 client 端,操作系统会为客户端分配一个临时端口,用于与服务器进行通信。因此,客户端不需要使用 bind() 来指定本地的端口号。)

listen() 操作:listen() 函数告诉操作系统,服务器套接字准备好接受客户端的连接请求。它实际上是使套接字处于监听状态,等待客户端发起连接。

#include <winsock2.h>

int listen(
  SOCKET s,
  int backlog
  // backlog 等待连接队列的最大长度。表示在 accept() 函数被调用之前,系统允许的等待连接请求的最大数量。如果队列已满,新到的连接请求会被拒绝,直到队列有空位。
);

在代码中,listen() 函数的第二个参数 SOMAXCONN 是一个宏定义 #define SOMAXCONN 0x7fffffff,一个 16进制数,0x7fffffff 只是一个理论值,实际情况中可能有所不同。我们也可以自己指定 backlog 这个值,但不能超过一个最大限制,一旦超过这个最大限制,操作系统会自动调整为最大限制的值,至于这个最大限制是多少,不同的操作系统中可能会不同。backlog 值过小可能导致连接请求被拒绝(连接队列已满),如果设置得太大,可能会消耗过多的系统资源(端口号在一台机器上是有限的资源)。对于高负载的服务器,backlog 可能需要更高的值。

server 端可以接受多个 client 端的连接,欢迎套接字的作用就是监听端口,等待来自客户端的连接,并为每一个 client 端单独创建一个套接字与之进行通信。在上面的示例代码中,仅接受一个 client 端的连接,因为只创建了一个 clientSocket。

server 端和 client 端可以在同一台电脑上运行,这样就实现了同一台机器上两个进程之间的通信,client 端 使用 127.0.0.1 IP 地址连接 server 端。

server 端 和 client 端也可以在不同的主机上运行,如果在同一个局域网中,client 端就使用运行 server 端进程的主机的局域网 IP 地址进行连接。如果 server 端处于公共的网络环境中,client 端就使用 server 端主机的公网 IP 地址进行连接。

下面这段 client 代码用来连接以上 server 端:

//client 端
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
#include <stdlib.h>

#pragma comment(lib, "Ws2_32.lib")

#define PORT 12345
#define BUFFER_SIZE 50000

int main() {
    system("chcp 65001");  // 设置命令行 utf-8 字符集

    // 初始化 Winsock
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        std::cerr << "WSAStartup failed: " << iResult << std::endl;
        return 1;
    }

    // 创建套接字
    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket == INVALID_SOCKET) {
        std::cerr << "Error at socket(): " << WSAGetLastError() << std::endl;
        WSACleanup();
        return 1;
    }

    // 设置服务器地址和端口
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);

    // 将服务器地址从文本转换为二进制形式
    in_addr serverIP;
    if (inet_pton(AF_INET, "127.0.0.1", &serverIP) <= 0) {
        std::cerr << "Invalid address/ Address not supported" << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }
    memcpy(&(serverAddr.sin_addr), &serverIP, sizeof(serverIP));

    // 连接到服务器
    iResult = connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    if (iResult == SOCKET_ERROR) {
        std::cerr << "Error at connect(): " << WSAGetLastError() << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }

    std::cout << "Connected to server" << std::endl;

    // 发送和接收数据的循环
    while (true) {
        // 读取用户输入
        std::cout << "Enter message to send (or type 'exit' to quit): ";
        std::string inputMessage;
        std::getline(std::cin, inputMessage);  // 获取用户输入

        // 如果用户输入 'exit',则退出循环
        if (inputMessage == "exit") {
            break;
        }

        // 发送消息到服务器
        int iSendResult = send(clientSocket, inputMessage.c_str(), inputMessage.length(), 0);
        if (iSendResult == SOCKET_ERROR) {
            std::cerr << "send failed: " << WSAGetLastError() << std::endl;
            break;
        }
        std::cout << "Message sent to server: " << inputMessage << std::endl;

        // 接收来自服务器的响应
        char recvbuf[BUFFER_SIZE] = { 0 };
        int iRecvResult = recv(clientSocket, recvbuf, BUFFER_SIZE - 1, 0);
        if (iRecvResult > 0) {
            recvbuf[iRecvResult] = '\0';  // 确保字符串结束
            std::cout << "Message from server: " << recvbuf << std::endl;
        }
        else if (iRecvResult == 0) {
            // 连接关闭
            std::cout << "Connection closed by server." << std::endl;
            break;
        }
        else {
            std::cerr << "recv failed: " << WSAGetLastError() << std::endl;
            break;
        }
    }

    // 关闭套接字
    closesocket(clientSocket);

    // 清理 Winsock
    WSACleanup();

    return 0;
}

运行:server端 监听 12345 端口,client 端连接本地的 12345 端口不断地从命令行接收 message,并发送给 server 端,server 每次收到消息 都只发送 “Hello from server. 你好我是服务器端。”
在这里插入图片描述

;