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

C++ Boost Websocket服务端关闭问题及tcp::acceptor accept()超时咨询

更新

经排查我发现该问题需要采用异步实现方案处理,待测试完成后我会更新新的解决方向

原始问题

我正在开发一款多服务器应用,可实现多机器间的信息收集、共享与请求,部分场景下机器A向机器B请求信息后需发送给机器C,再由C向A返回响应。暂不深入介绍应用业务逻辑,我需要针对客户端开发问题寻求帮助。

我设计的客户端采用双线程架构,基于Boost官方同步Websocket服务器示例开发,精简版代码如下:

#include "Poco/Clock.h"
#include "Poco/Task.h"
#include "Poco/Thread.h"
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <jsoncons/json.hpp>

namespace beast     = boost::beast;     // from <boost/beast.hpp>
namespace http      = beast::http;      // from <boost/beast/http.hpp>
namespace websocket = beast::websocket; // from <boost/beast/websocket.hpp>
namespace net       = boost::asio;      // from <boost/asio.hpp>
using tcp           = net::ip::tcp;     // from <boost/asio/ip/tcp.hpp>

class ResponseChannel : public Poco::Runnable {
    void do_session(tcp::socket socket)
    {
        try {
            websocket::stream<tcp::socket> ws{std::move(socket)};
            ws.set_option(websocket::stream_base::decorator(
                [](websocket::response_type& res) {
                    res.set(http::field::server,
                            std::string(BOOST_BEAST_VERSION_STRING) +
                                " websocket-server-sync");
                }));

            ws.accept();

            for (;;) {
                beast::flat_buffer buffer;
                ws.read(buffer);

                if (ws.got_binary()) {
                    // do something
                }
            }
        } catch (beast::system_error const& se) {
            if (se.code() != websocket::error::closed) {
                std::cerr << "do_session1 ->: " << se.code().message()
                          << std::endl;
                return;
            }
        } catch (std::exception const& e) {
            std::cerr << "do_session2 ->: " << e.what() << std::endl;
            return;
        }
    }

    virtual void run()
    {
        auto const address = net::ip::make_address(host);
        auto const port    = static_cast<unsigned short>(respPort);

        try {
            net::io_context ioc{1};
            tcp::acceptor   acceptor{ioc, {address, port}};
            tcp::socket     socket{ioc};

            for (; keep_running;) {
                acceptor.accept(socket);

                std::thread(&ResponseChannel::do_session, this,
                            std::move(socket))
                    .detach();
            }
        } catch (const std::exception& e) {
            std::cout << "run: " << e.what() << std::endl;
        }
    }

    void _terminate() { keep_running = false; }

  public:
    std::string                host;
    int                        respPort;
    bool                       keep_running  = true;
    int                        responseCount = 0;
    std::vector<long long int> latency_times;
    long long int              time_sum;
    Poco::Clock*               responseClock;
};

int main()
{
    using namespace std::chrono_literals;
    Poco::Clock     clock = Poco::Clock();
    Poco::Thread    response_thread;
    ResponseChannel response_channel;

    response_channel.responseClock = &clock;
    response_channel.host          = "0.0.0.0";
    response_channel.respPort      = 8080;
    response_thread.start(response_channel);
    response_thread.setPriority(Poco::Thread::Priority::PRIO_HIGH);

    // doing some work here. work will vary depending on command-line arguments
    std::this_thread::sleep_for(30s);
    response_channel.keep_running = false;
    response_thread.join();
}

多机器交互逻辑运行正常,可正常向机器B发送指令并接收机器C返回的结果。

目前遇到的问题是关闭承载本地响应通道的线程2:我在Poco::Thread和Poco::Task之间多次选型后,决定不使用Task,避免误从主线程关闭第二个线程/任务,我需要确认所有报文接收完成后再关闭第二个线程。

因此我需要在收到机器C返回的websocket::error::closed标志后再关闭相关资源,已分离的Websocket处理线程可随关闭信号自动退出,不存在问题。

但在套接字关闭后的重连循环逻辑中,线程会阻塞等待新连接:

acceptor.accept(socket);

该方法是阻塞调用,查阅官方文档未提供超时配置,我尝试调用close方法会抛出异常,额外增加了不必要的复杂度。

最终我希望服务器可持续循环处理机器B和机器C的系列连接,仅在客户端应用结束后停止运行,我在等待Poco::Thread执行完成前最后一步设置了运行标志,通知Websocket服务器停止运行。

我将该标志放在了阻塞accept()调用前,仅当标志置位时刚好有新连接建立再关闭的场景下才能生效,执行逻辑回到循环后才会检查标志位。

理想情况下希望accept()可设置超时,调用超时后回到循环,支持周期性检查运行标志判断是否继续保持线程运行。

请问是否有开发者遇到过同类问题,应当如何解决?


解决方案

可通过以下几种方案解决阻塞accept无法优雅退出的问题,你可以根据现有架构的改动成本选择:

  • 方案1:采用非阻塞模式实现accept超时检查
    改动量最小,不需要调整现有同步架构,只需要给acceptor开启非阻塞模式,每次accept失败后休眠短时间再检查运行标志即可,示例修改后的run方法循环逻辑如下:
    // 先开启acceptor非阻塞模式
    acceptor.non_blocking(true);
    for (; keep_running;) {
        boost::system::error_code ec;
        acceptor.accept(socket, ec);
        if (!ec) {
            std::thread(&ResponseChannel::do_session, this, std::move(socket)).detach();
            socket = tcp::socket(ioc); // 重置socket供下一次accept使用
        } else if (ec != boost::asio::error::would_block) {
            // 处理非预期错误,跳出循环
            std::cerr << "accept error: " << ec.message() << std::endl;
            break;
        }
        // 100ms检查一次运行标志,可根据需求调整间隔
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    
  • 方案2:主动取消accept操作
    不需要修改accept的阻塞模式,在_terminate方法中除了设置keep_running = false,额外调用acceptor.cancel()即可打断正在阻塞的accept调用,被打断的accept会抛出operation_aborted错误,你捕获该错误后直接退出循环即可。该方案不需要额外的轮询开销,响应更及时。
  • 方案3:切换为异步accept实现
    长期来看异步架构更适合你的多连接场景,Boost.Beast也提供了成熟的异步示例,io_context本身支持停止操作,当需要退出时调用ioc.stop()即可立刻终止所有待执行的异步操作,配合线程 join 可实现完全优雅的退出。

注意:需要将keep_running变量修改为std::atomic<bool>类型,避免多线程读写的可见性问题,确保主线程修改的标志可以立刻被服务线程感知。

内容的提问来源于stack exchange,提问作者Michael

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 20:27:03