boost::asio::ip::tcp::socketとboost::asio::ssl::streamを多態的に扱う
Boost.Asioのboost::asio::ip::tcp::socketとboost::asio::ssl::streamを多態的(動的ポリモーフィズム)に扱う例です。
C++でソケット通信を行いたい場合、有名どころのライブラリだとBoost.Asioを使うことになると思います。
HTTPなどのTCP通信を行いたい場合はboost::asio::ip::tcp::socketを、HTTPSなどTLSを使った通信を行いたい場合は、boost::asio::ssl::stream<boost::asio::ip::tcp::socket>を使います(以下、'boost'は省略します。namespace asio = boost::asio)。
TLSを使う場合、非TLS/TLSで処理を共通化するために各ソケットをポリモーフィズムで扱いと考えるかもしれませんが、asio::ip::tcp::socketとasio::ssl::streamは基底クラスが異なるためポリモーフィズムはできません。基本的にはテンプレートを使うことになります。
テンプレートを使った例(静的ポリモーフィズム)
#include <cstdint>
#include <iostream>
#include <memory>
#include <string>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
namespace asio = boost::asio;
asio::io_context io_context;
asio::ssl::context ssl_context(asio::ssl::context::tlsv12_client);
/*
* Factories
* asio::ssl::stream<asio::ip::tcp::socket>はcopyもmoveもできないので
* unique_ptrで返す。
*/
using socket_ptr = std::unique_ptr<asio::ip::tcp::socket>;
socket_ptr connect_to(const asio::ip::address& ip, uint16_t port) {
socket_ptr socket = std::make_unique<asio::ip::tcp::socket>(io_context);
socket->connect(asio::ip::tcp::endpoint(ip, port));
return socket;
}
using ssl_stream_ptr = std::unique_ptr<asio::ssl::stream<asio::ip::tcp::socket>>;
ssl_stream_ptr connect_to_with_tls(const asio::ip::address& ip, uint16_t port) {
ssl_stream_ptr stream = std::make_unique<asio::ssl::stream<asio::ip::tcp::socket>>(io_context, ssl_context);
// 証明書のチェックは省略
stream->lowest_layer().connect(asio::ip::tcp::endpoint(ip, port));
stream->handshake(asio::ssl::stream_base::client);
return stream;
}
/*
* ソケット(SyncStream)を受け取る関数をテンプレート化
*/
template<typename SyncStream>
void http_get(SyncStream& stream, const std::string& host, const std::string& path) {
std::ostringstream request_stream;
request_stream << "GET " << path << " HTTP/1.1\r\n"
<< "Host: " << host << "\r\n"
<< "Connection: Close\r\n";
request_stream << "\r\n";
asio::write(stream, asio::buffer(request_stream.str()));
boost::system::error_code error;
asio::streambuf receive_buffer;
asio::read(stream, receive_buffer, error);
if (error && error != asio::error::eof) {
throw std::runtime_error(error.message());
}
std::cout << asio::buffer_cast<const char*>(receive_buffer.data()) << std::endl;
}
int main() {
const std::string host = "localhost";
asio::ip::tcp::resolver resolver(io_context);
auto endpoints = resolver.resolve(asio::ip::tcp::v4(), host, "");
auto ip = endpoints.begin()->endpoint().address();
{
// asio::ip::tcp::socketを渡す
socket_ptr s = connect_to(ip, 80);
http_get(*s, host, "/");
}
{
// asio::ssl::stream<asio::ip::tcp::socket>を渡す
ssl_stream_ptr s = connect_to_with_tls(ip, 443);
http_get(*s, host, "/");
}
return 0;
}
この例ではhttp処理を行うhttp_get()はテンプレート化されており、asio::ip::tcp::socketとasio::ssl::streamのいずれも受け取ることができ、http/https通信の両方を処理できます。
で、ここからが本題。
上記のようにテンプレートを使えば(静的ですが)多態的な扱いができますが、それでも、virtualなメソッドで非TLS/TLS socketを引数で受けたい場合など、動的なポリモーフィズムで処理したい場合があると思います。そこで、asio::ip::tcp::socketとasio::ssl::streamをポリモーフィズムできるようなクラスを作ってみます。
全ソースはここに貼り付けるには少し長いのでhttps://github.com/kztomita/asio-socket-polymorphismにアップしました。
まず、asio::ip::tcp::socketとasio::ssl::streamはasio::write(), read()に渡せていましたが、asio::write(), read()に渡すソケットに求められる条件は何でしょうか?それは、SyncStream要件を満すことです。SyncStream要件についてはこちらのドキュメントを参照してください。簡単にいうとread_some(), write_some()メソッドを実装していればいいことになります。
そこで、方針としてSyncStream要件へ適合した基底クラスsync_stream_wrapperを作成し、サブクラスとしてasio::ip::tcp::socketやasio::ssl::streamを所有するクラスを作成するようにします(図1)。
基底クラスのsync_stream_wrapperは抽象クラスで、SyncStream要件を満すためにread_some()とwrite_some()を純粋仮想関数として持ちます。
sync_stream_wrapper基底クラス
class sync_stream_wrapper {
public:
virtual void set_host(const std::string& host) = 0;
virtual void connect(const boost::asio::ip::address& ip, uint16_t port) = 0;
// For SyncReadStream requirements
virtual std::size_t read_some(const boost::asio::mutable_buffer& buffer) = 0;
virtual std::size_t read_some(const boost::asio::mutable_buffer& buffer, boost::system::error_code& ec) = 0;
// For SyncWriteStream requirements
virtual std::size_t write_some(const boost::asio::const_buffer& buffer) = 0;
virtual std::size_t write_some(const boost::asio::const_buffer& buffer, boost::system::error_code& ec) = 0;
};
そして、asio::ip::tcp::socket, asio::ssl::streamを扱うサブクラスとしてtcp_socket, ssl_streamを作成します。
サブクラスのtcp_socket, ssl_streamではasio::ip::tcp::socket, asio::ssl::streamをそれぞれWrapし、自身のread_some(), write_some()が呼び出されたら、内部のasio::ip::tcp::socket, asio::ssl::streamのread_some(), write_some()に処理を渡すように純粋仮想関数の実装をします。
read_some()の呼出しを内部のasio::ip::tcp::socketに転送
std::size_t tcp_socket::read_some(const boost::asio::mutable_buffer& buffer) {
return socket_.read_some(buffer);
}
std::size_t tcp_socket::read_some(const boost::asio::mutable_buffer& buffer, boost::system::error_code& ec) {
return socket_.read_some(buffer, ec);
}
write_some()も同様に...
tcp_socket, ssl_streamはsync_stream_wrapperを継承しているおかげでSyncStreamに適合するので、asio::write(), read()などasio::ip::tcp::socketを渡せる関数なら同様に渡すことができます。
作成したwrapperクラスはasio::read()/write()に渡せる
// http_get()は上の静的ポリモーフィズムの例と異なり、テンプレート化されておらず // sync_stream_wrapper型でソケットを受けるので、非TLSのtcp_socketとTLSのssl_streamを // 受け取ることができる。 // また、受け取ったソケットはasio::read(),write()に渡すことができる。 void http_get(sync_stream_wrapper& sync_stream, const std::string& host, const std::string& path) { std::ostringstream request_stream; request_stream << "GET " << path << " HTTP/1.1\r\n" << "Host: " << host << "\r\n" << "Connection: Close\r\n"; request_stream << "\r\n"; // sync_stream_wrapper mets SyncStream requirements and // can be passed to asio::write(). asio::write(sync_stream, asio::buffer(request_stream.str())); // We can also pass asio::streambuf(DynamicBuffer requirements) to // asio::read(). boost::system::error_code error; asio::streambuf receive_buffer; asio::read(sync_stream, receive_buffer, error); if (error && error != asio::error::eof) { throw std::runtime_error(error.message()); } std::cout << asio::buffer_cast<const char*>(receive_buffer.data()) << std::endl; }
http_get()の呼出し例
{
// wrapper of boost::asio::ip::tcp::socket
tcp_socket s; // non TLS socket
s.connect(ip, 80);
http_get(s, host, "/");
}
{
// wrapper of boost::asio::ssl::stream
ssl_stream s(false); // TLS socket
s.connect(ip, 443);
http_get(s, host, "/");
}
これで、asio::ip::tcp::socketとasio::ssl::streamを多態的に扱うことができるようになりました。
SyncStream要件で求められるのがread_some(), write_some()の実装だけなので比較的簡単に対応できました。
ただし完全ではありません。read_some(), write_some()が受け取るバッファの型がmutable_buffer,const_buffer限定になります。ただ、asio::buffer()で作成するバッファやasio::streambufで扱うバッファはmutable_bufferおよびconst_bufferなので、これで事足りるはずです。
投稿日:2021/02/26 02:37