如何用C++ Asio实现HTTP/HTTPS通用Socket对象(抽象接口方案)
如何用Asio统一HTTP/HTTPS的Socket操作,避免重复代码
当然可以解决!你想要的这种统一抽象在Asio里完全能实现,我给你分享两种最实用的方案,帮你简化代码,不用再重复写asio::write这类逻辑。
方案一:基于抽象基类的多态实现
通过定义一个抽象基类,让TCP Socket和SSL Socket都实现这个接口,这样就能用基类指针/引用统一操作所有Socket类型,完美契合你想要的“一个对象搞定所有”的需求。
步骤1:定义抽象基类
先写一个包含核心操作的抽象基类,把你需要统一的write、connect等方法都声明为纯虚函数:
#include <asio.hpp> #include <asio/ssl.hpp> #include <memory> class AbstractSocket { public: virtual ~AbstractSocket() = default; // 统一的write接口(支持error_code和异常两种错误处理方式) virtual size_t write(const asio::const_buffer& buffer, asio::error_code& ec) = 0; virtual size_t write(const asio::const_buffer& buffer) = 0; // 统一的connect接口 virtual void connect(const asio::ip::tcp::resolver::results_type& endpoints, asio::error_code& ec) = 0; virtual void connect(const asio::ip::tcp::resolver::results_type& endpoints) = 0; };
步骤2:实现TCP Socket子类
让普通TCP Socket继承抽象基类,实现所有纯虚函数:
class TcpSocket : public AbstractSocket { public: explicit TcpSocket(asio::io_context& io_context) : socket_(io_context) {} size_t write(const asio::const_buffer& buffer, asio::error_code& ec) override { return asio::write(socket_, buffer, ec); } size_t write(const asio::const_buffer& buffer) override { return asio::write(socket_, buffer); } void connect(const asio::ip::tcp::resolver::results_type& endpoints, asio::error_code& ec) override { asio::connect(socket_, endpoints, ec); } void connect(const asio::ip::tcp::resolver::results_type& endpoints) override { asio::connect(socket_, endpoints); } // 暴露底层socket(如果需要额外操作的话) asio::ip::tcp::socket& get_socket() { return socket_; } private: asio::ip::tcp::socket socket_; };
步骤3:实现SSL Socket子类
同样地,让SSL Stream继承抽象基类,注意SSL连接需要先连接底层TCP Socket,再执行握手:
class SslSocket : public AbstractSocket { public: SslSocket(asio::io_context& io_context, asio::ssl::context& ssl_ctx) : socket_(io_context, ssl_ctx) {} size_t write(const asio::const_buffer& buffer, asio::error_code& ec) override { return asio::write(socket_, buffer, ec); } size_t write(const asio::const_buffer& buffer) override { return asio::write(socket_, buffer); } void connect(const asio::ip::tcp::resolver::results_type& endpoints, asio::error_code& ec) override { asio::connect(socket_.lowest_layer(), endpoints, ec); if (!ec) { socket_.handshake(asio::ssl::stream_base::client, ec); } } void connect(const asio::ip::tcp::resolver::results_type& endpoints) override { asio::error_code ec; connect(endpoints, ec); asio::detail::throw_error(ec, "connect"); } // 暴露底层SSL stream(如果需要额外操作的话) asio::ssl::stream<asio::ip::tcp::socket>& get_socket() { return socket_; } private: asio::ssl::stream<asio::ip::tcp::socket> socket_; };
步骤4:统一使用Socket
现在你就可以像最开始期望的那样,用一个基类指针统一操作了:
int main() { asio::io_context io_context; asio::ssl::context ssl_ctx(asio::ssl::context::tls_client); // 根据参数判断是否使用HTTPS bool bIsHttps = true; std::unique_ptr<AbstractSocket> my_socket; if (bIsHttps) { my_socket = std::make_unique<SslSocket>(io_context, ssl_ctx); } else { my_socket = std::make_unique<TcpSocket>(io_context); } // 解析目标地址 asio::ip::tcp::resolver resolver(io_context); auto endpoints = resolver.resolve("127.0.0.1", bIsHttps ? "443" : "80"); // 统一连接 my_socket->connect(endpoints); // 构造请求并统一写入 std::string request = "GET /myservice HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"; my_socket->write(asio::buffer(request)); // 后续的read等操作也可以统一处理,只要在基类中添加对应接口即可 return 0; }
方案二:使用std::variant(更轻量,无需继承)
如果你不想写继承体系,可以用C++17的std::variant来存储不同类型的Socket,配合std::visit统一执行操作,代码更简洁。
步骤1:定义Variant类型
先定义一个能容纳TCP Socket和SSL Stream的variant:
#include <asio.hpp> #include <asio/ssl.hpp> #include <variant> using SocketVariant = std::variant<asio::ip::tcp::socket, asio::ssl::stream<asio::ip::tcp::socket>>;
步骤2:定义统一操作的访问器
用lambda或者函数对象作为访问器,处理不同Socket类型的操作:
// 统一处理write操作的访问器 struct WriteHandler { template <typename Socket> size_t operator()(Socket& socket, const asio::const_buffer& buffer) const { return asio::write(socket, buffer); } }; // 统一处理connect操作的访问器(SSL需要额外握手) struct ConnectHandler { const asio::ip::tcp::resolver::results_type& endpoints; void operator()(asio::ip::tcp::socket& socket) const { asio::connect(socket, endpoints); } void operator()(asio::ssl::stream<asio::ip::tcp::socket>& socket) const { asio::connect(socket.lowest_layer(), endpoints); socket.handshake(asio::ssl::stream_base::client); } };
步骤3:统一使用Socket
现在可以直接用variant存储Socket,通过std::visit执行统一操作:
int main() { asio::io_context io_context; asio::ssl::context ssl_ctx(asio::ssl::context::tls_client); bool bIsHttps = true; SocketVariant my_socket; if (bIsHttps) { my_socket = asio::ssl::stream<asio::ip::tcp::socket>(io_context, ssl_ctx); } else { my_socket = asio::ip::tcp::socket(io_context); } // 解析地址 asio::ip::tcp::resolver resolver(io_context); auto endpoints = resolver.resolve("127.0.0.1", bIsHttps ? "443" : "80"); // 统一连接 std::visit(ConnectHandler{endpoints}, my_socket); // 构造请求并统一写入 std::string request = "GET /myservice HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"; std::visit(WriteHandler{}, my_socket, asio::buffer(request)); // 或者用lambda直接处理,更灵活 // std::visit([&](auto& socket) { // asio::write(socket, asio::buffer(request)); // }, my_socket); return 0; }
注意事项
- SSL上下文生命周期:无论用哪种方案,SSL上下文(
asio::ssl::context)都要确保在Socket的生命周期内有效,避免悬空引用。 - 错误处理:上面的示例用了抛出异常的方式,你可以根据需求改成
error_code版本,更灵活地处理错误。 - 扩展性:如果后续需要支持其他Socket类型(比如UDP),多态方案更容易扩展;variant方案则需要修改variant类型和访问器。
内容的提问来源于stack exchange,提问作者fr3948
相关产品推荐
相关产品推荐

