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

为什么直接用 socket + select 最稳妥
因为端口转发本质是双向数据搬运,不涉及协议解析,不需要用 libevent 或 boost.asio 这类重型库——它们反而会掩盖连接关闭、缓冲区满、EAGAIN 等底层细节,导致转发卡死或丢包。用原生 socket 配合 select 能精确控制每个 socket 的读写状态,也方便调试。
常见错误现象:select 返回可读但 recv 返回 0(对端关闭),此时不主动 close 对应 socket,就会让另一侧持续发包失败;或者没清空 fd_set 就重复调用 select,导致监听失效。
- 服务端 socket 必须设为
SOL_SOCKET, SO_REUSEADDR,否则重启时报Address already in use -
select的 timeout 建议设为 1 秒(tv_sec=1, tv_usec=0),避免无限阻塞又不过度轮询 - 每次
select返回后,必须用FD_ISSET检查每个 fd,不能假设顺序或只查一个
如何处理客户端连接与目标服务器连接的生命周期
一个客户端连接对应一个目标服务器连接,两者需严格配对。不能在主线程里阻塞 connect,否则整个转发器卡住;也不能在子线程里管理 socket,因为 select 不跨线程共享 fd_set。
正确做法是:主线程 accept 后,立即 setsockopt(fd, SOCK_NONBLOCK, ...) 设为非阻塞,然后调用 connect —— 它会立刻返回 -1 并置 errno = EINPROGRESS;之后把该 fd 加入 writefds,等 select 通知“可写”时,再用 getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) 检查是否连成功。
- 若
SO_ERROR == 0,说明连接建立,可以开始双向转发 - 若
SO_ERROR != 0,说明连接失败(如目标端口关了),应关闭该 fd 并清理对应关系 - 客户端断开时,必须同时关闭服务端 socket 和目标 socket,否则残留连接会占资源
数据转发时怎么避免粘包和死锁
TCP 是字节流,不是消息边界。转发器不能简单 “recv 一次就 send 一次”,否则小包会被合并或拆分,导致应用层协议错乱(比如 HTTP header 被截断)。但也不能盲目循环 recv,因为对方可能只发了半包就停了,你一直 recv 会阻塞。
解法是:每次 select 通知可读,就用 recv(fd, buf, sizeof(buf)-1, MSG_DONTWAIT) 尝试读,检查返回值:>0 正常转发,==0 关连接, 且 <code>errno == EAGAIN || errno == EWOULDBLOCK 就停止读,等下次通知。
- 发送侧同理:用
send(fd, buf, len, MSG_NOSIGNAL),返回值小于len时,需缓存剩余数据,下次可写时继续发 - 必须为每对连接维护独立的读/写缓冲区(比如两个
std::vector<uint8_t></uint8_t>),不能共用全局 buf - 别用
send(..., MSG_WAITALL)—— 它在转发场景下极易导致死锁
编译和运行时容易忽略的三个点
很多人写完代码能编译,但一跑就 Connection refused 或 Bad file descriptor,问题往往不在逻辑,而在环境或权限。
- Linux 下绑定低端口(
- 目标地址写成
"127.0.0.1"时,确保目标服务确实在本地监听(netstat -tlnp | grep :xxx),而不是只绑了::1 - g++ 编译要加
-lpthread(即使没显式用线程,某些系统调用内部依赖它),否则链接时报 undefined reference topthread_mutex_lock
复杂点在于:连接抖动时,select 可能频繁触发但实际无数据;这时得加简单计数防忙等,或者改用 epoll。但对“简单”工具来说,先跑通比追求高并发更重要。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











