Boost.Beast http::read()返回bad version错误,寻求技术帮助
问题分析与解决方案
你遇到的bad version错误,核心原因是在TLS模式下直接操作了底层的TCP流,而没有使用SSL流进行加密读写。
当你设置encryption::tls时,你的client_transport_tls::stream()返回的是m_stream.next_layer()(也就是原始的tcp_stream),然后你用这个流来调用http::write和http::read。这就相当于把明文的HTTP请求直接发送到了HTTPS端口(443),而服务器期望的是TLS握手请求,所以服务器会返回TLS握手的响应报文。HTTP解析器把这个TLS报文当成HTTP协议来解析,自然识别不了正确的HTTP版本,就抛出了bad version错误。
官方的sync-ssl示例之所以能正常工作,是因为它全程使用ssl_stream来进行读写,所有数据都经过TLS加密/解密,而你的代码绕过了SSL层直接操作底层TCP流,这就是问题所在。
具体修改方案
我们需要重构传输类的设计,把HTTP读写操作封装到传输类内部,让不同的传输类型(明文/TLS)自己处理对应的流操作:
1. 修改client_transport.hpp,封装流操作
#pragma once #include <boost/beast/http.hpp> #include <boost/beast/core/tcp_stream.hpp> #include <boost/beast/ssl.hpp> #include <boost/asio/ssl/context.hpp> namespace http = boost::beast::http; namespace elx::http { class client_transport { public: virtual ~client_transport() = default; // 连接到指定端点 virtual void connect(const boost::beast::tcp_stream::endpoint_type& endpoint) = 0; // 执行握手(TLS需要,明文不需要) virtual void handshake() = 0; // 发送HTTP请求 virtual void write(const http::request<http::string_body>& req) = 0; // 读取HTTP响应 virtual void read(boost::beast::flat_buffer& buffer, http::response<http::dynamic_body>& res) = 0; // 关闭连接 virtual void shutdown(boost::beast::error_code& ec) = 0; }; class client_transport_plain : public client_transport { public: explicit client_transport_plain(boost::asio::io_context& io_ctx) : m_stream(io_ctx) {} void connect(const boost::beast::tcp_stream::endpoint_type& endpoint) override { m_stream.connect(endpoint); } void handshake() override { // 明文传输无需握手 } void write(const http::request<http::string_body>& req) override { http::write(m_stream, req); } void read(boost::beast::flat_buffer& buffer, http::response<http::dynamic_body>& res) override { http::read(m_stream, buffer, res); } void shutdown(boost::beast::error_code& ec) override { m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); } private: boost::beast::tcp_stream m_stream; }; class client_transport_tls : public client_transport { public: // 改用移动语义传递ssl_ctx,避免引用生命周期问题 client_transport_tls(boost::asio::io_context& io_ctx, boost::asio::ssl::context ssl_ctx) : m_stream(io_ctx, std::move(ssl_ctx)) {} // 设置SNI主机名 void set_hostname(const std::string& hostname) { if (!SSL_set_tlsext_host_name(m_stream.native_handle(), hostname.c_str())) { throw boost::beast::system_error{ boost::beast::error_code{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()} }; } } void connect(const boost::beast::tcp_stream::endpoint_type& endpoint) override { // TLS连接需要先连接底层TCP流 m_stream.next_layer().connect(endpoint); } void handshake() override { // 执行TLS客户端握手 m_stream.handshake(boost::asio::ssl::stream_base::client); } void write(const http::request<http::string_body>& req) override { // 使用SSL流发送加密后的HTTP请求 http::write(m_stream, req); } void read(boost::beast::flat_buffer& buffer, http::response<http::dynamic_body>& res) override { // 使用SSL流读取解密后的HTTP响应 http::read(m_stream, buffer, res); } void shutdown(boost::beast::error_code& ec) override { // TLS连接需要先关闭SSL层 m_stream.shutdown(ec); } private: boost::beast::ssl_stream<boost::beast::tcp_stream> m_stream; }; } // namespace elx::http
2. 修改client.cpp中的synchronous_get方法,使用封装的传输操作
client::response client::synchronous_get(const request& req) { namespace http = boost::beast::http; // Setup SSL context (in case we need that) boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tlsv12_client); // Setup encryption switch (req.encryption) { case request::encryption::none: m_transport = std::make_unique<client_transport_plain>(m_io_ctx); break; case request::encryption::tls: ssl_ctx.set_default_verify_paths(); #warning "ToDo: We definitely want to verify the peer" ssl_ctx.set_verify_mode(boost::asio::ssl::verify_none); // 创建TLS传输对象并设置SNI auto tls_transport = std::make_unique<client_transport_tls>(m_io_ctx, std::move(ssl_ctx)); tls_transport->set_hostname(req.host); m_transport = std::move(tls_transport); break; } // Sanity check if (!m_transport) { throw std::runtime_error("Failed to initialize transport layer"); } // Look up the domain name auto const results = m_resolver.resolve(req.host, std::to_string(req.port)); // Make the connection on the IP address we get from a lookup m_transport->connect(results->endpoint()); // Perform handshake m_transport->handshake(); // Set up an HTTP GET request message http::request<boost::beast::http::string_body> beast_req; beast_req.set(http::field::host, req.host); beast_req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); if (!req.auth.is_empty()) beast_req.set(http::field::authorization, req.auth.to_string()); beast_req.method(http::verb::get); beast_req.version(req.http_version); beast_req.target(req.target); // Send the HTTP request via transport layer m_transport->write(beast_req); // This buffer is used for reading and must be persisted boost::beast::flat_buffer buffer; // Declare a container to hold the response response res; // Receive the HTTP response via transport layer try { m_transport->read(buffer, res.raw); } catch (const boost::beast::system_error& e) { throw; } // Gracefully close the connection boost::beast::error_code ec; m_transport->shutdown(ec); // Ignore not_connected error (common when server closes connection first) if (ec && ec != boost::beast::errc::not_connected) { throw boost::beast::system_error{ec}; } return res; }
关键修改点说明
- 封装流操作:不再暴露底层流对象给上层,而是让传输类自己处理HTTP读写,确保TLS模式下使用
ssl_stream进行加密传输。 - 修复SSL上下文生命周期:将
ssl_ctx通过移动语义传递给client_transport_tls,避免引用局部变量带来的潜在生命周期问题。 - 正确的TLS关闭流程:TLS连接需要先调用
ssl_stream::shutdown,而不是直接关闭底层TCP socket。
另外,建议你后续把resolver.resolve的结果遍历连接(参考官方示例的net::connect),而不是只取第一个端点,这样可以提高连接成功率,但这不是当前错误的原因。
内容的提问来源于stack exchange,提问作者Joel Bodenmann
相关产品推荐
相关产品推荐

