纯send()会导致客户端消息堆积甚至断连,因其忽略tcp背压,当客户端接收慢时,服务端持续发送会使socket发送队列溢出,触发连接重置;uwebsockets的res->write()和res->end()通过内置环形缓冲区与onwritable回调天然支持背压;boost.beast需手动检查is_open()及async_write错误码来实现背压;websocketpp应避免send_to_all(),改用可写连接池管理。

为什么纯 send() 会导致客户端消息堆积甚至断连?
WebSocket 广播不是“发完就了事”。当服务端以固定速率高频推送(比如行情 tick、实时日志),而某个客户端网络慢、UI 卡顿或解析逻辑阻塞时,它的接收缓冲区会持续积压。ASIO 底层会把待发数据缓存在 socket 发送队列里,一旦超过系统限制或对方 TCP window 关闭,async_write 就会挂起甚至失败——你却可能还在拼命 send(),最终触发背压崩溃或连接被对端重置。
uWebSockets 的 res->write() 和 res->end() 如何天然支持背压?
uWS 的设计从底层规避了手动管理写状态的麻烦。它把每个响应对象 res 绑定到单次 HTTP/WS 生命周期,并内置了完整的背压链路:
-
res->write()是非阻塞的:如果底层 TCP 缓冲区满,它自动暂停写入,把数据暂存在内部环形缓冲区,并注册onWritable回调 -
res->end()会等待所有已写数据真正 flush 完才关闭连接 - 你不需要轮询、不需检查
is_writable(),只要在onWritable里继续调用write()即可形成闭环
示例关键片段:
app.ws<SSL>("/stream", {
.open = [](auto *ws) {
// 启动广播循环
ws->publish("topic", "init");
},
.message = [](auto *ws, std::string_view message, uWS::OpCode op) {
// 收到控制指令
},
.writable = [](auto *ws) {
// 背压缓解:可以安全追加新消息了
ws->send("next_chunk", uWS::OpCode::TEXT, true);
}
});
用 Boost.Beast 手动实现背压时,必须检查 is_open() 和 is_closed() 吗?
必须。Beast 不像 uWS 那样封装写状态,你需要显式跟踪每个连接的发送能力:
立即学习“C++免费学习笔记(深入)”;
-
ws.is_open()只表示握手完成,不代表能发数据 - 真正关键的是
ws.next_layer().is_open()+ 检查async_write的error_code - 一旦
ec == boost::asio::error::would_block或ec == boost::beast::error::timeout,说明必须暂停广播,等async_write下次回调成功后再恢复 - 漏掉这个判断,广播线程会持续往已满的 socket 写,导致
std::bad_alloc或连接静默断开
广播时用 websocketpp 的 send() 还是 send_to_all()?
别用 send_to_all()。它本质是遍历所有连接并逐个调用 send(),但完全不感知各连接当前的写就绪状态——等于把背压问题外包给操作系统,极易引发雪崩。
正确做法是维护一个「可写连接池」:
- 在
set_message_handler里收到 ping/pong 后,标记该连接为“可写” - 广播前只向池中连接发消息;若某次
send(hdl, ...)返回pass_through错误,立即将其移出池 - 用
set_http_handler拦截 upgrade 请求,初始化连接状态
核心陷阱:websocketpp::frame::opcode::text 必须与实际 payload 类型严格一致,否则某些客户端(如 Safari)会直接关闭连接,且不报错。
背压不是加个队列就能解决的事——它要求你在协议层、传输层、应用层三者之间做精细的状态同步。最容易被忽略的,是客户端主动关闭连接时,服务端没及时清理写队列,导致后续广播仍往已失效句柄写,引发未定义行为。










