select 不适用于高并发 TCP 服务,因其每次调用需全量拷贝 fd_set 并 O(n) 遍历,FD_SETSIZE 默认 1024 且无边缘触发,返回后集合被清空需重填,易出错;生产环境应使用 epoll_wait。

select 在高并发下不适用,它只适合教学或低负载场景;真要写生产级 TCP 服务,Linux 上必须换 epoll_wait。
为什么 select 不能用于高并发 TCP 服务
select 每次调用都要把整个 fd_set 从用户态拷贝到内核态,内核再线性遍历所有 fd 判断就绪状态——时间复杂度是 O(n)。连接数一过千,延迟就明显上升。
更实际的限制还有:
-
FD_SETSIZE默认为 1024,超出后FD_SET会越界写内存,行为未定义 -
select()返回后,原fd_set被清空,每次循环都得重填,容易漏加或重复加 - 没有边缘触发(ET)模式,只能水平触发(LT),频繁通知已就绪但未处理完的 fd
- Windows 下虽可用,但 Linux 生产环境几乎没人用它扛真实流量
select 的正确初始化和循环结构
如果非要用(比如教学、嵌入式或兼容老系统),必须严格按以下顺序组织主循环,否则会丢连接或卡死:
立即学习“C++免费学习笔记(深入)”;
- 每次进入
select()前,用FD_ZERO()清空三个集合 - 用
FD_SET()重新加入监听 socket(server_fd)和所有已连接 client fd -
nfds必须设为当前最大 fd + 1,不能硬写 1024 -
timeout不建议传NULL(永久阻塞),至少设个 1 秒超时,避免失控
典型错误:在 select() 返回后,直接对 readfds 遍历时没用 FD_ISSET() 检查,而是假设所有 fd 都就绪——这会导致对未就绪 fd 调用 recv(),返回 -1 并置 errno = EAGAIN 或 EWOULDBLOCK。
如何避免 bind 失败:SO_REUSEADDR 是必须的
服务端重启时,旧连接常处于 TIME_WAIT 状态,直接 bind() 会报错 Address already in use。这不是 bug,是 TCP 规范要求。
解决方法是在 socket() 之后、bind() 之前加这段:
int on = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
注意:SO_REUSEADDR 允许新 socket 绑定到处于 TIME_WAIT 的地址,但它不解决 TIME_WAIT 本身——只是让服务能快速重启。别误以为它是“万能端口复用开关”。
select 模型里 recv() 和 send() 仍会阻塞
很多人误以为用了 select() 就彻底告别阻塞了。其实不是:select() 只保证 recv() 不会因“没数据”而挂起,但若对方发来半包、粘包,或网络中断,recv() 仍可能返回 0(对端关闭)或 -1(错误)。send() 更危险:当发送缓冲区满时,它会阻塞(除非 socket 设为非阻塞)。
所以实际代码中必须:
- 对每个 client fd 调用
recv()后,检查返回值:== 0表示对端关闭,应close()并从集合中移除;且 <code>errno == EAGAIN表示暂时无数据,跳过 - 对
send()建议设非阻塞:fcntl(client_fd, F_SETFL, O_NONBLOCK),否则一次发不出全部数据就会卡住整个事件循环 - 不要在
select()循环里做耗时操作(如文件 I/O、sleep、复杂计算),否则所有客户端都会被拖慢
真正的难点不在怎么写通,而在怎么写稳:连接异常断开、缓冲区溢出、fd 泄漏、select() 返回后集合状态错乱——这些细节比语法更消耗调试时间。



















