boost::beast WebSocket服务器未响应客户端close()的问题解决
我有一个基于boost::beast的WebSocket服务器,负责向连接的客户端流式传输数据。由于不需要处理客户端发送的内容,我只持续调用async_write而未调用async_receive,但这导致服务器无法及时感知客户端的socket.close()请求。
因为未调用async_receive(),我不得不设置timeout.idle_timeout = none()——因为写操作不会重置空闲定时器,而默认30秒的handshake_timeout是合适的。相关C++代码如下:
void Session::Accept(Request&& req){ auto timeout = boost::beast::websocket::stream_base::timeout::suggested( boost::beast::role_type::server // 设置handshake_timeout = 30s ); timeout.idle_timeout = boost::beast::websocket::stream_base::none(); m_client.set_option( timeout ); // 执行accept相关操作 Write(); } void Session::Write(){ m_client.async_write( m_buffer.data(), boost::beast::bind_front_handler( &Session::OnWrite, shared_from_this() ) ); } void Session::OnWrite( const boost::beast::error_code ec, std::size_t bytes_transferred ){ if (ec){ std::cerr << "client disconnected with: " << ec.message() << std::endl; return; } PopulateBuffer(); Write(); }
当使用JavaScript客户端调用socket.close()时,socket.close()与socket.onclose触发之间存在长达30秒的延迟,这是我需要解决的问题。客户端代码如下:
socket = new WebSocket('ws://...'); socket.onopen = function() { ... } socket.onmessage = function(event) { if (...) socket.close() } socket.onclose = function(event) { console.log(event) }
最终触发socket.onclose时,退出码为1006(异常关闭——未收到关闭帧),这是客户端判定的结果,而非服务器发送的。
我的推测是:客户端调用socket.close()后向服务器发送了关闭请求,但由于服务器未调用async_read(),boost::beast的WebSocket无法处理该请求,持续发送数据直到握手超时(30秒)。超时后,客户端收到1006关闭原因,服务器的async_write()回调则出现“broken pipe”错误码。
我不想调用async_receive(),因为需要等待其完成才能再次调用async_write(),阻塞在无意义的读操作会中断数据流。如果有快速检测可读数据的方法,我可以采用,但不清楚可用的方法。请问最优解决方案是什么?
核心逻辑:WebSocket协议依赖读操作处理控制帧
boost::beast的WebSocket实现必须通过async_read(或同步read)来处理协议层面的控制帧(包括关闭帧)。如果不启动读操作,服务器永远无法感知客户端发送的关闭请求,只能等到底层TCP连接超时或出错断开,这就是你遇到30秒延迟的根本原因。
最优方案:同时启动异步读和异步写,非阻塞处理控制帧
你担心的“阻塞数据流”是误解——boost::beast的异步操作是非阻塞的,async_read和async_write可以同时运行,不会互相等待。即使不需要处理客户端的业务数据,也需要启动一个持续的异步读操作来监听控制帧,这样服务器能立即响应客户端的关闭请求,主动发送关闭响应,让客户端快速触发onclose。
修改后的代码示例:
// 新增成员变量:boost::beast::flat_buffer m_dummy_buffer; void Session::Accept(Request&& req){ auto timeout = boost::beast::websocket::stream_base::timeout::suggested( boost::beast::role_type::server ); // 恢复默认空闲超时,因为读操作会自动重置定时器 // timeout.idle_timeout = boost::beast::websocket::stream_base::none(); m_client.set_option(timeout); // 执行accept相关操作 // 同时启动读和写操作 Read(); Write(); } void Session::Read(){ m_client.async_read( m_dummy_buffer, // 用空缓冲区接收数据,无需处理 boost::beast::bind_front_handler( &Session::OnRead, shared_from_this() ) ); } void Session::OnRead( const boost::beast::error_code ec, std::size_t bytes_transferred ){ if (ec){ // 处理关闭或错误事件,比如客户端主动发送关闭帧 std::cerr << "read error: " << ec.message() << std::endl; // 主动发送正常关闭响应 m_client.async_close(boost::beast::websocket::close_code::normal, [self=shared_from_this()](boost::beast::error_code){}); return; } // 清空缓冲区并重启读操作,持续监听控制帧 m_dummy_buffer.consume(m_dummy_buffer.size()); Read(); } // Write和OnWrite保持原有逻辑不变 void Session::Write(){ m_client.async_write( m_buffer.data(), boost::beast::bind_front_handler( &Session::OnWrite, shared_from_this() ) ); } void Session::OnWrite( const boost::beast::error_code ec, std::size_t bytes_transferred ){ if (ec){ std::cerr << "client disconnected with: " << ec.message() << std::endl; return; } PopulateBuffer(); Write(); }
关键说明
- Dummy缓冲区的作用:用一个空的动态缓冲区接收客户端数据,读完直接清空并重启读操作,无需解析或存储业务数据,仅用于处理WebSocket控制帧。
- 恢复空闲超时:读操作会定期重置空闲定时器,无需设置
idle_timeout = none(),服务器仍能自动清理长时间无活动的连接。 - 主动关闭连接:当
async_read检测到客户端关闭帧时,服务器调用async_close发送正常关闭响应,客户端会收到1000(正常关闭)的退出码,而非1006。
替代方案:监听底层TCP可读事件(不推荐)
如果坚持不想启动WebSocket的async_read,可以直接监听底层socket的可读事件,一旦有数据可读,就主动关闭连接。但这种方式跳过了WebSocket协议处理,可能导致客户端收到异常关闭码,不符合协议规范,因此仅作为备选。
示例代码:
void Session::Accept(Request&& req){ // ... 现有代码 ... // 监听底层socket的可读事件 m_client.next_layer().async_wait( boost::asio::socket_base::wait_read, boost::beast::bind_front_handler( &Session::OnSocketReadable, shared_from_this() ) ); Write(); } void Session::OnSocketReadable(const boost::beast::error_code ec){ if (ec){ std::cerr << "socket error: " << ec.message() << std::endl; return; } // 检测到可读数据,直接关闭WebSocket连接 m_client.async_close(boost::beast::websocket::close_code::normal, [self=shared_from_this()](boost::beast::error_code){}); }
内容的提问来源于stack exchange,提问作者Stewart

