不能直接用select或poll写高并发隧道代理,因其连接数增长时性能线性下降:select有1024 fd硬限制且需全量拷贝与O(n)遍历,poll虽无数量上限但仍需传入全部fd数组并执行O(n)扫描;万级并发必须选用epoll(Linux)或kqueue(macOS/BSD),配合ET模式、非阻塞IO、SO_REUSEPORT及合理线程模型。

为什么不能直接用 select 或 poll 写高并发隧道代理
因为它们在连接数增长时性能线性下降,内核遍历 fd 集合的开销不可忽视;select 还有 1024 fd 限制,poll 虽无硬上限但每次调用仍需传入全部待查 fd 数组。真正支撑万级并发的隧道代理(比如中转 WebSocket、TCP、UDP 流量)必须用 epoll(Linux)或 kqueue(macOS/BSD),且要避免单线程瓶颈——多路复用不等于单线程复用。
实操建议:
- Linux 下优先用
epoll的边缘触发(EPOLLET)模式,配合非阻塞 socket,减少重复通知 - 每个 worker 线程独占一个
epoll_fd,用SO_REUSEPORT让内核分发新连接,避免 accept 竞争 - UDP 复用需单独处理:TCP 用连接上下文,UDP 用
sockaddr_storage+ 端口哈希做 session key,不能混进同一 event loop - 别在
epoll_wait回调里做协议解析——把数据拷贝到 worker 线程队列,解耦 I/O 和业务逻辑
怎么让一个 socket 同时支持 HTTP CONNECT、SOCKS5 和原始 TCP 透传
关键不是“识别协议”,而是“延迟协商”:收到首包后,根据前几个字节特征判断类型,再切换状态机。HTTP CONNECT 以 CONNECT 开头,SOCKS5 初始包是版本+方法数+方法列表,纯 TCP 则无固定头部——所以不能一上来就按某协议解析,得保留缓冲区并设超时(比如 500ms)。
实操建议:
- 为每个新连接分配固定大小的前置缓冲区(如 1024 字节),
recv到数据先存这里 - 写一个
detect_protocol(const uint8_t* buf, size_t len)函数,只检查前 16 字节,返回枚举值(PROTO_HTTP/PROTO_SOCKS5/PROTO_RAW) - SOCKS5 的
USERNAME/PASSWORD认证必须走独立子状态机,不能和握手混在一起;HTTP CONNECT 的 Host 头解析要防 CRLF 注入 - 检测失败时,直接关闭连接——别尝试“兼容模式”,隧道代理的语义必须明确,模糊处理会引发中间件拦截或 TLS 握手失败
UDP over TCP 或 UDP 多路复用怎么避免乱序和丢包放大
UDP 本身无序、不可靠,但隧道里封装成 TCP 流后,上层应用(比如 DNS 查询、QUIC 初始包)对顺序和丢包极其敏感。常见错误是把多个 UDP 包拼成一个 TCP 段发送,导致接收端无法拆包;或者用简单长度前缀但没处理粘包/半包。
实操建议:
- UDP 封装格式必须带长度字段(2 字节大端),且每个 UDP 数据报独立成帧,禁止跨帧合并
- 接收端用循环读取:先读 2 字节长度,再按长度读 payload,不足则缓存等待,超出则截断并告警
- 如果走 UDP 复用(如 QUIC-style connection ID),每个 client IP:port + server port 组成唯一 key,映射到独立接收缓冲区,防止不同 client 的包互相污染
- 别给 UDP 隧道加重传——那是上层协议的事;你只负责“尽力投递”,丢包由真实 UDP 层或 QUIC 自己处理
std::shared_ptr 和 std::unique_ptr 在连接生命周期管理中怎么选
连接对象(比如 TcpSession)必然跨线程:I/O 线程收包、worker 线程解密、转发线程写远端。用裸指针或全局池极易 double-free 或 use-after-free;但滥用 std::shared_ptr 会导致原子计数器竞争,万级连接下性能明显下降。
实操建议:
- I/O 线程内创建连接时用
std::make_unique,所有权立即移交到其所属的 event loop 对象(如() EpollLoop的std::vector<:unique_ptr>>) - 需要跨线程传递数据时,只传递 const 引用或拷贝必要字段(如
session_id,remote_addr),而非整个shared_ptr - 仅在回调闭包中需要延长生命周期时才用
shared_ptr,例如 timer 回调要访问 session,此时用auto self = shared_from_this(),但必须确保该 session 已被 loop 持有 - 所有析构函数里显式关闭 socket fd,并检查
close()返回值是否为 -1 且errno == EINTR——要重试
真正难的不是实现多协议或多路复用,而是当 HTTP CONNECT 流量里夹着 TLS 1.3 early data、SOCKS5 请求连着 UDP 关联包、同时还有 2000 个 idle UDP session 定时心跳时,状态机之间不互相干扰,fd 不泄漏,内存不碎片化——这些细节不会出现在任何框架文档里,只能靠日志、perf record 和反复压测暴露。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











