基于Boost Beast与协程的HTTP客户端问题:co_await响应异常
Boost Beast + C++20协程异步HTTP客户端问题解答
首次尝试基于Boost Beast和C++20协程实现异步HTTP客户端,编译通过但调试发现未进入task_main_GET或do_session协程。参考Stack Overflow示例并遵循@sehe建议未使用自定义协程,但对Asio用法不熟悉,同时有以下疑问:
- 如何通过executor调用
co_spawn?目前仅能传入io_context使其运行,但这并非推荐用法; - 不理解阻塞式
run()函数的意义,希望实现可通过co_await获取响应的异步HTTP客户端,但run()是阻塞的该如何处理?希望应用逻辑无需回调即可链式调用。
原代码
#include "openssl/conf.h" #include <boost/beast/core.hpp> #include <boost/beast/http.hpp> #include <boost/beast/ssl.hpp> #include <boost/beast/version.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/awaitable.hpp> #include <boost/asio/experimental/promise.hpp> #include <boost/asio/experimental/use_promise.hpp> #include <boost/asio/as_tuple.hpp> #include <boost/asio/detached.hpp> #include <cstdlib> #include <functional> #include <iostream> #include <string> namespace this_coro = boost::asio::this_coro; #include <boost/certify/extensions.hpp> #include <boost/certify/https_verification.hpp> #include <syncstream> namespace beast = boost::beast; namespace http = beast::http; namespace net = boost::asio; namespace ssl = boost::asio::ssl; using tcp = boost::asio::ip::tcp; using boost::asio::use_awaitable; //------------------------------------------------------------------------------ #define use_void #ifdef use_void using T = void; #else using T = std::string; #endif boost::asio::awaitable<T> do_session( std::string host, std::string port, std::string target, ssl::context& ctx) { beast::error_code ec; auto ex = co_await net::this_coro::executor; tcp::resolver resolver(ex); beast::ssl_stream<beast::tcp_stream> stream(ex, ctx); if(! SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { ec.assign(static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()); std::cerr << ec.message() << "\n"; co_return; } try{ auto const results = co_await resolver.async_resolve(host, port, use_awaitable); beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); co_await beast::get_lowest_layer(stream).async_connect(results, use_awaitable); beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); co_await stream.async_handshake(ssl::stream_base::client, use_awaitable); http::request<http::string_body> req{http::verb::get, target, 11}; req.set(http::field::host, host); req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); co_await http::async_write(stream, req, use_awaitable); beast::flat_buffer b; http::response<http::dynamic_body> res; auto [ec, bytes] = co_await http::async_read(stream, b, res, boost::asio::as_tuple(use_awaitable)); std::cout << res << std::endl; beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); if(ec == net::error::eof) { if(ec != net::error::eof) throw beast::system_error(ec); } else { std::cout << res << std::endl; co_await stream.async_shutdown(use_awaitable); } } catch(beast::system_error const& se) { throw; } } //------------------------------------------------------------------------------ boost::asio::awaitable<T> task_main_GET(net::io_context &ioc) { auto const host = "https://microsoftedge.github.io/Demos/json-dummy-data/64KB.json"; auto const port = "443"; auto const target = "/"; std::osyncstream(std::cout) << "GET " << host << ":" << port << target << std::endl; ssl::context ctx{ssl::context::tlsv12_client}; ctx.set_verify_mode(ssl::context::verify_peer ); boost::certify::enable_native_https_server_verification(ctx); auto ex = this_coro::executor; auto task = boost::asio::co_spawn( ioc, [&]() mutable -> boost::asio::awaitable<std::string> { co_await do_session(host, port, target, ctx); }, use_awaitable ); #ifdef use_void co_return; #else std::string responseText = co_await task; std::osyncstream(std::cout) << "response:\n" << responseText << std::endl; co_return responseText ; #endif } int main() { net::io_context ioc; auto task = boost::asio::co_spawn(ioc, task_main_GET(ioc), use_awaitable); ioc.run(); return EXIT_SUCCESS; }
原CMakeLists.txt
cmake_minimum_required(VERSION 3.5) project(boost_beast_example LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (MSVC) add_compile_options(/bigobj) else () add_compile_options(-Wa,-mbig-obj) endif () set(OPENSSL_ROOT_DIR "C:/OpenSSL-Win64") set(OPENSSL_INCLUDE_DIR "C:/OpenSSL-Win64/include") find_package(OpenSSL REQUIRED) link_directories("C:/OpenSSL-Win64/") include_directories(${OPENSSL_INCLUDE_DIR}) message("OPENSSL_FOUND: " ${OPENSSL_FOUND}) message("OPENSSL_INCLUDE_DIR: " ${OPENSSL_INCLUDE_DIR}) message("OPENSSL_CRYPTO_LIBRARY: " ${OPENSSL_CRYPTO_LIBRARY}) message("OPENSSL_SSL_LIBRARY: " ${OPENSSL_SSL_LIBRARY}) set(Boost_DEBUG 1) SET(CMAKE_INCLUDE_PATH ${CMAKE_INCLUDE_PATH} "G:/SoftwareDev/libs/boost") SET(CMAKE_LIBRARY_PATH ${CMAKE_LIBRARY_PATH} "G:/SoftwareDev/libs/boost/bin/x64/lib/cmake") find_package(Boost REQUIRED context system coroutine regex PATHS "G:/SoftwareDev/libs/boost/bin/x64/lib/cmake") include_directories("G:/SoftwareDev/libs/boost/libs/beast") include_directories(${Boost_INCLUDE_DIRS}) message("Boost_FOUND: " ${Boost_FOUND}) message("Boost_INCLUDE_DIRS: " ${Boost_INCLUDE_DIRS}) message("Boost_LIBRARY_DIRS: " ${Boost_LIBRARY_DIRS}) message("Boost_LIBRARIES: " ${Boost_LIBRARIES}) message("Boost_CONTEXT_LIBRARY: " ${Boost_CONTEXT_LIBRARY}) message("Boost_SYSTEM_LIBRARY: " ${Boost_SYSTEM_LIBRARY}) find_package(Threads REQUIRED) add_executable(${PROJECT_NAME} main.cpp) include_directories(libs/certify/include) if(MSVC) target_link_libraries(${PROJECT_NAME} Crypt32.lib) endif() if(APPLE) target_link_libraries(${PROJECT_NAME} INTERFACE "-framework CoreFoundation" "-framework Security") set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "-Wl,-F/Library/Frameworks") endif() target_link_libraries(${PROJECT_NAME} OpenSSL::Crypto OpenSSL::SSL) target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES} Threads::Threads)
问题分析与修复
1. 协程未执行的核心原因
- host参数错误:传入了完整URL(
https://microsoftedge.github.io/...),但tcp::resolver仅需纯域名(如microsoftedge.github.io),错误参数会导致解析失败并抛出异常,协程提前终止。 - co_spawn调度方式错误:
main中co_spawn使用use_awaitable但未对返回的awaitable执行co_await,导致协程未被调度执行,应改用detached直接将协程加入调度队列。
2. 疑问解答
问题1:通过executor调用co_spawn
从当前协程中获取executor(co_await this_coro::executor),并确保捕获变量的生命周期安全(避免悬空引用)。示例修改:
boost::asio::awaitable<T> task_main_GET(net::io_context& ioc) { auto const host = "microsoftedge.github.io"; auto const port = "443"; auto const target = "/Demos/json-dummy-data/64KB.json"; std::osyncstream(std::cout) << "GET " << host << ":" << port << target << std::endl; ssl::context ctx{ssl::context::tlsv12_client}; ctx.set_verify_mode(ssl::context::verify_peer); boost::certify::enable_native_https_server_verification(ctx); // 获取当前协程的executor auto ex = co_await this_coro::executor; // 使用executor启动协程,通过move传递不可拷贝的ssl::context auto task = boost::asio::co_spawn( ex, [host, port, target, ctx = std::move(ctx)]() mutable -> boost::asio::awaitable<T> { co_await do_session(host, port, target, ctx); }, use_awaitable ); #ifdef use_void co_return; #else std::string responseText = co_await task; std::osyncstream(std::cout) << "response:\n" << responseText << std::endl; co_return responseText; #endif }
问题2:处理run()阻塞的问题
io_context::run()是Asio处理异步事件的核心,必须运行才能触发异步操作的完成回调。若希望应用逻辑无需显式管理run(),可采用两种方案:
方案A:后台线程运行io_context
将run()放在后台线程,协程完成后停止io_context并等待线程结束:
int main() { net::io_context ioc; // 用detached调度协程 boost::asio::co_spawn(ioc, task_main_GET(ioc), boost::asio::detached); // 后台线程处理异步事件 std::thread io_thread([&ioc]() { ioc.run(); }); // 执行其他业务逻辑... // 协程完成后停止io_context并等待线程退出 ioc.stop(); io_thread.join(); return EXIT_SUCCESS; }
方案B:使用thread_pool自动管理线程
asio::thread_pool内部管理线程池,无需手动创建线程,join()会等待所有任务完成:
int main() { net::thread_pool pool; // 传入thread_pool的executor启动协程 boost::asio::co_spawn(pool, task_main_GET(pool.get_executor()), boost::asio::detached); // 等待所有异步任务完成 pool.join(); return EXIT_SUCCESS; }
内容的提问来源于stack exchange,提问作者user2366975
相关产品推荐
相关产品推荐

