首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

Windows Socket 编程:select 模型详解与高并发服务器实例

     編輯:關於C語言

select 模型核心思想

select 允许单线程同时监控多个套接字的可读、可写、异常状态,实现I/O 多路复用。避免为每个连接创建线程,降低资源消耗,适合连接数适中(<1000)的场景。

函数原型与参数

int select(int nfds,           // Windows 忽略,保留兼容性
           fd_set *readfds,    // 关注可读的套接字集合
           fd_set *writefds,   // 关注可写的套接字集合
           fd_set *exceptfds,  // 关注异常的套接字集合
           const struct timeval *timeout); // 超时时间,NULL=永久阻塞

返回值:就绪套接字总数;SOCKET_ERROR 表示出错,需 WSAGetLastError()。

fd_set 宏操作

宏作用
FD_ZERO(&set)清空集合
FD_SET(s, &set)将套接字 s 加入集合
FD_CLR(s, &set)从集合移除 s
FD_ISSET(s, &set)测试 s 是否在集合中(select 返回后调用)

Windows 默认 FD_SETSIZE=64,可在包含 winsock2.h 前定义更大值(≤1024)。

可读/可写/异常的语义

  • 可读:有数据到达、连接关闭/复位、监听套接字有新连接(accept 不阻塞)。
  • 可写:发送缓冲区有空间、非阻塞连接建立完成。
  • 异常:带外数据到达、非阻塞连接失败。

注意:可写事件通常持续就绪,若持续监控会导致 select 频繁返回,建议仅在需要发送数据时加入 writefds。

select 事件循环标准流程

  1. FD_ZERO 初始化关注集合。
  2. FD_SET 将监听套接字及所有已连接套接字加入 readfds。
  3. 调用 select 阻塞等待。
  4. 返回后,遍历 readfds(用 FD_ISSET 判断):
    • 监听套接字就绪 → accept 新连接,加入集合。
    • 普通套接字就绪 → recv 读取数据;若返回 0 或错误 → 关闭套接字,从集合移除。
  5. 如有数据需回发,将该套接字加入 writefds,下轮 select 监控可写;发送完成后移出 writefds。
  6. 循环回步骤 1。

完整 Echo 服务器示例(Windows)

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <map>
#pragma comment(lib, "ws2_32.lib")

int main() {
    WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa);
    SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in addr = {}; addr.sin_family = AF_INET; addr.sin_port = htons(5000); addr.sin_addr.s_addr = INADDR_ANY;
    bind(listen_sock, (sockaddr*)&addr, sizeof(addr));
    listen(listen_sock, SOMAXCONN);

    fd_set read_set; std::map clients; // 简化:不缓存未发完数据
    while (true) {
        FD_ZERO(&read_set);
        FD_SET(listen_sock, &read_set);
        for (auto &kv : clients) FD_SET(kv.first, &read_set);

        int ret = select(0, &read_set, NULL, NULL, NULL);
        if (ret == SOCKET_ERROR) break;

        if (FD_ISSET(listen_sock, &read_set)) {
            SOCKET client = accept(listen_sock, NULL, NULL);
            clients[client] = 0;
        }

        for (auto it = clients.begin(); it != clients.end(); ) {
            SOCKET s = it->first;
            if (!FD_ISSET(s, &read_set)) { ++it; continue; }
            char buf[1024];
            int len = recv(s, buf, sizeof(buf), 0);
            if (len <= 0) { closesocket(s); it = clients.erase(it); continue; }
            // 立即回显(实际应处理可写、缓存未发完数据)
            send(s, buf, len, 0);
            ++it;
        }
    }
    WSACleanup(); return 0;
}

select 的局限与替代方案

总结

select 是跨平台、入门级的 I/O 多路复用模型,适合连接数较少、逻辑简单的场景。掌握其事件循环范式(FD_ZERO/SET/ISSET + 循环)是网络编程基本功。高并发生产环境请迁移至 IOCP (Windows) 或 epoll (Linux)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
局限影响替代
FD_SETSIZE 限制(默认 64/1024)无法处理超大连接数IOCP (Windows)、epoll (Linux)、kqueue (BSD)
每次调用需全量拷贝 fd_set 到内核O(N) 开销,N 大时性能下降epoll/kqueue/IOCP 仅注册一次
无法区分“可读有数据”与“连接关闭”需额外 recv 判断epoll EPOLLRDHUP、IOCP 完成通知