Windows中网络编程selec模型和IOCP网络IO模型
一、select 模型
1. 基本原理
select 是一种 同步 I/O 多路复用 模型。核心思想是:用一个线程同时监视多个 socket,当其中任意一个 socket 有数据可读/可写/异常时,select 返回,然后程序逐个检查处理。
单线程
│
▼
select(监视 socket A, B, C, D...) ← 阻塞等待
│
▼ 某个 socket 就绪
遍历所有 socket,逐个检查 FD_ISSET
│
▼
recv/send 处理数据
│
▼
回到 select 继续等待
2. 适用场景
| 场景 | 说明 |
|---|---|
| 连接数较少 | 几十个到几百个客户端 |
| 跨平台需求 | Linux/Unix 也支持 select(虽然实现不同) |
| 简单应用 | 聊天室、小型游戏服务器、测试工具 |
| 学习/原型 | 理解 I/O 多路复用的入门模型 |
3. 核心缺点
| 问题 | 说明 |
|---|---|
| 遍历开销 | 每次返回都要遍历所有 socket(O(n)),连接越多越慢 |
| fd_set 大小限制 | Windows 默认最多 64 个 socket(可改,但有限) |
| 频繁用户态/内核态切换 | 每次调用都要把 fd_set 从用户空间拷贝到内核空间 |
| 不能充分利用多核 | 单线程处理所有 I/O,CPU 可能成为瓶颈 |
4.select 注意事项
fd_set 大小限制
C
// Windows 默认 FD_SETSIZE = 64
// 需要修改可以:
#define FD_SETSIZE 1024 // 必须在包含 winsock2.h 之前定义
#include <winsock2.h>
- 第一个参数在 Windows 中被忽略 ,但通常写
0 - 每次循环都要重新设置 fd_set ,因为
select会修改它 - 注意检查 FD_ISSET 的顺序:先处理 listen socket 的新连接,再处理客户端数据
- 发送大数据时要处理部分发送 :
send可能只发送了一部分,需要循环发送
5.基于Select模型测试代码
C++
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 8080
#define MAX_CLIENTS 64
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 创建监听 socket
SOCKET listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// 允许地址复用
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, SOMAXCONN);
printf("Server listening on port %d...\n", PORT);
// 客户端 socket 数组
SOCKET clients[MAX_CLIENTS];
int client_count = 0;
for (int i = 0; i < MAX_CLIENTS; i++) clients[i] = INVALID_SOCKET;
fd_set read_fds, master_fds;
FD_ZERO(&master_fds);
FD_SET(listen_fd, &master_fds);
SOCKET max_fd = listen_fd;
char buffer[1024];
while (1) {
read_fds = master_fds;
// select 超时设置(NULL 表示阻塞)
struct timeval tv = {1, 0}; // 1秒超时
// Windows 中第一个参数被忽略,但为了兼容通常设为 max_fd + 1
int ret = select(0, &read_fds, NULL, NULL, &tv);
if (ret == SOCKET_ERROR) {
printf("select error: %d\n", WSAGetLastError());
break;
}
if (ret == 0) {
// 超时,可以在这里做定时任务
continue;
}
// 检查监听 socket 是否有新连接
if (FD_ISSET(listen_fd, &read_fds)) {
struct sockaddr_in client_addr;
int addr_len = sizeof(client_addr);
SOCKET new_client = accept(listen_fd, (struct sockaddr*)&client_addr, &addr_len);
if (new_client != INVALID_SOCKET) {
printf("New connection from %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 添加到客户端数组
int added = 0;
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i] == INVALID_SOCKET) {
clients[i] = new_client;
FD_SET(new_client, &master_fds);
if (new_client > max_fd) max_fd = new_client;
client_count++;
added = 1;
break;
}
}
if (!added) {
printf("Max clients reached, connection rejected\n");
closesocket(new_client);
}
}
}
// 检查客户端 socket 是否有数据
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i] != INVALID_SOCKET && FD_ISSET(clients[i], &read_fds)) {
int recv_len = recv(clients[i], buffer, sizeof(buffer) - 1, 0);
if (recv_len > 0) {
buffer[recv_len] = '\0';
printf("Received from client %d: %s\n", i, buffer);
// 回声
send(clients[i], buffer, recv_len, 0);
} else if (recv_len == 0 || WSAGetLastError() == WSAECONNRESET) {
printf("Client %d disconnected\n", i);
closesocket(clients[i]);
FD_CLR(clients[i], &master_fds);
clients[i] = INVALID_SOCKET;
client_count--;
}
}
}
}
// 清理
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i] != INVALID_SOCKET) closesocket(clients[i]);
}
closesocket(listen_fd);
WSACleanup();
return 0;
}
客户端测试代码:
c++
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <string.h>
#include <conio.h>
#pragma comment(lib, "ws2_32.lib")
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
printf("WSAStartup failed: %d\n", result);
return 1;
}
SOCKET client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == INVALID_SOCKET) {
printf("socket failed: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
struct sockaddr_in server_addr = { 0 };
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
printf("Connecting to 127.0.0.1:%d ...\n", SERVER_PORT);
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("connect failed: %d\n", WSAGetLastError());
closesocket(client_fd);
WSACleanup();
return 1;
}
printf("Connected successfully!\n");
printf("----------------------------------------\n");
printf("Usage: Type message and press Enter to send\n");
printf(" Type 'quit' to exit\n");
printf("----------------------------------------\n\n");
// 设置 socket 为非阻塞模式,这样 recv 不会卡住
u_long mode = 1; // 1 = non-blocking
ioctlsocket(client_fd, FIONBIO, &mode);
char send_buffer[BUFFER_SIZE];
char recv_buffer[BUFFER_SIZE];
int recv_total = 0;
while (1) {
// 检查是否有服务器数据(非阻塞接收)
int recv_len = recv(client_fd, recv_buffer + recv_total, BUFFER_SIZE - recv_total - 1, 0);
if (recv_len > 0) {
recv_total += recv_len;
recv_buffer[recv_total] = '\0';
// 检查是否收到完整消息(简单处理:假设消息以换行或缓冲区满为结束)
printf("[RECV from Server] %s\n", recv_buffer);
recv_total = 0; // 重置缓冲区
}
else if (recv_len == 0) {
printf("\n[Server closed connection]\n");
break;
}
else {
int err = WSAGetLastError();
if (err != WSAEWOULDBLOCK) {
printf("\n[recv error: %d]\n", err);
break;
}
// WSAEWOULDBLOCK 表示暂时没有数据,正常
}
// 检查用户输入(使用 _kbhit 非阻塞检测键盘)
if (_kbhit()) {
printf("[SEND] ");
if (fgets(send_buffer, BUFFER_SIZE, stdin) != NULL) {
// 去掉末尾换行符
size_t len = strlen(send_buffer);
if (len > 0 && send_buffer[len - 1] == '\n') {
send_buffer[len - 1] = '\0';
len--;
}
if (strcmp(send_buffer, "quit") == 0) {
printf("[Disconnecting...]\n");
break;
}
if (len > 0) {
// 发送数据
int send_len = send(client_fd, send_buffer, (int)len, 0);
if (send_len == SOCKET_ERROR) {
printf("[send failed: %d]\n", WSAGetLastError());
break;
}
printf("[Sent %d bytes] Waiting for echo...\n", send_len);
}
}
}
Sleep(10); // 10ms 休眠,避免 CPU 空转
}
printf("\n----------------------------------------\n");
printf("Closing connection...\n");
closesocket(client_fd);
WSACleanup();
printf("Client exited. Press any key to close...\n");
_getch();
return 0;
}
二、IOCP模型(I/O Completion Port)
1. 基本原理
IOCP 是 Windows 特有的 异步 I/O + 完成端口 机制,属于 Proactor 模式。
核心流程:
-
创建一个完成端口(Completion Port)
-
把 socket 绑定到这个端口
-
发起异步操作(如
WSARecv),立即返回不阻塞 -
操作系统在后台处理 I/O
-
I/O 完成后,结果放入完成端口的队列
-
工作线程从队列中取出结果处理
主线程 操作系统内核 工作线程池
│ │ │
│ 创建 IOCP │ │
│─────────────────────────►│ │
│ │ │
│ 绑定 socket 到 IOCP │ │
│─────────────────────────►│ │
│ │ │
│ WSARecv(重叠I/O) │ │
│─────────────────────────►│ 后台异步读取数据 │
│ 立即返回 │◄─────────────────────────│
│ │ │
│ 继续处理其他事情 │ 数据读取完成 │
│ │─────────────────────────►│
│ │ 放入完成队列 │
│ │ │
│ │ │ GetQueuedCompletionStatus
│ │ │ 取出结果,处理数据
│ │ │
│ │ │ 继续等待下一个完成通知
2. 适用场景
| 场景 | 说明 |
|---|---|
| 高并发连接 | 几千到几万甚至更多客户端 |
| 高吞吐量 | 文件传输、视频流、游戏服务器、代理服务器 |
| 需要利用多核 | 工作线程池可以跑在多个 CPU 核心上 |
| Windows 平台 | IOCP 是 Windows 最高效的网络 I/O 模型 |
3. 核心优势
| 优势 | 说明 |
|---|---|
| 真正的异步 | 发起 I/O 后立即返回,线程不等待 |
| 零拷贝优化 | 数据直接从网卡到用户缓冲区,减少内核/用户态拷贝 |
| 线程池管理 | 通过 Concurrency 参数控制活跃线程数,避免线程爆炸 |
| 高性能 | 可以轻松支撑数万并发连接 |
| 自动负载均衡 | 完成端口自动把工作分配给空闲的工作线程 |
4.注意事项
-
重叠结构(OVERLAPPED)生命周期
c// 错误:用栈上的 OVERLAPPED OVERLAPPED ol; // 函数返回后栈释放,但 I/O 可能还没完成! // 正确:用堆分配或池化 PER_IO_DATA* ioData = new PER_IO_DATA(); // 包含 OVERLAPPED -
粘包/拆包问题更突出
- 异步 I/O 可能一次收到不完整的数据包
- 需要自己维护接收缓冲区,做包边界解析
-
工作线程数设置
c// 通常设为 CPU 核心数 SYSTEM_INFO si; GetSystemInfo(&si); int threadCount = si.dwNumberOfProcessors * 2; // 或 * 1 -
优雅关闭
- 先
shutdown(s, SD_SEND)发送 FIN - 等待对方关闭或超时后再
closesocket
- 先
-
避免内存泄漏
- 每个
WSARecv/WSASend分配的 PER_IO_DATA 必须在完成回调中释放 - 连接关闭时要清理所有待处理的 I/O 请求
- 每个
5.IOCP测试代码
C++
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define PORT 8080
#define MAX_WORKER_THREADS 4
#define BUFFER_SIZE 4096
#define MAX_CONCURRENT_CONNECTIONS 1000
// 每个连接的操作类型
typedef enum {
OP_ACCEPT, // 接受连接
OP_READ, // 读取数据
OP_WRITE // 写入数据
} OPERATION_TYPE;
// 重叠 I/O 的上下文结构(必须包含 OVERLAPPED)
typedef struct {
OVERLAPPED overlapped; // 必须是第一个字段!
SOCKET socket;
WSABUF wsaBuf;
char buffer[BUFFER_SIZE];
OPERATION_TYPE opType;
DWORD bytesTransferred;
} PER_IO_DATA, *LPPER_IO_DATA;
// 每个连接的信息
typedef struct {
SOCKET socket;
sockaddr_in addr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
// 工作者线程函数
DWORD WINAPI WorkerThread(LPVOID lpParam) {
HANDLE completionPort = (HANDLE)lpParam;
DWORD bytesTransferred;
ULONG_PTR completionKey;
LPPER_IO_DATA ioData = NULL;
BOOL success;
while (1) {
// 等待 I/O 完成(阻塞)
success = GetQueuedCompletionStatus(
completionPort,
&bytesTransferred,
&completionKey,
(LPOVERLAPPED*)&ioData,
INFINITE
);
// 检查退出信号
if (!success && ioData == NULL) {
if (GetLastError() == ERROR_ABANDONED_WAIT_0) {
break; // IOCP 被关闭
}
continue;
}
// 客户端断开或出错
if (!success || (bytesTransferred == 0 && ioData->opType == OP_READ)) {
printf("Client disconnected or error: %d\n", GetLastError());
if (completionKey != 0) {
LPPER_HANDLE_DATA handleData = (LPPER_HANDLE_DATA)completionKey;
closesocket(handleData->socket);
free(handleData);
}
if (ioData) {
closesocket(ioData->socket);
free(ioData);
}
continue;
}
switch (ioData->opType) {
case OP_ACCEPT: {
printf("New client connected\n");
// 创建新的客户端上下文
LPPER_HANDLE_DATA clientHandle = (LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
clientHandle->socket = ioData->socket;
// 将新 socket 关联到 IOCP
CreateIoCompletionPort((HANDLE)ioData->socket, completionPort, (ULONG_PTR)clientHandle, 0);
// 发起第一个读请求
LPPER_IO_DATA readIo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
ZeroMemory(readIo, sizeof(PER_IO_DATA));
readIo->socket = ioData->socket;
readIo->wsaBuf.buf = readIo->buffer;
readIo->wsaBuf.len = BUFFER_SIZE;
readIo->opType = OP_READ;
DWORD flags = 0;
int ret = WSARecv(ioData->socket, &readIo->wsaBuf, 1, NULL, &flags,
&readIo->overlapped, NULL);
if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
printf("WSARecv failed: %d\n", WSAGetLastError());
free(readIo);
}
// 为下一个 accept 做准备
// (实际生产代码需要循环投递 accept)
free(ioData);
break;
}
case OP_READ: {
ioData->buffer[bytesTransferred] = '\0';
printf("Received %lu bytes: %s\n", bytesTransferred, ioData->buffer);
// 回声:将读取的数据写回
ioData->opType = OP_WRITE;
ioData->wsaBuf.len = bytesTransferred;
DWORD sent;
int ret = WSASend(ioData->socket, &ioData->wsaBuf, 1, &sent, 0,
&ioData->overlapped, NULL);
if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
printf("WSASend failed: %d\n", WSAGetLastError());
free(ioData);
}
break;
}
case OP_WRITE: {
printf("Sent %lu bytes\n", bytesTransferred);
// 继续读取
ZeroMemory(&ioData->overlapped, sizeof(OVERLAPPED));
ioData->opType = OP_READ;
ioData->wsaBuf.len = BUFFER_SIZE;
DWORD flags = 0;
int ret = WSARecv(ioData->socket, &ioData->wsaBuf, 1, NULL, &flags,
&ioData->overlapped, NULL);
if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
printf("WSARecv failed: %d\n", WSAGetLastError());
free(ioData);
}
break;
}
}
}
return 0;
}
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 1. 创建 IOCP
HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, MAX_WORKER_THREADS);
if (!completionPort) {
printf("CreateIoCompletionPort failed: %d\n", GetLastError());
return 1;
}
// 2. 创建工作者线程池
HANDLE threads[MAX_WORKER_THREADS];
for (int i = 0; i < MAX_WORKER_THREADS; i++) {
threads[i] = CreateThread(NULL, 0, WorkerThread, completionPort, 0, NULL);
}
// 3. 创建监听 socket
SOCKET listenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
int opt = 1;
setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listenSocket, (struct sockaddr*)&addr, sizeof(addr));
listen(listenSocket, SOMAXCONN);
printf("IOCP Server listening on port %d with %d worker threads...\n", PORT, MAX_WORKER_THREADS);
// 4. 使用 AcceptEx 异步接受连接(简化版:这里用同步 accept + 投递读请求演示)
// 注意:生产环境应使用 AcceptEx 实现完全异步
while (1) {
struct sockaddr_in clientAddr;
int addrLen = sizeof(clientAddr);
SOCKET clientSocket = accept(listenSocket, (struct sockaddr*)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET) continue;
printf("Accepted connection from %s:%d\n",
inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
// 将客户端 socket 关联到 IOCP
LPPER_HANDLE_DATA handleData = (LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
handleData->socket = clientSocket;
handleData->addr = clientAddr;
CreateIoCompletionPort((HANDLE)clientSocket, completionPort, (ULONG_PTR)handleData, 0);
// 投递异步读请求
LPPER_IO_DATA ioData = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
ZeroMemory(ioData, sizeof(PER_IO_DATA));
ioData->socket = clientSocket;
ioData->wsaBuf.buf = ioData->buffer;
ioData->wsaBuf.len = BUFFER_SIZE;
ioData->opType = OP_READ;
DWORD flags = 0;
DWORD recvBytes;
int ret = WSARecv(clientSocket, &ioData->wsaBuf, 1, &recvBytes, &flags,
&ioData->overlapped, NULL);
if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
printf("Initial WSARecv failed: %d\n", WSAGetLastError());
free(ioData);
closesocket(clientSocket);
free(handleData);
}
}
// 清理(实际中需要信号机制优雅退出)
for (int i = 0; i < MAX_WORKER_THREADS; i++) {
// 向每个线程发送退出信号
PostQueuedCompletionStatus(completionPort, 0, 0, NULL);
}
WaitForMultipleObjects(MAX_WORKER_THREADS, threads, TRUE, INFINITE);
for (int i = 0; i < MAX_WORKER_THREADS; i++) {
CloseHandle(threads[i]);
}
CloseHandle(completionPort);
closesocket(listenSocket);
WSACleanup();
return 0;
}
| 特性 | select | IOCP |
|---|---|---|
| I/O 模式 | 同步多路复用 | 异步完成端口 |
| 设计模式 | Reactor | Proactor |
| 连接数上限 | 641024(有限) | 理论上无上限(受内存限制) |
| 性能 | 低并发够用 | 高并发极优 |
| 线程模型 | 单线程轮询 | 多线程池处理 |
| CPU 利用 | 单核 | 多核充分利用 |
| 编程复杂度 | 简单 | 复杂(重叠 I/O、WSABUF、OVERLAPPED 等) |
| 可移植性 | 跨平台 | Windows 独有 |
| 内存拷贝 | 常规拷贝 | 零拷贝优化 |
| ---------------- | --------------------------------------- | |
| I/O 模式 | 同步多路复用 | 异步完成端口 |
| 设计模式 | Reactor | Proactor |
| 连接数上限 | 641024(有限) | 理论上无上限(受内存限制) |
| 性能 | 低并发够用 | 高并发极优 |
| 线程模型 | 单线程轮询 | 多线程池处理 |
| CPU 利用 | 单核 | 多核充分利用 |
| 编程复杂度 | 简单 | 复杂(重叠 I/O、WSABUF、OVERLAPPED 等) |
| 可移植性 | 跨平台 | Windows 独有 |
| 内存拷贝 | 常规拷贝 | 零拷贝优化 |