基于Boost ASIO:同类同io_context下同步异步TCP操作实现问询
我希望使用Boost ASIO编写一个TCPClient类,实现TCP连接创建、数据收发功能,支持同步(带超时阻塞)和异步两种调用方式,且用户无需接触任何ASIO相关内容(包括创建io_context对象)。
当前类的大致实现如下(不含收发方法):
class TCPClient { public: TCPClient() { deadline_.expires_from_now(std::chrono::duration<int>::max()); check_deadline(); } // --- Callbacks --- using ConnectCallback = std::function<void()>; // Synchronous and asynchronous auto connect(const std::string& host, int port, int timeout=0) -> void; auto connect_async(const std::string& host, int port, ConnectCallback callback) -> void; auto close() -> void { if(socket_) { boost::system::error_code ignored_ec; socket_->close(ignored_ec); } } private: /** * Adapted from Boost ASIO timeout example * Replaced deadline_timer with steady_timer to make it compatible with std::chrono */ auto check_deadline() -> void { // Close socket if the timer has expired if(deadline_.expires_at() <= std::chrono::steady_clock::now()) { close(); deadline_.expires_from_now(std::chrono::duration<int>::max()); } deadline_.async_wait( [this](const auto& ec) { check_deadline(); }); } auto socket() -> tcp::socket& { // Create socket if it doesn't exist if(!socket_) { socket_ = std::make_unique<tcp::socket>(io_); } // Open interface if(!socket_->is_open()) { socket_->open(tcp::v4()); } return *socket_; } boost::asio::io_context io_; std::unique_ptr<tcp::socket> socket_; boost::asio::steady_timer deadline_; };
借助ASIO的deadline机制,我已实现了同步阻塞的connect方法:
auto TCPClient::connect(const std::string& host, int port, int timeout) -> void { // Resolve remote endpoint auto query = tcp::resolver::query(host, std::to_string(port)); auto iter = tcp::resolver(io_).resolve(query); // setup deadline if(timeout) deadline_.expires_from_now(std::chrono::seconds(timeout)); // start asynchronous connection boost::system::error_code ec = net::error::would_block; // Never a valid error code if async operation fails socket().async_connect( iter->endpoint(), [&ec](const auto& e) { ec = e; }); do { io_.run_one(); } while(ec == net::error::would_block); if(ec || !socket().is_open()) { throw SocketException( get_error_message(ec) ); } }
可以看到,同步方法底层使用异步操作,通过反复调用io_context.run_one()等待完成。现咨询:实现connect_async方法的最佳方案是什么?我曾考虑每次调用异步操作时启动线程执行io_context.run(),但未成功且担心同步异步方法混用会引发问题;是否需要拆分阻塞/非阻塞操作的类?由于所有通信需共用同一个socket,无法使用独立的io_context对象。
核心思路:用后台线程持续托管io_context
你的同步方法依赖run_one()手动驱动io_context,而异步操作需要io_context持续运行来处理事件回调。最佳方案是在TCPClient构造时启动一个后台线程,专门负责运行io_context.run(),这样同步和异步操作能共享同一个io_context,无需重复启停线程,也不会出现混用冲突。
具体实现步骤
添加线程成员与生命周期管理
在类中添加后台线程变量,构造时启动线程运行io_.run(),析构时停止io_context并等待线程退出,避免资源泄漏。实现connect_async方法
直接发起异步连接操作,在回调中处理连接结果(包括错误),无需手动管理io_context的运行。同时复用已有的deadline机制,支持异步连接的超时控制。保证操作的串行性
由于Boost ASIO的socket对象本身不是线程安全的,需确保同一时间只有一个连接/收发操作在执行。可以通过互斥锁保护socket的访问,避免并发调用同步/异步方法导致的未定义行为。
修改后的完整代码示例
#include <boost/asio.hpp> #include <functional> #include <memory> #include <thread> #include <mutex> #include <stdexcept> using namespace boost::asio; using tcp = boost::asio::ip::tcp; class SocketException : public std::runtime_error { public: using std::runtime_error::runtime_error; }; std::string get_error_message(const boost::system::error_code& ec) { return ec.message(); } class TCPClient { public: // 优化回调,携带错误信息方便用户处理失败场景 using ConnectCallback = std::function<void(const boost::system::error_code&)>; TCPClient() : deadline_(io_) { deadline_.expires_from_now(std::chrono::duration<int>::max()); check_deadline(); // 启动后台线程托管io_context work_ = std::make_unique<io_context::work>(io_); thread_ = std::thread([this]() { io_.run(); }); } ~TCPClient() { // 停止io_context并等待线程退出 work_.reset(); // 让io_context的run()在所有操作完成后退出 io_.stop(); if(thread_.joinable()) { thread_.join(); } close(); } // 同步连接(带超时) auto connect(const std::string& host, int port, int timeout=0) -> void { std::lock_guard<std::mutex> lock(socket_mutex_); close(); // 先关闭已有连接 tcp::resolver resolver(io_); auto endpoints = resolver.resolve(host, std::to_string(port)); // 设置超时 if(timeout > 0) { deadline_.expires_from_now(std::chrono::seconds(timeout)); } else { deadline_.expires_from_now(std::chrono::duration<int>::max()); } boost::system::error_code ec = boost::asio::error::would_block; socket().async_connect(endpoints, [&ec](const auto& e) { ec = e; }); do { io_.run_one(); } while(ec == boost::asio::error::would_block); if(ec || !socket().is_open()) { close(); throw SocketException(get_error_message(ec)); } } // 异步连接 auto connect_async(const std::string& host, int port, ConnectCallback callback) -> void { std::lock_guard<std::mutex> lock(socket_mutex_); close(); // 先关闭已有连接 tcp::resolver resolver(io_); resolver.async_resolve(host, std::to_string(port), [this, callback=std::move(callback)](const boost::system::error_code& ec, tcp::resolver::results_type endpoints) { if(ec) { callback(ec); return; } // 发起异步连接 socket().async_connect(endpoints, [this, callback=std::move(callback)](const boost::system::error_code& ec) { if(ec || !socket().is_open()) { close(); } callback(ec); }); }); } auto close() -> void { std::lock_guard<std::mutex> lock(socket_mutex_); if(socket_) { boost::system::error_code ignored_ec; socket_->close(ignored_ec); socket_.reset(); } } private: auto check_deadline() -> void { if(deadline_.expires_at() <= std::chrono::steady_clock::now()) { close(); deadline_.expires_from_now(std::chrono::duration<int>::max()); } deadline_.async_wait([this](const auto& ec) { if(!ec) { // 忽略取消错误 check_deadline(); } }); } auto socket() -> tcp::socket& { if(!socket_) { socket_ = std::make_unique<tcp::socket>(io_); } if(!socket_->is_open()) { socket_->open(tcp::v4()); } return *socket_; } io_context io_; std::unique_ptr<io_context::work> work_; // 防止io_context.run()无任务时退出 std::thread thread_; std::unique_ptr<tcp::socket> socket_; steady_timer deadline_; std::mutex socket_mutex_; // 保护socket的并发访问 };
关键说明
- io_context托管:通过
io_context::work保证后台线程不会因为暂时没有异步任务而退出,确保随时能处理新的异步操作。 - 同步异步兼容性:同步方法依然用
run_one()驱动,而异步方法的回调会由后台线程自动处理,两者共享同一个io_context,不会冲突。 - 线程安全:用
std::mutex保护socket的所有操作,避免并发调用同步/异步方法导致的未定义行为。 - 无需拆分类:通过上述设计,同一个TCPClient类可以同时支持同步和异步操作,满足共用socket的需求。
内容的提问来源于stack exchange,提问作者Teppichboden2

