You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 21:50:20