select和poll可实现单线程多端口监听:需将各监听socket设为非阻塞,加入读集合;select注意nfds为最大fd+1,poll无fd数量硬限制;每次调用前须重置集合或数组,避免状态残留。

用 select 或 poll 实现单线程多端口监听
一个进程监听多个 TCP 端口,最直接的方式不是开多个线程分别 bind+listen,而是复用一个事件循环——select 和 poll 是 POSIX 标准下最轻量、兼容性最好的选择。它们不依赖额外库,也不强制要求 C++11 以上。
常见错误是把每个 socket 都设成阻塞模式后直接 accept,结果第一个连接卡住,其余端口完全无法响应。必须先将所有监听 socket 设为非阻塞(fcntl(fd, F_SETFL, O_NONBLOCK)),再丢进 select 的读集合里。
-
select要注意nfds参数是最大 fd + 1,不是 socket 数量;超过 1024 fd 时部分系统会截断(Linux 默认可调,但别依赖) -
poll没有 fd 数量硬限制,结构体数组需手动管理,比select略繁琐但更清晰 - 每次调用前必须重置读集合(
FD_ZERO+FD_SET)或 poll 数组,否则上次就绪状态会残留
用 epoll 提升高并发下的多端口性能
Linux 下 epoll 是 select/poll 的替代方案,当监听端口数 > 10 且连接频次高时,性能差异明显——它避免了每次调用都遍历整个 fd 集合。
关键点在于:所有监听 socket 共享同一个 epoll_fd,每个新 socket 创建后调用 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) 注册 EPOLLIN 事件即可。触发时,epoll_wait 返回的是就绪的 fd 列表,而非全量集合。
立即学习“C++免费学习笔记(深入)”;
- 务必对每个监听 socket 单独调用
bind和listen,epoll不替你做网络层初始化 -
EPOLLET(边缘触发)模式下,必须循环accept直到返回EAGAIN,否则会漏掉新连接 - 如果某个端口只用于 UDP,注册时 event.events 应设为
EPOLLIN | EPOLLET,并用recvfrom而非accept
用 std::thread 分端口独立监听的适用场景
不是所有情况都要共享事件循环。若几个端口语义完全不同(比如一个 HTTP 管理端口 + 一个二进制协议数据端口 + 一个本地 Unix domain socket),且各自逻辑复杂、第三方库绑定死线程模型(如某些 HTTP 库强制单线程 run loop),分线程反而更清晰。
这时每个线程做自己的 socket → bind → listen → accept 循环,用 std::thread 启动,通过 std::atomic<bool></bool> 控制退出即可。但要注意资源隔离:
- 每个线程的 socket fd 必须独立创建,不能跨线程共享监听 fd(
accept是线程安全的,但行为未定义) - 避免在多个线程里同时
close同一个 fd;关闭前确保所有线程已退出该 socket 的处理逻辑 - 若需跨端口共享状态(如连接总数统计),用
std::mutex保护,不要用全局变量裸读写
端口复用与地址冲突的典型报错
启动时报 Address already in use,90% 是上一次程序异常退出后 socket 处于 TIME_WAIT 状态,或另一个进程占用了端口。解决方法不是等 60 秒,而是主动设置 socket 选项:
在 bind 前调用:
int opt = 1;<br>setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
SO_REUSEADDR允许立即重用处于TIME_WAIT的地址,对监听 socket 安全有效 -
SO_REUSEPORT(Linux 3.9+、FreeBSD)允许多个 socket 绑定同一端口,常用于多进程负载均衡,单进程多线程中一般不用 - UDP 场景下若要接收广播包,还需加
setsockopt(..., IPPROTO_IP, IP_MULTICAST_LOOP, ...)
真正容易被忽略的是:不同端口绑定时,sockaddr_in 的 sin_addr.s_addr 若填 INADDR_ANY,表示监听所有本地 IP;若指定某个 IP,则只响应该接口的请求——这在多网卡服务器上会导致“明明端口开着却连不上”的问题。


















