Home > ブログ > boost::asio::ip::tcp::socketとboost::asio::ssl::streamを多態的に扱う

ブログ

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 class
図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

タグ: C++ ネットワーク プログラミング

Top

アーカイブ

タグ

Server (28) 作業実績 (21) PHP (19) ネットワーク (17) プログラミング (15) OpenSSL (10) C (8) C++ (8) PHP関連更新作業 (8) EC-CUBE (7) Webアプリ (7) laravel (6) 書籍 (5) Nginx (5) Linux (5) AWS (4) Vue.js (4) JavaScript (4) 与太話 (4) Rust (3) Symfony (2) お知らせ (2) Golang (2) OSS (1) MySQL (1) デモ (1) CreateJS (1) Apache (1)