深入拆解 Windows Socket 五种 I/O 模型
引言:I/O 模型对 Windows 游戏服务性能的核心影响
网络 I/O 模型的本质是解决 "如何协调数据传输的'等待就绪'与'数据拷贝'两大核心阶段",以平衡效率与资源消耗。与 Linux 不同,Windows 作为闭源操作系统,虽遵循 I/O 传输的通用逻辑,却未采用 Linux 依赖的 POSIX 标准,而是基于自身内核的底层特性(如内核级线程池调度、用户态 - 内核态内存直接映射机制),设计了 IOCP(I/O 完成端口)、重叠 I/O 等专有 I/O 实现,形成了与 Linux 截然不同的 I/O 架构。
核心差异基础:Windows 与 Linux I/O 模型的底层分野
Windows 与 Linux I/O 模型的差异并非表层接口差异,而是源于系统调用、事件通知、异步实现、并发管理四大底层机制的根源性不同,直接决定了两种系统的 I/O 设计逻辑、性能上限与适用场景。
系统调用接口:POSIX 通用兼容 vs 内核贴合的专有设计:系统调用是用户态与内核交互的桥接,直接影响 I/O 操作的效率与适配范围::
- Windows :用
WSARecv
/WSASend
/WSARecvFrom
等 Winsock 专有接口,接口参数(如 WSAOVERLAPPED 结构体、WSABUF 内存描述符)直接贴合 Windows 内核机制。比如WSABUF
结构体可实现 "用户态 - 内核态内存零拷贝"(内核直接读写用户态内存,不用额外拷贝);WSAOVERLAPPED
结构体原生承载异步状态(事件、结果),不用用户态再封装,调用效率更高。 - Linux :严格遵循 POSIX 标准,通过
read
/write
/recvfrom
等通用接口实现 I/O 操作。接口设计以 "跨平台兼容" 为核心,需适配嵌入式设备、服务器、桌面终端等多场景,因此未深度绑定某一内核特性,调用逻辑相对通用,牺牲了部分效率(比如无法直接零拷贝)。
事件通知机制:用户态主动检查 vs 内核级主动触发:事件通知机制解决 "如何让程序知道 I/O 事件已发生" 的问题,其效率直接影响高并发场景下的资源利用率:
- Windows :内核主动触发。比如 WSAEventSelect 模型中 Socket 绑定内核事件(
WSAEVENT
),当 Socket 触发指定事件(如数据可读、新连接到达)时,内核直接激活该事件对象,无需用户态轮询;IOCP 模型更直接,将已完成的 I/O 事件(含操作结果、关联 Socket、用户数据)放入 "完成端口队列",工作线程通过 GetQueuedCompletionStatus 直接获取,无需任何轮询或遍历开销)。 - Linux :用户态主动检查。比如 多路复用(如
epoll
/select
/poll
) 模型需用户态调用epoll_wait
/select
等函数阻塞等待事件,事件就绪后需遍历 FD 集合处理;信号驱动 I/O 模型则通过信号通知 I/O 事件,虽能避免轮询,但也存在 "信号风暴"(大量事件同时触发导致进程频繁中断)、信号丢失等风险。
异步 I/O 实现:用户态库模拟 vs 内核原生支持:异步 I/O 的核心是 "发起 I/O 操作后无需阻塞等待,可继续处理其他任务,待操作完成后再处理结果",其实现深度(内核参与范围)决定异步流程的效率:
- Windows :内核原生支持,以 "重叠 I/O 为基础、IOCP 为高并发框架" 实现端到端异步。重叠 I/O 是底层支撑,通过
WSAOVERLAPPED
结构体发起的异步操作,从 "等待数据就绪"(内核监听网卡或磁盘缓冲区)到 "数据拷贝完成"(内核完成数据传输),全流程由内核自主调度,用户态发起操作后可立即返回处理其他任务;IOCP 则为升级版,基于重叠 I/O 整合了内核线程池,内核根据 I/O 负载动态分配线程处理完成事件,无需用户态维护线程池或异步队列,异步调度深度远超 Linux); - Linux :无内核原生端到端支持,核心依赖用户态主导实现,用户态需手动维护异步操作队列,管理待执行、已完成的异步任务,内核仅负责执行基础 I/O 操作,内核完成 I/O 操作通过信号或回调向用户态发送通知,不参与异步流程的调度逻辑,核心调度(队列排序、线程分配、结果关联)均需用户态实现,效率受限于用户态与内核态协同(如队列延迟拖累性能),其网络异步需通过 "非阻塞 I/O +
epoll
+ 线程池" 组合模拟。
多并发管理:用户态手动配置 vs 内核动态适配:多并发管理解决 "如何分配线程资源处理大量连接" 的问题,调度精度直接影响线程切换开销与资源利用率,两者的管理逻辑差异明显:
- Linux :"进程 / 线程 + 多路复用" 的用户态管理模式处理多并发,线程资源配置依赖开发者经验:需通过
pthread_create
等接口手动创建线程池,线程数量需开发者根据场景估算(如按 "CPU 核心数 ×2" 配置);以至并发资源利用率易受参数适配影响(线程数量过多,会导致 CPU 频繁切换线程上下文(保存 / 恢复线程栈、寄存器数据);线程数量过少,会引发 I/O 事件排队阻塞,无法及时处理业务)。 - Windows:"内核线程池 + IOCP" 的动态管理模式处理多并发,线程资源由内核自动适配负载,内核线程池的线程数量默认与 CPU 核心数匹配(通常为 "核心数 + 1",根源上避免线程数量与 CPU 能力不匹配的问题);内核会根据 I/O 完成事件的密度实时调整线程状态(事件密集时(如峰值时段连接请求集中)激活更多线程并行处理,事件稀疏时(如凌晨连接活跃度低)挂起部分线程减少切换开销),无需用户态手动创建线程、估算数量或处理线程启停,内核托管所有线程资源,开发者专注于业务逻辑实现。
Windows Socket I/O 模型详解:机制拆解、Linux 对比与逻辑实现
Windows 网络 I/O 模型可归纳为 5 种(阻塞 I/O 、非阻塞 I/O 、I/O 多路复用 (select/WSAEventSelect)、WSAAsyncSelect 、异步 I/O(重叠 I/O + IOCP)),核心差异体现在 "等待数据就绪""数据拷贝" 阶段的阻塞规则、事件通知方式上,部分与 Linux 模型功能对应但实现逻辑不同,需结合游戏场景(如单人工具、副本服务器、MMO 主服)选择适配模型。
同时以下将以回声服务器(客户端发送数据,服务器原封不动返回) 为案例,逐一讲解各模型的实现逻辑,配套完整可运行代码,帮助理解 Linux 网络编程的核心思路。
通用客户端代码:所有服务器模型通用
scss
#include <winsock2.h> // Windows Socket 核心头文件
#include <ws2tcpip.h> // 包含 inet_pton 等函数(IP 地址转换)
#include <stdio.h>
#include <string.h>
// 链接 Windows Socket 静态库(必须添加,否则编译报错)
#pragma comment(lib, "ws2_32.lib")
// 配置参数(与原 Linux 代码保持一致)
#define SERVER_IP "127.0.0.1" // 服务器 IP(本地测试用 127.0.0.1)
#define PORT 5150 // 服务器端口
#define BUF_SIZE 1024 // 数据缓冲区大小
int main() {
// 1. 初始化 Windows Socket 环境(Windows 特有步骤,Linux 无需此操作)
WSADATA wsaData; // 存储 Socket 初始化信息
// 初始化 2.2 版本 Socket(主流兼容版本)
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
// 初始化失败:打印错误码(WSAGetLastError 是 Windows 特有错误获取函数)
printf("WSAStartup 初始化失败!错误码:%d\n", WSAGetLastError());
return 1;
}
// 2. 创建 TCP 套接字(Windows 中套接字类型为 SOCKET,而非 Linux 的 int)
SOCKET client_fd; // Windows 套接字句柄(unsigned int 类型)
// 参数:AF_INET(IPv4)、SOCK_STREAM(TCP 流式协议)、IPPROTO_TCP(TCP 协议)
client_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (client_fd == INVALID_SOCKET) { // Windows 无效套接字判断(而非 Linux 的 <0)
printf("创建套接字失败!错误码:%d\n", WSAGetLastError());
WSACleanup(); // 失败时需释放 Socket 资源(Windows 特有)
return 1;
}
// 3. 初始化服务器地址结构(与 Linux 逻辑一致,参数含义相同)
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体
server_addr.sin_family = AF_INET; // IPv4 地址族
server_addr.sin_port = htons(PORT); // 端口转换为网络字节序(大端序)
// 将字符串 IP 转换为二进制格式(inet_pton 跨平台,但 Windows 需 ws2tcpip.h 头文件)
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
printf("IP 地址转换失败!错误码:%d\n", WSAGetLastError());
closesocket(client_fd); // 关闭套接字(Windows 用 closesocket,Linux 用 close)
WSACleanup(); // 释放 Socket 环境
return 1;
}
// 4. 连接服务器(逻辑与 Linux 一致,错误判断为 SOCKET_ERROR)
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("连接服务器失败!错误码:%d\n", WSAGetLastError());
closesocket(client_fd);
WSACleanup();
return 1;
}
printf("已连接服务器,输入数据发送(输入'quit'退出):\n");
// 5. 发送-接收循环(核心逻辑与 Linux 完全一致)
char buf[BUF_SIZE];
int ret; // 存储系统调用返回值
while (1) {
// 读取用户输入
printf("客户端发送:");
fgets(buf, BUF_SIZE, stdin); // 读取控制台输入
// 去掉输入末尾的换行符(fgets 会把回车符 '\n' 也读入缓冲区)
int len = strlen(buf);
if (buf[len - 1] == '\n') {
buf[--len] = '\0';
}
// 输入 "quit" 则退出循环
if (strcmp(buf, "quit") == 0) {
break;
}
// 发送数据到服务器(参数与 Linux 一致,错误判断为 SOCKET_ERROR)
ret = send(client_fd, buf, len, 0);
if (ret == SOCKET_ERROR) {
printf("发送数据失败!错误码:%d\n", WSAGetLastError());
break;
}
printf("发送字节数:%d\n", ret);
// 接收服务器反射的响应数据
memset(buf, 0, BUF_SIZE); // 清空缓冲区,避免残留旧数据
ret = recv(client_fd, buf, BUF_SIZE, 0);
if (ret == SOCKET_ERROR) {
printf("接收数据失败!错误码:%d\n", WSAGetLastError());
break;
}
else if (ret == 0) {
// ret == 0 表示服务器主动关闭连接(与 Linux 逻辑一致)
printf("服务器已断开连接!\n");
break;
}
printf("服务器返回:%s(接收字节数:%d)\n\n", buf, ret);
}
// 6. 资源清理(Windows 特有步骤:先关套接字,再释放 Socket 环境)
closesocket(client_fd); // 关闭套接字句柄(替代 Linux 的 close)
WSACleanup(); // 释放 Windows Socket 环境资源
printf("已断开连接,资源已清理\n");
return 0;
}
阻塞 I/O(Blocking I/O):低并发场景的基础方案
核心机制 :"等待数据就绪" 与 "数据拷贝" 均阻塞调用线程,在回声服务器中,主线程通过 accept
阻塞等待客户端连接,连接成功后创建子线程,子线程通过 recv
阻塞等待客户端发送数据,接收完成后用 send
阻塞返回回声,整个过程中子线程完全挂起,直至操作完成。
Windows 特有细节:Socket 默认阻塞,若需切换非阻塞,需通过 ioctlsocket 函数显式配置,传入 FIONBIO 命令(参数设为非零值);而 Linux 中,可通过 fcntl 函数设置 O_NONBLOCK 标志实现非阻塞切换。
与 Linux 对比及适用场景对比:逻辑与 Linux 阻塞 I/O 完全一致,阻塞线程均会被内核调度器挂起(不占用 CPU 资源),避免无效消耗;差异仅在于 Socket 模式切换的接口(Windows 用 ioctlsocket,Linux 用 fcntl);适用低并发、低交互场景,如游戏 GM 工具的单客户端连接等。
scss
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <windows.h>
#include <process.h>
#include <ws2tcpip.h> // 用于 INET_ADDRSTRLEN 和 inet_ntop
// 链接 Winsock 库(Windows 特有)
#pragma comment(lib, "ws2_32.lib")
#define PORT 5150
#define BUF_SIZE 1024
#define IP_LEN 16 // 足够存储 IPv4 地址(如 "255.255.255.255")
typedef struct {
SOCKET client_fd; // Windows 用 SOCKET 类型(对应 Linux 的 int)
char client_ip[IP_LEN]; // 客户端 IP 地址
int client_port; // 客户端端口(随机分配)
} ClientParam;
//接收客户端数据→回显→错误处理
unsigned int __stdcall handle_client(void* arg) {
ClientParam* param = (ClientParam*)arg;
SOCKET client_fd = param->client_fd;
char client_ip[IP_LEN];
int client_port = param->client_port;
//复制客户端 IP(避免主线程覆盖,线程安全)
strncpy(client_ip, param->client_ip, IP_LEN - 1);
client_ip[IP_LEN - 1] = '\0'; // 确保字符串结束符
free(param); // 释放动态分配的参数(避免内存泄漏)
char buf[BUF_SIZE];
int ret; // 接收/发送返回值
while (1) {
// 阻塞接收客户端数据(初始化缓冲区)
memset(buf, 0, BUF_SIZE);
ret = recv(client_fd, buf, BUF_SIZE, 0); // Windows recv 用法与 Linux 兼容
//接收结果判断
if (ret == 0) {
// 正常断开:客户端主动关闭连接
printf("客户端:%s:%d正常断开\n", client_ip, client_port);
break;
}
else if (ret < 0) {
// 异常断开:排除 "系统中断" 错误(Windows 对应 WSAEINTR)
if (WSAGetLastError() != WSAEINTR) { // Windows 用 WSAGetLastError()
printf("客户端:%s:%d异常断开\n", client_ip, client_port);
break;
}
continue; // 若为中断错误,重试 recv
}
// 接收成功:打印数据并回显(流程与 Linux 完全一致)
printf("接收:%s(%d字节)\n", buf, ret);
ret = send(client_fd, buf, ret, 0); // Windows send 用法
if (ret <= 0) {
printf("send fail: %d\n", WSAGetLastError());
break;
}
}
// 释放资源:关闭客户端套接字(Windows 用 closesocket)
closesocket(client_fd);
_endthreadex(0); // Windows 线程结束(确保资源释放)
return 0;
}
int main() {
// 变量定义
SOCKET listen_fd, client_fd;
struct sockaddr_in server_addr, client_addr; // 地址结构体
int client_addr_len = sizeof(client_addr);
HANDLE hThread;
// 初始化网络库(Windows 特有,流程前置)
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup fail: %d\n", WSAGetLastError());
return -1;
}
// 创建监听套接字
listen_fd = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
if (listen_fd == INVALID_SOCKET) { // Windows 用 INVALID_SOCKET 替代 -1
printf("socket fail: %d\n", WSAGetLastError());
WSACleanup(); // Windows 需清理 Winsock 资源
return -1;
}
// 端口复用 + 绑定地址
int reuse = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) == SOCKET_ERROR) {
printf("setsockopt fail: %d\n", WSAGetLastError());
closesocket(listen_fd); // 替代 Linux 的 close()
WSACleanup();
return -1;
}
// 地址赋值
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(PORT); // 端口转网络字节序
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
// 绑定(API 适配:Windows 用 SOCKET_ERROR 替代 -1)
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("bind fail: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 开始监听
if (listen(listen_fd, 5) == SOCKET_ERROR) { // 监听队列大小 5
printf("listen fail: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
printf("阻塞IO服务器启动:端口=%d\n", PORT);
// 循环接受客户端连接
while (1) {
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == INVALID_SOCKET) {
printf("accept fail: %d\n", WSAGetLastError());
continue;
}
// 获取客户端 IP 和端口
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port); // 端口转主机序
printf("新客户端连接:%s:%d\n", client_ip, client_port);
// 封装线程参数
ClientParam* param = (ClientParam*)malloc(sizeof(ClientParam));
if (param == NULL) {
printf("malloc fail\n");
closesocket(client_fd);
continue;
}
param->client_fd = client_fd;
strncpy(param->client_ip, client_ip, IP_LEN - 1);
param->client_ip[IP_LEN - 1] = '\0';
param->client_port = client_port;
// 创建子线程处理客户端
hThread = (HANDLE)_beginthreadex(NULL, 0, handle_client, param, 0, NULL);
if (hThread == NULL) { // 线程创建失败
printf("pthread_create fail\n");
closesocket(client_fd);
free(param); // 释放参数内存
}
CloseHandle(hThread); // Windows 释放线程句柄
}
// 资源清理(理论上不执行)
closesocket(listen_fd);
WSACleanup(); // Windows 特有:清理 Winsock 库
return 0;
}
非阻塞 I/O(Non-Blocking I/O):高频小数据的轮询方案
核心机制 :"等待数据就绪" 不阻塞线程,"数据拷贝" 仍阻塞。在回声服务器中,accept
、recv
、send
均设为非阻塞:若客户端未发送数据,recv
会立即返回 WSAEWOULDBLOCK
错误,服务器需通过 while
循环轮询(搭配 Sleep(10)
减少 CPU 占用),直至数据就绪后执行回声发送。
与 Linux 对比:均需用户态轮询检查数据状态,轮询过程消耗 CPU 资源,需搭配线程池(如 Windows 用 CreateThreadPool,Linux 用 pthread_create)分散轮询压力,避免单线程轮询占用过多资源;差异在于"数据未就绪" 的错误标识不同(Windows 通过 WSAGetLastError() 获取 WSAEWOULDBLOCK 错误码,Linux 则是 EAGAIN/EWOULDBLOCK 错误码),此外接口略有差异(Windows 非阻塞 Socket 需通过 ioctlsocket 配置,Linux 可通过 fcntl 或 ioctl 配置)。
适用场景:适用场景 高频、小数据量的交互场景,如游戏实时心跳检测、玩家短消息发送(单次数据量 < 1KB),不适合高并发场景(如 1000 人战场会因轮询导致 CPU 利用率飙升)。
ini
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <winsock2.h>
#include <windows.h>
#include <process.h>
#include <ws2tcpip.h> // 用于 INET_ADDRSTRLEN 和 inet_ntop
#pragma comment(lib, "ws2_32.lib")
#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 10
#define IP_LEN 16
// 客户端参数结构体
typedef struct {
SOCKET client_fd;
char client_ip[IP_LEN];
int client_port;
} ClientParam;
// 设置非阻塞模式
void set_nonblocking(SOCKET fd) {
u_long mode = 1;
ioctlsocket(fd, FIONBIO, &mode);
}
// 处理客户端(非阻塞轮询)
unsigned int __stdcall handle_client(void* arg) {
ClientParam* param = (ClientParam*)arg;
SOCKET client_fd = param->client_fd;
char client_ip[IP_LEN];
int client_port = param->client_port;
strncpy(client_ip, param->client_ip, IP_LEN - 1);
client_ip[IP_LEN - 1] = '\0';
free(param);
char buf[BUF_SIZE];
int ret;
// 设置非阻塞
set_nonblocking(client_fd);
printf("开始处理客户端:%s:%d\n", client_ip, client_port);
while (1) {
// 非阻塞接收
ret = recv(client_fd, buf, BUF_SIZE, 0);
if (ret > 0) {
buf[ret] = '\0';
printf("接收:%s(%d字节)\n", buf, ret);
send(client_fd, buf, ret, 0);
}
else if (ret == 0) {
printf("客户端:%s:%d正常断开\n", client_ip, client_port);
break;
}
else {
int err = WSAGetLastError();
if (err != WSAEWOULDBLOCK && err != WSAEINPROGRESS) {
printf("客户端:%s:%d异常断开(错误码:%d)\n",
client_ip, client_port, err);
break;
}
// 无数据时休眠
Sleep(10);
}
}
closesocket(client_fd);
return 0;
}
int main() {
WSADATA wsaData;
SOCKET listen_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
int client_addr_len = sizeof(client_addr);
HANDLE hThread;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 创建监听套接字并设置非阻塞
listen_fd = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
set_nonblocking(listen_fd);
// 端口复用
int reuse = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse));
// 绑定与监听
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listen_fd, 5);
printf("非阻塞I/O服务器启动:端口=%d\n", PORT);
// 非阻塞接受连接
while (1) {
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd != INVALID_SOCKET) {
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);
printf("新客户端连接:%s:%d\n", client_ip, client_port);
ClientParam* param = (ClientParam*)malloc(sizeof(ClientParam));
param->client_fd = client_fd;
strncpy(param->client_ip, client_ip, IP_LEN - 1);
param->client_port = client_port;
hThread = (HANDLE)_beginthreadex(NULL, 0, handle_client, param, 0, NULL);
if (hThread == NULL) {
closesocket(client_fd);
free(param);
}
CloseHandle(hThread);
}
else {
int err = WSAGetLastError();
if (err != WSAEWOULDBLOCK && err != WSAEINPROGRESS) {
printf("accept错误:%d\n", err);
}
Sleep(10);
}
}
closesocket(listen_fd);
WSACleanup();
return 0;
}
I/O 多路复用(I/O Multiplexing):中低并发的高效监听
Windows 提供两种主流多路复用模型,功能上对应 Linux 的 select/poll,但基于内核事件对象优化,更适配中低并发场景。
select 模型:兼容旧系统的基础多路复用
核心机制 :单线程通过 "读 / 写 / 异常" 三个 FD 集合监听多 Socket。在回声服务器中,select
阻塞等待任一客户端触发事件(如 FD_READ
表示有数据待接收),事件就绪后遍历 FD 集合,找到对应客户端执行 recv
接收数据、send
返回回声。
与 Linux 对比: 共性是默认支持 64 个 Socket(可通过宏扩展,但效率下降)、每次调用需重新初始化 FD 集合(因内核会修改集合),效率随 Socket 数量增加而下降;而差异是 Windows select 的首参(nfds)无效(传 0),Linux select 需传入 "最大文件描述符 + 1",以致内核可确定遍历范围;且Windows FD 集合仅支持 Socket,Linux 支持所有文件描述符(如文件、管道)。
适用场景:兼容 WinXP 等旧系统,或连接数 < 100 的小型游戏房间(如 20 人斗地主房间,仅需低频次的聊天、出牌数据交互)。
scss
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdbool.h>
#pragma comment(lib, "ws2_32.lib")
// 全局宏定义(与 Linux 版本保持一致)
#define PORT 5150
#define BUF_SIZE 1024
#define MAX_FD 1024 // select 支持的最大套接字数量
#define IP_LEN 16 // IPv4 地址存储长度
// 客户端信息结构体(与 Linux 版本保持一致)
typedef struct {
bool in_use; // 标记是否正在使用
char ip[IP_LEN]; // 客户端 IP 地址
int port; // 客户端端口
char send_buf[BUF_SIZE];// 待发送数据缓冲区
int send_len; // 待发送数据长度
} Client;
// 全局数组:用套接字句柄作为索引管理所有客户端
Client clients[MAX_FD];
// 初始化客户端结构体
void init_client(SOCKET fd, const char* ip, int port) {
memset(&clients[fd], 0, sizeof(Client));
clients[fd].in_use = true;
strncpy(clients[fd].ip, ip, IP_LEN - 1);
clients[fd].port = port;
clients[fd].send_len = 0;
}
// 清理客户端结构体
void cleanup_client(SOCKET fd) {
memset(&clients[fd], 0, sizeof(Client));
clients[fd].in_use = false;
}
int main() {
WSADATA wsaData;
SOCKET listen_fd, client_fd;
SOCKET max_fd; // 改为 main 函数局部变量(与 Linux 版本统一)
struct sockaddr_in server_addr, client_addr;
int client_addr_len = sizeof(client_addr);
fd_set read_fds, write_fds, except_fds;
fd_set tmp_read, tmp_write, tmp_except;
char recv_buf[BUF_SIZE];
int ret, i;
// 初始化 Winsock 库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup 失败: %d\n", WSAGetLastError());
return -1;
}
// 初始化客户端数组
memset(clients, 0, sizeof(clients));
for (int i = 0; i < MAX_FD; i++) {
clients[i].in_use = false;
}
// 初始化事件集合
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
FD_ZERO(&except_fds);
// 创建监听套接字
listen_fd = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
if (listen_fd == INVALID_SOCKET) {
printf("创建套接字失败: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
FD_SET(listen_fd, &read_fds);
FD_SET(listen_fd, &except_fds);
max_fd = listen_fd; // 局部变量初始化
// 端口复用+绑定+监听
int reuse = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) == SOCKET_ERROR) {
printf("setsockopt 失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("绑定失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
if (listen(listen_fd, 5) == SOCKET_ERROR) {
printf("监听失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
printf("select服务器(IO多路复用)启动:端口=%d\n", PORT);
// select 事件循环
while (1) {
// 复制事件集合
tmp_read = read_fds;
tmp_write = write_fds;
tmp_except = except_fds;
// 等待事件发生(Windows select 第一个参数忽略,传 0)
ret = select(0, &tmp_read, &tmp_write, &tmp_except, NULL);
if (ret == SOCKET_ERROR) {
printf("select 失败: %d\n", WSAGetLastError());
continue;
}
// 处理读事件
for (i = 0; i <= max_fd; i++) {
if (!FD_ISSET(i, &tmp_read)) continue;
if (i == listen_fd) {
// 处理新客户端连接
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == INVALID_SOCKET) {
printf("accept 失败: %d\n", WSAGetLastError());
continue;
}
if (client_fd >= MAX_FD) {
printf("客户端超出select最大限制(%d),拒绝连接\n", MAX_FD);
closesocket(client_fd);
continue;
}
// 记录客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);
init_client(client_fd, client_ip, client_port);
// 添加到事件集合并更新 max_fd
FD_SET(client_fd, &read_fds);
FD_SET(client_fd, &except_fds);
if (client_fd > max_fd) {
max_fd = client_fd; // 局部变量更新
}
// 新连接提示
printf("新客户端连接:%s:%d\n", client_ip, client_port);
}
else {
// 处理客户端发送的数据
if (!clients[i].in_use) continue;
memset(recv_buf, 0, BUF_SIZE);
ret = recv(i, recv_buf, BUF_SIZE - 1, 0);
if (ret <= 0) {
// 客户端断开连接
printf("客户端:%s:%d正常断开\n",
clients[i].ip, clients[i].port);
// 清理资源
FD_CLR(i, &read_fds);
FD_CLR(i, &write_fds);
FD_CLR(i, &except_fds);
cleanup_client(i);
closesocket(i);
continue;
}
// 接收数据并准备发送
printf("接收:%s(%d字节)→ 暂存待发送\n", recv_buf, ret);
strncpy(clients[i].send_buf, recv_buf, ret);
clients[i].send_len = ret;
FD_SET(i, &write_fds);
}
}
// 处理写事件
for (i = 0; i <= max_fd; i++) {
if (!clients[i].in_use || !FD_ISSET(i, &tmp_write) || clients[i].send_len <= 0) {
continue;
}
ret = send(i, clients[i].send_buf, clients[i].send_len, 0);
if (ret == SOCKET_ERROR) {
printf("send 失败: %d\n", WSAGetLastError());
FD_CLR(i, &read_fds);
FD_CLR(i, &write_fds);
FD_CLR(i, &except_fds);
cleanup_client(i);
closesocket(i);
}
else {
// 发送完成提示
printf("发送:%s(%d字节)→ 发送完成\n",
clients[i].send_buf, clients[i].send_len);
clients[i].send_len = 0;
FD_CLR(i, &write_fds);
}
}
// 处理异常事件
for (i = 0; i <= max_fd; i++) {
if (!FD_ISSET(i, &tmp_except) || i == listen_fd || !clients[i].in_use) {
continue;
}
// 异常断开提示
printf("客户端:%s:%d异常断开\n",
clients[i].ip, clients[i].port);
// 清理资源
FD_CLR(i, &read_fds);
FD_CLR(i, &write_fds);
FD_CLR(i, &except_fds);
cleanup_client(i);
closesocket(i);
// 更新最大套接字句柄(局部变量操作)
if (i == max_fd) {
while (max_fd > 0 && !FD_ISSET(max_fd, &read_fds) && !FD_ISSET(max_fd, &write_fds)) {
max_fd--;
}
}
}
}
// 清理资源(理论上不会执行)
closesocket(listen_fd);
WSACleanup();
return 0;
}
WSAEventSelect 模型:中并发的事件驱动优化
核心机制 :通过内核事件对象(WSAEVENT)绑定 每个 Socket 事件,监听 FD_READ
/FD_ACCEPT
/FD_CLOSE
等事件(如 FD_READ 表示数据可读、FD_ACCEPT 表示有新连接、FD_CLOSE 表示连接关闭),在回声服务器中,服务器通过 WSAWaitForMultipleEvents
阻塞等待事件,事件就绪后通过 WSAEnumNetworkEvents
直接获取事件类型(无需遍历),找到对应客户端执行回声逻辑。
与 Linux 对比 :类似 Linux 的 poll
,但基于内核事件驱动,效率比 select 高 30%~50%,接近 epoll
的水平触发(不支持边缘触发,可能重复通知)。
适用场景:中并发场景,如 100 人团队副本服务器(需处理技能释放、伤害计算等中等频次数据)、中小型公会聊天服务。
ini
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdbool.h>
#pragma comment(lib, "ws2_32.lib")
// 全局宏定义
#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 10
#define MAX_EVENTS 11 // 1个监听事件 + 10个客户端事件
#define IP_LEN 16 // IPv4地址存储长度
// 客户端信息结构体
typedef struct {
bool in_use; // 标记是否正在使用
SOCKET fd; // 客户端套接字句柄
char ip[IP_LEN]; // 客户端IP地址
int port; // 客户端端口
char read_buf[BUF_SIZE]; // 读缓冲区
char send_buf[BUF_SIZE]; // 待发送数据缓冲区
int send_len; // 待发送数据总长度
int send_pos; // 当前发送位置(解决部分发送问题)
WSAEVENT event; // 关联事件对象
} Client;
// 全局数组:管理所有客户端
Client clients[MAX_CLIENTS];
WSAEVENT events[MAX_EVENTS]; // 事件数组
int event_count = 0; // 当前事件总数
// 初始化客户端结构体
void init_client(int index, SOCKET fd, const char* ip, int port) {
memset(&clients[index], 0, sizeof(Client));
clients[index].in_use = true;
clients[index].fd = fd;
strncpy(clients[index].ip, ip, IP_LEN - 1);
clients[index].port = port;
clients[index].send_len = 0;
clients[index].send_pos = 0;
clients[index].event = WSA_INVALID_EVENT;
}
// 清理客户端结构体
void cleanup_client(int index) {
if (clients[index].fd != INVALID_SOCKET) {
closesocket(clients[index].fd);
clients[index].fd = INVALID_SOCKET;
}
if (clients[index].event != WSA_INVALID_EVENT) {
WSACloseEvent(clients[index].event);
clients[index].event = WSA_INVALID_EVENT;
}
memset(&clients[index], 0, sizeof(Client));
clients[index].in_use = false;
}
// 查找空闲客户端槽位
int find_free_client() {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (!clients[i].in_use) {
return i;
}
}
return -1;
}
// 移除事件数组中的指定事件
void remove_event(WSAEVENT event) {
for (int i = 1; i < event_count; i++) { // 从1开始:0是监听事件
if (events[i] == event) {
// 移动数组元素填补空缺
memmove(&events[i], &events[i + 1], (event_count - i - 1) * sizeof(WSAEVENT));
event_count--;
break;
}
}
}
// 发送数据函数(处理部分发送情况)
void send_data(int client_idx) {
Client* cli = &clients[client_idx];
if (cli->send_len <= 0 || cli->send_pos >= cli->send_len) {
// 没有数据可发送,恢复事件监听
WSAEventSelect(cli->fd, cli->event, FD_READ | FD_CLOSE);
cli->send_len = 0;
cli->send_pos = 0;
return;
}
// 发送剩余数据
int sent = send(cli->fd, cli->send_buf + cli->send_pos,
cli->send_len - cli->send_pos, 0);
if (sent == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) {
// 暂时无法发送,保留事件监听等待下次机会
return;
}
// 真正的错误,关闭连接
printf("客户端 %s:%d 发送失败: %d\n", cli->ip, cli->port, err);
remove_event(cli->event);
cleanup_client(client_idx);
return;
}
cli->send_pos += sent;
// 数据发送完成
if (cli->send_pos >= cli->send_len) {
printf("发送:%s(%d字节)→ 发送完成\n", cli->send_buf, cli->send_len);
cli->send_len = 0;
cli->send_pos = 0;
// 恢复事件监听,只关注读和关闭事件
WSAEventSelect(cli->fd, cli->event, FD_READ | FD_CLOSE);
}
else {
// 还有数据未发送,继续监听可写事件
printf("客户端 %s:%d 部分发送:已发送%d字节,剩余%d字节\n",
cli->ip, cli->port, sent, cli->send_len - cli->send_pos);
}
}
int main() {
WSADATA wsaData;
SOCKET listen_fd;
struct sockaddr_in server_addr, client_addr;
int client_addr_len = sizeof(client_addr);
DWORD wait_result;
int ret;
// 初始化Winsock库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup 失败: %d\n", WSAGetLastError());
return -1;
}
// 初始化客户端数组
memset(clients, 0, sizeof(clients));
for (int i = 0; i < MAX_CLIENTS; i++) {
clients[i].in_use = false;
clients[i].fd = INVALID_SOCKET;
clients[i].event = WSA_INVALID_EVENT;
}
// 创建监听套接字
listen_fd = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
if (listen_fd == INVALID_SOCKET) {
printf("创建套接字失败: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
// 启用端口复用
int reuse = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
(const char*)&reuse, sizeof(reuse)) == SOCKET_ERROR) {
printf("setsockopt 失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 配置服务器地址并绑定
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("绑定失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 开始监听
if (listen(listen_fd, 5) == SOCKET_ERROR) {
printf("监听失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 初始化监听事件
events[0] = WSACreateEvent();
if (events[0] == WSA_INVALID_EVENT) {
printf("创建监听事件失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
WSAEventSelect(listen_fd, events[0], FD_ACCEPT | FD_CLOSE);
event_count = 1;
printf("WSAEventSelect服务器(IO多路复用)启动:端口=%d,最大连接=%d\n", PORT, MAX_CLIENTS);
// 事件循环
while (1) {
// 等待事件触发
wait_result = WSAWaitForMultipleEvents(event_count, events, FALSE, WSA_INFINITE, FALSE);
if (wait_result == WSA_WAIT_FAILED) {
printf("WSAWaitForMultipleEvents 失败: %d\n", WSAGetLastError());
break;
}
int event_index = wait_result - WSA_WAIT_EVENT_0;
// 遍历处理所有触发的事件
for (int i = event_index; i < event_count; i++) {
if (WSAWaitForMultipleEvents(1, &events[i], TRUE, 0, FALSE) != WSA_WAIT_EVENT_0) {
continue;
}
// 重置事件状态
WSAResetEvent(events[i]);
// 处理监听事件(新连接)
if (i == 0) {
WSANETWORKEVENTS network_events;
ret = WSAEnumNetworkEvents(listen_fd, events[0], &network_events);
if (ret == SOCKET_ERROR) {
printf("WSAEnumNetworkEvents 失败: %d\n", WSAGetLastError());
continue;
}
// 处理新连接事件
if (network_events.lNetworkEvents & FD_ACCEPT) {
if (network_events.iErrorCode[FD_ACCEPT_BIT] != 0) {
printf("FD_ACCEPT 失败: %d\n", network_events.iErrorCode[FD_ACCEPT_BIT]);
continue;
}
// 接受新连接
SOCKET client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == INVALID_SOCKET) {
printf("accept 失败: %d\n", WSAGetLastError());
continue;
}
// 检查连接限制
int free_slot = find_free_client();
if (free_slot == -1 || event_count >= MAX_EVENTS) {
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("客户端:%s:%d 超出最大限制,拒绝连接\n",
client_ip, ntohs(client_addr.sin_port));
closesocket(client_fd);
continue;
}
// 初始化客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);
// 创建客户端事件
WSAEVENT client_event = WSACreateEvent();
if (client_event == WSA_INVALID_EVENT) {
printf("创建客户端事件失败: %d\n", WSAGetLastError());
closesocket(client_fd);
continue;
}
// 初始化客户端结构体
init_client(free_slot, client_fd, client_ip, client_port);
clients[free_slot].event = client_event;
// 绑定事件与套接字
WSAEventSelect(client_fd, client_event, FD_READ | FD_CLOSE);
// 添加事件到数组
events[event_count] = client_event;
event_count++;
printf("新客户端连接:%s:%d\n", client_ip, client_port);
}
}
// 处理客户端事件
else {
// 查找对应的客户端
int client_idx = -1;
for (int j = 0; j < MAX_CLIENTS; j++) {
if (clients[j].in_use && clients[j].event == events[i]) {
client_idx = j;
break;
}
}
if (client_idx == -1) continue;
Client* cli = &clients[client_idx];
WSANETWORKEVENTS network_events;
ret = WSAEnumNetworkEvents(cli->fd, cli->event, &network_events);
if (ret == SOCKET_ERROR) {
printf("客户端 %s:%d WSAEnumNetworkEvents 失败: %d\n",
cli->ip, cli->port, WSAGetLastError());
continue;
}
// 处理可读事件
if (network_events.lNetworkEvents & FD_READ) {
if (network_events.iErrorCode[FD_READ_BIT] != 0) {
printf("客户端 %s:%d FD_READ 失败: %d\n",
cli->ip, cli->port, network_events.iErrorCode[FD_READ_BIT]);
continue;
}
// 读取数据
int valread = recv(cli->fd, cli->read_buf, BUF_SIZE - 1, 0);
if (valread <= 0) {
printf("客户端 %s:%d 异常断开\n", cli->ip, cli->port);
remove_event(cli->event);
cleanup_client(client_idx);
continue;
}
// 准备回显数据
cli->read_buf[valread] = '\0';
memcpy(cli->send_buf, cli->read_buf, valread);
cli->send_len = valread;
cli->send_pos = 0; // 重置发送位置
printf("接收:%s(%d字节)→ 暂存待发送\n", cli->read_buf, valread);
// 注册写事件,准备发送数据
WSAEventSelect(cli->fd, cli->event, FD_READ | FD_WRITE | FD_CLOSE);
// 立即尝试发送数据
send_data(client_idx);
}
// 处理可写事件(仅在有数据时处理)
if (network_events.lNetworkEvents & FD_WRITE && cli->send_len > 0) {
if (network_events.iErrorCode[FD_WRITE_BIT] != 0) {
printf("客户端 %s:%d FD_WRITE 失败: %d\n",
cli->ip, cli->port, network_events.iErrorCode[FD_WRITE_BIT]);
continue;
}
send_data(client_idx);
}
// 处理关闭事件
if (network_events.lNetworkEvents & FD_CLOSE) {
printf("客户端 %s:%d 正常断开\n", cli->ip, cli->port);
remove_event(cli->event);
cleanup_client(client_idx);
}
}
}
}
// 清理资源
for (int i = 0; i < event_count; i++) {
WSACloseEvent(events[i]);
}
closesocket(listen_fd);
WSACleanup();
return 0;
}
异步选择模型(WSAAsyncSelect):GUI 程序的消息驱动方案
核心机制 :基于 Windows 窗口消息传递 Socket 事件。在回声服务器中,需先创建窗口(即使隐藏),将 Socket 事件与自定义消息(如 WM_SOCKET
)绑定;当客户端发送数据(触发 FD_READ
),内核会向窗口消息队列发送 WM_SOCKET
,窗口过程(WndProc
)接收消息后,调用 recv
/send
执行回声逻辑,不阻塞 GUI 线程。
与 Linux 对比:Linux 无窗口消息机制,无直接对应模型,功能类似 "信号驱动 I/O",但通过消息队列可缓存事件,不会频繁中断进程(避免 "信号风暴"),但 WSAAsyncSelect 依赖窗口环境,必须在有窗口的进程中使用(纯服务端无法使用),而 Linux 信号驱动 I/O 可在无 GUI 的服务端使用,但信号处理优先级低,易丢失事件。
适用场景:GUI 程序的网络交互,如处理登录验证、简单文字聊天等低频网络交互,不适合纯服务端高并发场景。
ini
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS // 新增:禁用Winsock过时API警告
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdbool.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 10
#define IP_LEN 16
#define WM_SOCKET (WM_USER + 1)
typedef struct {
bool in_use;
SOCKET fd;
char ip[IP_LEN];
int port;
char send_buf[BUF_SIZE];
int send_len;
} Client;
Client clients[MAX_CLIENTS];
SOCKET listen_fd;
HWND hWnd;
void init_client(Client* cli, SOCKET fd, const char* ip, int port) {
memset(cli, 0, sizeof(Client));
cli->in_use = true;
cli->fd = fd;
strncpy(cli->ip, ip, IP_LEN - 1);
cli->port = port;
cli->send_len = 0;
}
void cleanup_client(Client* cli) {
if (cli->fd != INVALID_SOCKET) {
closesocket(cli->fd);
cli->fd = INVALID_SOCKET;
}
memset(cli, 0, sizeof(Client));
cli->in_use = false;
}
int find_free_client() {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (!clients[i].in_use) {
return i;
}
}
return -1;
}
int find_client_by_fd(SOCKET fd) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].in_use && clients[i].fd == fd) {
return i;
}
}
return -1;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
SOCKET sock = (SOCKET)wParam;
int event = WSAGETSELECTEVENT(lParam);
int error = WSAGETSELECTERROR(lParam);
int ret, slot;
char recv_buf[BUF_SIZE];
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
switch (msg) {
case WM_SOCKET:
if (error != 0) {
printf("套接字事件错误: %d\n", error);
if (sock != listen_fd) {
slot = find_client_by_fd(sock);
if (slot != -1) {
printf("客户端:%s:%d异常断开\n", clients[slot].ip, clients[slot].port);
cleanup_client(&clients[slot]);
}
}
return 0;
}
if (sock == listen_fd) {
if (event == FD_ACCEPT) {
SOCKET client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == INVALID_SOCKET) {
printf("accept 失败: %d\n", WSAGetLastError());
return 0;
}
slot = find_free_client();
if (slot == -1) {
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str));
printf("客户端超出最大限制(%d),拒绝连接\n", MAX_CLIENTS);
closesocket(client_fd);
return 0;
}
char client_ip[IP_LEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, IP_LEN - 1);
int client_port = ntohs(client_addr.sin_port);
init_client(&clients[slot], client_fd, client_ip, client_port);
WSAAsyncSelect(client_fd, hWnd, WM_SOCKET, FD_READ | FD_CLOSE);
printf("新客户端连接:%s:%d\n", client_ip, client_port);
}
return 0;
}
slot = find_client_by_fd(sock);
if (slot == -1) return 0;
Client* cli = &clients[slot];
if (event == FD_CLOSE) {
printf("客户端:%s:%d正常断开\n", cli->ip, cli->port);
cleanup_client(cli);
return 0;
}
if (event == FD_READ) {
memset(recv_buf, 0, BUF_SIZE);
ret = recv(cli->fd, recv_buf, BUF_SIZE - 1, 0);
if (ret <= 0) {
printf("客户端:%s:%d异常断开\n", cli->ip, cli->port);
cleanup_client(cli);
return 0;
}
printf("接收:%s(%d字节)→ 暂存待发送\n", recv_buf, ret);
strncpy(cli->send_buf, recv_buf, ret);
cli->send_len = ret;
WSAAsyncSelect(cli->fd, hWnd, WM_SOCKET, FD_WRITE | FD_CLOSE);
return 0;
}
if (event == FD_WRITE) {
if (cli->send_len <= 0) {
WSAAsyncSelect(cli->fd, hWnd, WM_SOCKET, FD_READ | FD_CLOSE);
return 0;
}
ret = send(cli->fd, cli->send_buf, cli->send_len, 0);
if (ret == SOCKET_ERROR) {
printf("send 失败: %d\n", WSAGetLastError());
cleanup_client(cli);
return 0;
}
printf("发送:%s(%d字节)→ 发送完成\n", cli->send_buf, cli->send_len);
cli->send_len = 0;
WSAAsyncSelect(cli->fd, hWnd, WM_SOCKET, FD_READ | FD_CLOSE);
return 0;
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
return 0;
}
bool create_window() {
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = "SocketWindowClass";
if (!RegisterClassEx(&wc)) {
printf("窗口类注册失败: %d\n", GetLastError());
return false;
}
hWnd = CreateWindowEx(0, "SocketWindowClass", "WSAAsyncSelect服务器",
0, 0, 0, 0, 0, HWND_MESSAGE, NULL, wc.hInstance, NULL);
if (!hWnd) {
printf("窗口创建失败: %d\n", GetLastError());
UnregisterClass(wc.lpszClassName, wc.hInstance);
return false;
}
return true;
}
int main() {
WSADATA wsaData;
struct sockaddr_in server_addr;
MSG msg;
int i;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup 失败: %d\n", WSAGetLastError());
return -1;
}
memset(clients, 0, sizeof(clients));
for (i = 0; i < MAX_CLIENTS; i++) {
clients[i].in_use = false;
}
if (!create_window()) {
WSACleanup();
return -1;
}
listen_fd = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
if (listen_fd == INVALID_SOCKET) {
printf("创建套接字失败: %d\n", WSAGetLastError());
DestroyWindow(hWnd);
WSACleanup();
return -1;
}
int reuse = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) == SOCKET_ERROR) {
printf("setsockopt 失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
DestroyWindow(hWnd);
WSACleanup();
return -1;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("绑定失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
DestroyWindow(hWnd);
WSACleanup();
return -1;
}
if (listen(listen_fd, 5) == SOCKET_ERROR) {
printf("监听失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
DestroyWindow(hWnd);
WSACleanup();
return -1;
}
if (WSAAsyncSelect(listen_fd, hWnd, WM_SOCKET, FD_ACCEPT) == SOCKET_ERROR) {
printf("WSAAsyncSelect 初始化失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
DestroyWindow(hWnd);
WSACleanup();
return -1;
}
printf("WSAAsyncSelect服务器(消息驱动)启动:端口=%d\n", PORT);
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
closesocket(listen_fd);
for (i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].in_use) {
cleanup_client(&clients[i]);
}
}
UnregisterClass("SocketWindowClass", GetModuleHandle(NULL));
WSACleanup();
return 0;
}
异步 I/O:中高至超高并发的核心方案
作为 Windows 应对中高至超高并发的关键,异步 I/O 突破线程阻塞限制,实现 "发起即返回、内核传数据、完成再通知" 的全异步流程。核心是 "重叠 I/O(底层基础,支持零拷贝、管理异步状态)+ IOCP(高并发优化,内核动态调度线程)" 架构,依托 Windows 内核特性,适配游戏万级任务服、十万级 MMO 主服场景。
重叠 I/O(Overlapped I/O):中高并发的异步进阶方案
核心机制 :"等待就绪" 与 "数据拷贝" 均不阻塞线程。在回声服务器中,通过 WSAOVERLAPPED
发起异步 WSARecv
(接收客户端数据),线程无需等待即可处理其他任务;当内核完成数据接收与拷贝后,会激活关联的 WSAEVENT
;线程通过 WSAWaitForMultipleEvents
捕获事件后,必须调用 WSAGetOverlappedResult
获取异步操作的具体结果(如实际接收 / 发送的字节数、错误码),再基于结果发起异步 WSASend
执行回声逻辑,确保操作状态准确。
与 Linux 对比:类似 Linux 的 "非阻塞 I/O + epoll" 组合,Linux 需用户态维护非阻塞 Socket 与 epoll 事件的关联,手动检查操作结果;Windows 重叠 I/O 由内核托管异步操作,无需用户态维护状态,WSAGetOverlappedResult 可直接获取操作结果,减少用户态与内核态的交互次数,效率比 Linux 同方案高 20%~30%,接近 Linux io_uring 模型(但 io_uring 需用户态维护提交队列,重叠 I/O 无需)。
适用场景:高并发场景,如 1 万人同服的日常任务服务器(需处理任务提交、奖励发放等高频数据)、游戏道具商城(玩家频繁领取道具,需低延迟响应)。
ini
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <stdbool.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 10
#define IP_LEN 16
typedef struct {
SOCKET fd; // 客户端套接字
bool in_use; // 连接状态
char ip[IP_LEN]; // 客户端IP
int port; // 客户端端口
char read_buf[BUF_SIZE]; // 读缓冲区
char send_buf[BUF_SIZE]; // 发送缓冲区
int send_len; // 待发送长度
int send_pos; // 发送位置
WSAOVERLAPPED overlapped; // 重叠结构
WSAEVENT event; // 独立事件对象(关键修复:使用独立事件而非重叠结构内的事件)
bool is_sending; // 操作类型标记
} Client;
Client clients[MAX_CLIENTS];
WSAEVENT events[MAX_CLIENTS + 1]; // 0号为监听事件,1~MAX为客户端事件
int event_count = 0;
// 初始化客户端
void init_client(Client* cli, SOCKET fd, const char* ip, int port) {
if (cli == NULL) return;
memset(cli, 0, sizeof(Client));
cli->fd = fd;
cli->in_use = true;
strncpy(cli->ip, ip, IP_LEN - 1);
cli->port = port;
cli->send_len = 0;
cli->send_pos = 0;
cli->is_sending = false;
// 创建独立事件对象,与重叠结构分离
cli->event = WSACreateEvent();
memset(&cli->overlapped, 0, sizeof(WSAOVERLAPPED));
}
// 清理客户端
void cleanup_client(Client* cli) {
if (cli == NULL || !cli->in_use) return;
closesocket(cli->fd);
WSACloseEvent(cli->event); // 关闭独立事件
memset(cli, 0, sizeof(Client));
cli->in_use = false;
}
// 查找空闲客户端槽位
int find_free_client() {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (!clients[i].in_use) {
return i;
}
}
return -1;
}
// 继续发送剩余数据
void continue_sending(Client* cli) {
if (cli == NULL || !cli->in_use || cli->send_pos >= cli->send_len) {
return;
}
WSABUF wsa_buf;
DWORD bytes_sent;
int remaining = cli->send_len - cli->send_pos;
wsa_buf.buf = cli->send_buf + cli->send_pos;
wsa_buf.len = remaining;
// 重置重叠结构,绑定到客户端独立事件
memset(&cli->overlapped, 0, sizeof(WSAOVERLAPPED));
cli->overlapped.hEvent = cli->event; // 绑定到独立事件
cli->is_sending = true;
if (WSASend(cli->fd, &wsa_buf, 1, &bytes_sent, 0, &cli->overlapped, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSA_IO_PENDING) {
printf("客户端:%s:%d send失败: %d\n", cli->ip, cli->port, WSAGetLastError());
cleanup_client(cli);
return;
}
}
else {
// 数据立即发送完成
cli->send_pos += bytes_sent;
if (cli->send_pos >= cli->send_len) {
printf("发送:%s(%d字节)→ 发送完成\n", cli->send_buf, cli->send_len);
cli->is_sending = false;
cli->send_len = 0;
cli->send_pos = 0;
// 重新发起读操作(确保读操作正确绑定事件)
WSABUF recv_buf;
DWORD bytes_recv, flags = 0;
recv_buf.buf = cli->read_buf;
recv_buf.len = BUF_SIZE - 1;
memset(&cli->overlapped, 0, sizeof(WSAOVERLAPPED));
cli->overlapped.hEvent = cli->event; // 绑定到独立事件
if (WSARecv(cli->fd, &recv_buf, 1, &bytes_recv, &flags, &cli->overlapped, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSA_IO_PENDING) {
printf("客户端:%s:%d recv失败: %d\n", cli->ip, cli->port, WSAGetLastError());
cleanup_client(cli);
return;
}
}
}
}
}
// 处理I/O完成事件
void process_overlapped_result(Client* cli) {
if (cli == NULL || !cli->in_use) return;
DWORD bytes_transferred, flags = 0;
// 获取操作结果(使用WSAGetOverlappedResult获取实际传输字节数)
if (!WSAGetOverlappedResult(cli->fd, &cli->overlapped, &bytes_transferred, FALSE, &flags)) {
printf("客户端:%s:%d异常断开(错误码:%d)\n", cli->ip, cli->port, WSAGetLastError());
cleanup_client(cli);
return;
}
// 处理读操作结果
if (!cli->is_sending) {
if (bytes_transferred == 0) {
printf("客户端:%s:%d正常断开\n", cli->ip, cli->port);
cleanup_client(cli);
return;
}
cli->read_buf[bytes_transferred] = '\0';
printf("接收:%s(%d字节)→ 暂存待发送\n", cli->read_buf, (int)bytes_transferred);
// 准备发送数据
memcpy(cli->send_buf, cli->read_buf, bytes_transferred);
cli->send_len = (int)bytes_transferred;
cli->send_pos = 0;
// 开始发送
continue_sending(cli);
}
// 处理写操作结果
else {
if (bytes_transferred == 0) {
printf("客户端:%s:%d发送失败\n", cli->ip, cli->port);
cleanup_client(cli);
return;
}
cli->send_pos += bytes_transferred;
// 检查是否还有剩余数据
if (cli->send_pos < cli->send_len) {
continue_sending(cli); // 继续发送剩余部分
}
else {
printf("发送:%s(%d字节)→ 发送完成\n", cli->send_buf, cli->send_len);
cli->is_sending = false;
cli->send_len = 0;
cli->send_pos = 0;
// 重新发起读操作(确保每次发送后都能接收新数据)
WSABUF recv_buf;
DWORD bytes_recv;
recv_buf.buf = cli->read_buf;
recv_buf.len = BUF_SIZE - 1;
flags = 0;
memset(&cli->overlapped, 0, sizeof(WSAOVERLAPPED));
cli->overlapped.hEvent = cli->event; // 绑定到独立事件
if (WSARecv(cli->fd, &recv_buf, 1, &bytes_recv, &flags, &cli->overlapped, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSA_IO_PENDING) {
printf("客户端:%s:%d recv失败: %d\n", cli->ip, cli->port, WSAGetLastError());
cleanup_client(cli);
return;
}
}
}
}
}
int main() {
WSADATA wsaData;
SOCKET listen_fd = INVALID_SOCKET, client_fd = INVALID_SOCKET;
struct sockaddr_in server_addr, client_addr;
int client_addr_len = sizeof(client_addr);
// 初始化Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup 失败: %d\n", WSAGetLastError());
return -1;
}
// 初始化客户端列表
for (int i = 0; i < MAX_CLIENTS; i++) {
clients[i].in_use = false;
clients[i].event = WSA_INVALID_EVENT;
}
// 创建监听套接字
listen_fd = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (listen_fd == INVALID_SOCKET) {
printf("创建套接字失败: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
// 端口复用
int reuse = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) == SOCKET_ERROR) {
printf("setsockopt 失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("绑定失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 开始监听
if (listen(listen_fd, 5) == SOCKET_ERROR) {
printf("监听失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 初始化监听事件(0号事件)
events[0] = WSACreateEvent();
if (events[0] == WSA_INVALID_EVENT) {
printf("创建监听事件失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
event_count = 1; // 初始事件数:仅监听事件
WSAEventSelect(listen_fd, events[0], FD_ACCEPT);
printf("重叠I/O服务器(IO多路异步)启动:端口=%d\n", PORT);
// 事件循环
while (1) {
// 等待事件(正确处理事件索引)
DWORD wait_result = WSAWaitForMultipleEvents(event_count, events, FALSE, WSA_INFINITE, FALSE);
if (wait_result == WSA_WAIT_FAILED) {
printf("WSAWaitForMultipleEvents 失败: %d\n", WSAGetLastError());
break;
}
int event_idx = wait_result - WSA_WAIT_EVENT_0;
if (event_idx >= event_count) continue;
// 重置事件(必须在处理前重置)
if (!WSAResetEvent(events[event_idx])) {
printf("WSAResetEvent 失败: %d\n", WSAGetLastError());
continue;
}
// 处理新连接(0号事件)
if (event_idx == 0) {
WSANETWORKEVENTS net_events;
if (WSAEnumNetworkEvents(listen_fd, events[0], &net_events) == SOCKET_ERROR) {
printf("WSAEnumNetworkEvents 失败: %d\n", WSAGetLastError());
continue;
}
if (net_events.lNetworkEvents & FD_ACCEPT) {
if (net_events.iErrorCode[FD_ACCEPT_BIT] != 0) {
printf("FD_ACCEPT错误: %d\n", net_events.iErrorCode[FD_ACCEPT_BIT]);
continue;
}
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == INVALID_SOCKET) {
printf("accept 失败: %d\n", WSAGetLastError());
continue;
}
int slot = find_free_client();
if (slot == -1) {
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("客户端:%s:%d 超出最大限制(%d),拒绝连接\n",
client_ip, ntohs(client_addr.sin_port), MAX_CLIENTS);
closesocket(client_fd);
continue;
}
// 初始化客户端
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);
init_client(&clients[slot], client_fd, client_ip, client_port);
// 添加客户端事件到事件数组
if (event_count < MAX_CLIENTS + 1) {
events[event_count] = clients[slot].event;
event_count++;
}
// 发起首次读操作
WSABUF wsa_buf;
DWORD bytes_recv, flags = 0;
wsa_buf.buf = clients[slot].read_buf;
wsa_buf.len = BUF_SIZE - 1;
memset(&clients[slot].overlapped, 0, sizeof(WSAOVERLAPPED));
clients[slot].overlapped.hEvent = clients[slot].event; // 绑定事件
if (WSARecv(client_fd, &wsa_buf, 1, &bytes_recv, &flags,
&clients[slot].overlapped, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSA_IO_PENDING) {
printf("recv 失败: %d\n", WSAGetLastError());
cleanup_client(&clients[slot]);
event_count--;
continue;
}
}
printf("新客户端连接:%s:%d\n", client_ip, client_port);
}
}
// 处理客户端事件(1~MAX_CLIENTS号事件)
else {
// 查找事件对应的客户端(正确匹配事件与客户端)
int cli_idx = -1;
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].in_use && clients[i].event == events[event_idx]) {
cli_idx = i;
break;
}
}
if (cli_idx == -1) {
// 移除无效事件(清理不再使用的事件)
for (int i = event_idx; i < event_count - 1; i++) {
events[i] = events[i + 1];
}
event_count--;
continue;
}
// 处理I/O结果
process_overlapped_result(&clients[cli_idx]);
// 检查客户端是否已断开,更新事件数组
if (!clients[cli_idx].in_use) {
for (int i = event_idx; i < event_count - 1; i++) {
events[i] = events[i + 1];
}
event_count--;
}
}
}
// 资源清理
for (int i = 0; i < event_count; i++) {
WSACloseEvent(events[i]);
}
closesocket(listen_fd);
WSACleanup();
return 0;
}
I/O 完成端口(IOCP):超高并发的性能最优解
核心机制:通过 "完成端口对象 + 工作线程池" 实现超高并发。在回声服务器中,先调用 CreateIoCompletionPort 创建完成端口,并将监听 Socket 及新接入的所有客户端 Socket 绑定到该端口;发起异步 WSARecv(传入封装了操作类型、缓冲区的 OverlappedOp 结构体)后,工作线程池中的线程通过 GetQueuedCompletionStatus 阻塞等待;当回声读写完成,线程直接获取事件(含客户端 Socket、传输字节数、操作类型),读完成则暂存数据并发起异步 WSASend,写完成则再次发起 WSARecv,循环执行回声逻辑。
与 Linux 对比:Linux 无直接对应模型,功能上接近 io_uring 但效率更高, epoll 仅负责 "就绪事件通知",需用户态遍历事件列表、手动发起 I/O 操作;且无原生线程池,需自行实现线程管理(如 pthread 线程池),高并发下存在用户态逻辑开销; io_uring 虽支持异步 I/O 与内核队列,减少了部分用户态干预,但生态不成熟(如驱动支持有限、库适配少),且线程调度需用户态干预;IOCP 由 Windows 内核深度整合 "完成队列 + 工作线程池",I/O 操作触发、结果回调、线程调度均由内核优化(如线程数自动匹配 CPU 核心),无需用户态额外处理;在 10 万连接的高并发场景下,指令处理吞吐量比 Linux epoll 高 20%-30%,且稳定性更优。
适用场景:十万级超高并发场景,如 MMO 游戏主服(需支撑海量玩家同时在线、跨服交互)、大型赛事服务器(需低延迟处理战斗数据)。
ini
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <winsock2.h>
#include <windows.h>
#include <process.h>
#include <stdbool.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 1024
#define WORKER_THREAD_COUNT 4
#define IP_LEN 16
// 操作类型枚举
typedef enum {
OP_READ,
OP_WRITE
} OpType;
// 重叠操作结构体
typedef struct {
WSAOVERLAPPED overlapped;
WSABUF wsa_buf;
OpType op_type;
SOCKET fd;
int client_idx;
} OverlappedOp;
// 客户端结构体
typedef struct {
SOCKET fd; // 客户端套接字
char ip[IP_LEN]; // 客户端IP地址
int port; // 客户端端口
char read_buf[BUF_SIZE]; // 读缓冲区
char send_buf[BUF_SIZE]; // 发送缓冲区
int send_len; // 待发送长度
struct sockaddr_in addr; // 客户端地址
bool in_use; // 连接状态
} Client;
Client clients[MAX_CLIENTS];
HANDLE iocp_handle; // IOCP句柄
// 初始化客户端
void init_client(Client* cli) {
cli->fd = INVALID_SOCKET;
cli->send_len = 0;
cli->in_use = false;
memset(cli->ip, 0, IP_LEN);
cli->port = 0;
memset(cli->read_buf, 0, BUF_SIZE);
memset(cli->send_buf, 0, BUF_SIZE);
memset(&cli->addr, 0, sizeof(struct sockaddr_in));
}
// 查找空闲客户端槽位
int find_free_client() {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (!clients[i].in_use) {
return i;
}
}
return -1;
}
// 工作线程函数(处理IOCP完成事件)
unsigned int __stdcall worker_thread(void* arg) {
DWORD bytes_transferred;
ULONG_PTR completion_key;
OverlappedOp* op;
Client* cli;
int last_error;
while (1) {
// 获取完成事件
if (GetQueuedCompletionStatus(iocp_handle, &bytes_transferred,
&completion_key, (LPOVERLAPPED*)&op, INFINITE) == 0) {
last_error = GetLastError();
// 处理客户端异常断开(错误码64与select的异常断开同步)
if (last_error == ERROR_NETNAME_DELETED && op != NULL) {
int client_idx = op->client_idx;
if (client_idx >= 0 && client_idx < MAX_CLIENTS) {
cli = &clients[client_idx];
if (cli->in_use && cli->fd == op->fd) {
// 与select版本完全一致的异常断开提示
printf("客户端:%s:%d异常断开\n", cli->ip, cli->port);
closesocket(cli->fd);
init_client(cli);
}
}
free(op);
continue;
}
// 其他错误保持原有提示格式
printf("GetQueuedCompletionStatus 失败: %d\n", last_error);
continue;
}
// 处理关闭通知
if (completion_key == 0 && bytes_transferred == 0) {
break;
}
int client_idx = op->client_idx;
if (client_idx < 0 || client_idx >= MAX_CLIENTS) {
free(op);
continue;
}
cli = &clients[client_idx];
if (!cli->in_use || cli->fd != op->fd) {
free(op);
continue;
}
// 处理读操作完成
if (op->op_type == OP_READ) {
if (bytes_transferred == 0) {
// 正常断开提示(与select一致)
printf("客户端:%s:%d正常断开\n", cli->ip, cli->port);
closesocket(cli->fd);
init_client(cli);
free(op);
continue;
}
cli->read_buf[bytes_transferred] = '\0';
cli->send_len = bytes_transferred;
memcpy(cli->send_buf, cli->read_buf, bytes_transferred);
printf("接收:%s(%d字节)→ 暂存待发送\n", cli->read_buf, (int)bytes_transferred);
// 发起写操作
OverlappedOp* write_op = (OverlappedOp*)malloc(sizeof(OverlappedOp));
memset(write_op, 0, sizeof(OverlappedOp));
write_op->op_type = OP_WRITE;
write_op->fd = cli->fd;
write_op->client_idx = client_idx;
write_op->wsa_buf.buf = cli->send_buf;
write_op->wsa_buf.len = cli->send_len;
DWORD bytes_sent;
if (WSASend(cli->fd, &write_op->wsa_buf, 1, &bytes_sent, 0,
(LPWSAOVERLAPPED)write_op, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSA_IO_PENDING) {
printf("WSASend 失败: %d\n", WSAGetLastError());
closesocket(cli->fd);
init_client(cli);
free(write_op);
}
}
}
// 处理写操作完成
else if (op->op_type == OP_WRITE) {
if (bytes_transferred <= 0) {
printf("客户端:%s:%d发送失败\n", cli->ip, cli->port);
closesocket(cli->fd);
init_client(cli);
free(op);
continue;
}
printf("发送:%s(%d字节)→ 发送完成\n", cli->send_buf, (int)bytes_transferred);
cli->send_len = 0;
// 发起读操作,支持多次通信
OverlappedOp* read_op = (OverlappedOp*)malloc(sizeof(OverlappedOp));
memset(read_op, 0, sizeof(OverlappedOp));
read_op->op_type = OP_READ;
read_op->fd = cli->fd;
read_op->client_idx = client_idx;
read_op->wsa_buf.buf = cli->read_buf;
read_op->wsa_buf.len = BUF_SIZE - 1;
DWORD bytes_recv, flags = 0;
if (WSARecv(cli->fd, &read_op->wsa_buf, 1, &bytes_recv, &flags,
(LPWSAOVERLAPPED)read_op, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSA_IO_PENDING) {
printf("WSARecv 失败: %d\n", WSAGetLastError());
closesocket(cli->fd);
init_client(cli);
free(read_op);
}
}
}
free(op);
}
return 0;
}
int main() {
WSADATA wsaData;
SOCKET listen_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
int client_addr_len = sizeof(client_addr);
HANDLE worker_threads[WORKER_THREAD_COUNT];
// 初始化Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup 失败: %d\n", WSAGetLastError());
return -1;
}
// 初始化客户端列表
for (int i = 0; i < MAX_CLIENTS; i++) {
init_client(&clients[i]);
}
// 创建IOCP
iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (iocp_handle == NULL) {
printf("CreateIoCompletionPort 失败: %d\n", GetLastError());
return -1;
}
// 创建工作线程
for (int i = 0; i < WORKER_THREAD_COUNT; i++) {
worker_threads[i] = (HANDLE)_beginthreadex(NULL, 0, worker_thread, NULL, 0, NULL);
if (worker_threads[i] == NULL) {
printf("创建工作线程 失败\n");
return -1;
}
}
// 创建监听套接字
listen_fd = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (listen_fd == INVALID_SOCKET) {
printf("创建套接字 失败: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
// 端口复用
int reuse = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) == SOCKET_ERROR) {
printf("setsockopt 失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("绑定失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 开始监听
if (listen(listen_fd, 5) == SOCKET_ERROR) {
printf("监听失败: %d\n", WSAGetLastError());
closesocket(listen_fd);
WSACleanup();
return -1;
}
// 将监听套接字关联到IOCP
CreateIoCompletionPort((HANDLE)listen_fd, iocp_handle, (ULONG_PTR)listen_fd, 0);
// 启动提示
printf("IOCP服务器(IO完成端口)启动:端口=%d\n", PORT);
// 接受连接循环
while (1) {
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == INVALID_SOCKET) {
printf("accept 失败: %d\n", WSAGetLastError());
Sleep(10);
continue;
}
int slot = find_free_client();
if (slot == -1) {
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("客户端:%s:%d 超出最大限制(%d),拒绝连接\n",
client_ip, ntohs(client_addr.sin_port), MAX_CLIENTS);
closesocket(client_fd);
continue;
}
// 初始化客户端信息
Client* new_cli = &clients[slot];
new_cli->fd = client_fd;
new_cli->addr = client_addr;
new_cli->in_use = true;
inet_ntop(AF_INET, &client_addr.sin_addr, new_cli->ip, IP_LEN - 1);
new_cli->port = ntohs(client_addr.sin_port);
// 将客户端套接字关联到IOCP
if (CreateIoCompletionPort((HANDLE)client_fd, iocp_handle, (ULONG_PTR)client_fd, 0) == NULL) {
printf("关联IOCP 失败: %d\n", GetLastError());
closesocket(client_fd);
init_client(new_cli);
continue;
}
// 发起第一个读操作
OverlappedOp* read_op = (OverlappedOp*)malloc(sizeof(OverlappedOp));
memset(read_op, 0, sizeof(OverlappedOp));
read_op->op_type = OP_READ;
read_op->fd = client_fd;
read_op->client_idx = slot;
read_op->wsa_buf.buf = new_cli->read_buf;
read_op->wsa_buf.len = BUF_SIZE - 1;
DWORD bytes_recv, flags = 0;
if (WSARecv(client_fd, &read_op->wsa_buf, 1, &bytes_recv, &flags,
(LPWSAOVERLAPPED)read_op, NULL) == SOCKET_ERROR) {
if (WSAGetLastError() != WSA_IO_PENDING) {
printf("WSARecv 失败: %d\n", WSAGetLastError());
closesocket(client_fd);
init_client(new_cli);
free(read_op);
continue;
}
}
// 新连接提示
printf("新客户端连接:%s:%d\n", new_cli->ip, new_cli->port);
}
// 清理资源
for (int i = 0; i < WORKER_THREAD_COUNT; i++) {
PostQueuedCompletionStatus(iocp_handle, 0, 0, NULL);
WaitForSingleObject(worker_threads[i], INFINITE);
CloseHandle(worker_threads[i]);
}
closesocket(listen_fd);
CloseHandle(iocp_handle);
WSACleanup();
return 0;
}
总结:Windows Socket 六种 I/O 模型核心要点
Windows Socket 六种 I/O 模型(阻塞 I/O、非阻塞 I/O、select、WSAEventSelect、WSAAsyncSelect、重叠 I/O、I/O 完成端口)的核心差异,源于对 "等待数据就绪" 与 "数据拷贝" 两阶段的阻塞策略、事件通知方式设计不同(阻塞 I/O 两阶段均阻塞,实现简单;非阻塞 I/O 等待阶段不阻塞却需轮询,CPU 消耗高;select 兼容旧系统但存在 Socket 数量限制;WSAEventSelect 依托内核事件驱动,效率优于 select;WSAAsyncSelect 基于 Windows 窗口消息传递事件,专为 GUI 程序设计;重叠 I/O 实现全异步且由内核托管流程;I/O 完成端口(IOCP)通过 "完成端口对象 + 内核线程池" 深度优化,性能最优)。