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 (10) 作業実績 (10) C++ (6) Webアプリ (5) PHP (5) laravel (4) Linux (4) プログラミング (3) ネットワーク (3) JavaScript (3) 書籍 (2) Nginx (2) Vue.js (2) Golang (2) EC-CUBE (2) C (1) デモ (1) CreateJS (1) AWS (1)

技術的な情報は以下にもあります。