Windows中网络编程selec模型和IOCP网络IO模型

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>
  1. 第一个参数在 Windows 中被忽略 ,但通常写 0
  2. 每次循环都要重新设置 fd_set ,因为 select 会修改它
  3. 注意检查 FD_ISSET 的顺序:先处理 listen socket 的新连接,再处理客户端数据
  4. 发送大数据时要处理部分发送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 模式

核心流程:

  1. 创建一个完成端口(Completion Port)

  2. 把 socket 绑定到这个端口

  3. 发起异步操作(如 WSARecv),立即返回不阻塞

  4. 操作系统在后台处理 I/O

  5. I/O 完成后,结果放入完成端口的队列

  6. 工作线程从队列中取出结果处理

    主线程 操作系统内核 工作线程池
    │ │ │
    │ 创建 IOCP │ │
    │─────────────────────────►│ │
    │ │ │
    │ 绑定 socket 到 IOCP │ │
    │─────────────────────────►│ │
    │ │ │
    │ WSARecv(重叠I/O) │ │
    │─────────────────────────►│ 后台异步读取数据 │
    │ 立即返回 │◄─────────────────────────│
    │ │ │
    │ 继续处理其他事情 │ 数据读取完成 │
    │ │─────────────────────────►│
    │ │ 放入完成队列 │
    │ │ │
    │ │ │ GetQueuedCompletionStatus
    │ │ │ 取出结果,处理数据
    │ │ │
    │ │ │ 继续等待下一个完成通知

2. 适用场景

场景 说明
高并发连接 几千到几万甚至更多客户端
高吞吐量 文件传输、视频流、游戏服务器、代理服务器
需要利用多核 工作线程池可以跑在多个 CPU 核心上
Windows 平台 IOCP 是 Windows 最高效的网络 I/O 模型

3. 核心优势

优势 说明
真正的异步 发起 I/O 后立即返回,线程不等待
零拷贝优化 数据直接从网卡到用户缓冲区,减少内核/用户态拷贝
线程池管理 通过 Concurrency 参数控制活跃线程数,避免线程爆炸
高性能 可以轻松支撑数万并发连接
自动负载均衡 完成端口自动把工作分配给空闲的工作线程

4.注意事项

  1. 重叠结构(OVERLAPPED)生命周期

    c 复制代码
    // 错误:用栈上的 OVERLAPPED
    OVERLAPPED ol;  // 函数返回后栈释放,但 I/O 可能还没完成!
    
    // 正确:用堆分配或池化
    PER_IO_DATA* ioData = new PER_IO_DATA();  // 包含 OVERLAPPED
  2. 粘包/拆包问题更突出

    • 异步 I/O 可能一次收到不完整的数据包
    • 需要自己维护接收缓冲区,做包边界解析
  3. 工作线程数设置

    c 复制代码
    // 通常设为 CPU 核心数
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    int threadCount = si.dwNumberOfProcessors * 2;  // 或 * 1
  4. 优雅关闭

    • shutdown(s, SD_SEND) 发送 FIN
    • 等待对方关闭或超时后再 closesocket
  5. 避免内存泄漏

    • 每个 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 独有
内存拷贝 常规拷贝 零拷贝优化