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

