Asio (Boost.Asio) C++ライブラリ入門 5 - 実践編 主要クラス群とI/O機能群の紹介 -

執筆者:井上 叡
監修者:稲葉 貴昭・高橋 浩和

※ 「Asio (Boost.Asio) C++ライブラリ入門」連載記事一覧はこちら


はじめに

本記事では、非同期I/Oを扱うC++ライブラリであるAsio(またはBoost.Asio)ライブラリを紹介・解説します。Asio(またはBoost.Asio)ライブラリは、非同期I/Oを行う際に利用候補に挙がることがありますが、初めて使うと意外と仕組みが理解しづらく少し苦労したりします。そこで、本連載では、Asio(またはBoost.Asio)ライブラリが採用する非同期モデルやコア機能など、ライブラリの基礎となる部分を中心に解説していきます。解説の際にはなるべくサンプルコードを掲載し、具体例と共に理解していけたらと思います。最終的には、公式リファレンスを読むことである程度好きな機能を作れるようになることを目指します。

本記事で扱うライブラリのバージョンは、Boost 1.85とAsio1.30.2です。 C++20を標準としています。コンパイラはC++20に対応したものを利用してください。 本章以降で「Asio」と記載した際、その記述は「Boost.Asio」と「Asio」の両方について述べているものとします。また、Boost.Asioを利用している場合はasio::という名前空間はboost::asio::と読み替えて下さい。

最後となる本記事では、Asioライブラリのもつ各種クラス・I/O等の機能やサンプルコードについて網羅的に紹介します。前回までの解説記事を読んでいれば本記事で紹介させるクラス・関数等は全て問題なく利用できるかと思います。なお、本記事は1から全て読んで頂くというよりは、辞書的に利用して頂くことを想定しています。

エグゼキューター関連

本章ではエグゼキューターに関連する機能を紹介します。

io_context

asio::io_contextは、Asioの非同期I/Oを利用する場合に基本となるエグゼキューターです。(※注 正確にはエグゼキューターそのものではなく、エグゼキューターを含むクラスです。execution_contextと呼ばれます。) Asioの非同期I/Oを利用する上では、特別な理由が無い場合はこのクラスを利用しておけば事足りることも多いです。 このクラスをエグゼキューターとして設定してAsioの非同期操作を行うと、その操作の完了ハンドラは、io_context::run()もしくはそれに相当するメソッドを呼び出したスレッドで処理されるようになります。

このクラスを利用する上でいくつか覚えておく必要がある事項は以下の通りです。

  • キューに入った完了ハンドラの処理を開始するためには、io_context::run()もしくはそれに相当するメソッドを呼び出す必要がある。
  asio::io_context io_context;
  asio::ip::tcp::acceptor acc(io_context, tcp::endpoint(tcp::v4(), 7650));
  auto fut = acc.async_accept(asio::use_future);
  io_context.run(); // これを呼び出さないとacceptの完了ハンドラが呼ばれない
  • io_context::run()もしくはそれに相当するメソッドを呼び出したスレッドで、キューに入った完了ハンドラが処理される。
  asio::io_context io_context;
  asio::ip::tcp::acceptor acc(io_context, tcp::endpoint(tcp::v4(), 7650));
  auto fut = acc.async_accept(asio::use_future);
  std::thread([&io_context](){ io_context.run(); }).detach(); // このスレッドでacceptの完了ハンドラが実行される
  • io_context::run()は一度呼び出せば継続的にハンドラの処理をし続けるが、ハンドラのキューが空になると動作を終了する。その場合は、io_context::restartを呼んだあと再度run()するか、もしくはasio::executor_work_guard等を使って、キューが空になってもrun()が終了しないようにする。

io_context::restart()を利用する例

  asio::io_context io_context;
  asio::ip::tcp::acceptor acc(io_context, tcp::endpoint(tcp::v4(), 7650));
  auto fut = acc.async_accept(asio::use_future);
  io_context.run(); // このrun()は↑のハンドラを実行したら返る
  fut.get();        // この段階で、io_context::run()は終了している。
  io_context.restart(); // またrun()を行うためには一度restart()を呼ぶ。
  auto fut2 = acc.async_accept(asio::use_future);
  io_context.run(); // 2回目のacceptの完了ハンドラを処理し始める

executor_work_guardを利用する例

  asio::io_context io_context;
  asio::executor_work_guard<asio::io_context::executor_type> 
    work_guard = asio::make_work_guard(io_context); 
  // このオブジェクトを作ることで、io_context::run()は終了しない
  asio::ip::tcp::acceptor acc(io_context, tcp::endpoint(tcp::v4(), 7650));
  auto fut = acc.async_accept(asio::use_future);
  std::thread([&io_context](){ io_context.run(); }).detach();
  fut.get();

thread

asio::io_contextはメインスレッド以外にスレッドを立ち上げてそこでrun()等を行う場合が多いかと思いますが、asio::threadはそのような用途のために用意されたクラスです。これは非常に単純なスレッドの抽象化で、メソッドはコンストラクタとデストラクタとjoinしか持っていません。リファレンスでは以下のようにio_context::runを呼ぶ手段として使えるよと紹介されています。正直な所、普通にstd::threadを使えば良いので、あまり使う機会は無いかもしれません。

io_context::runを別スレッドで起動する

asio::io_context io_context;
// ...
asio::thread t(boost::bind(&asio::io_context::run, &io_context));
// ...
t.join();

thread_pool

asio::thread_poolは、非同期操作の完了ハンドラをスレッドプールで実行するためのエグゼキューターです。 (※注 正確にはexecution_context) これを利用すると、固定サイズのスレッドプールのスレッドをワーカースレッドとして利用し、そこで非同期操作の完了ハンドラを実行することが出来ます。

このクラスはio_conextとは異なり、完了ハンドラの処理を開始するのにrun()等を呼ぶ必要はありません。渡された時点で処理を開始し、ユーザー側は通常のスレッドと同じようにthread_pool::join()でタスク(完了ハンドラ)の処理終了を待つことが可能です。このクラスはAsioの非同期I/Oの完了ハンドラをスレッドプールで並列処理するためにも利用できますし、単にスレッドプールとしていくつかのタスクを並列処理するのにも利用できます。スレッドプールは渡されたタスク同士の関係などについては考慮せず実行するため、共有資源は競合状態になる可能性があります。ですので、ユーザーは必要に応じて自分で排他制御を行うか、この後紹介するasio::strand等を利用して競合状態を避ける責任があります。 ちなみに、asio::static_thread_poolというものもありますが、これは単なる別名です。

Asioの非同期I/Oの完了ハンドラをスレッドプールで実行する例

  asio::thread_pool pool(4); // 4つのスレッドを持つスレッドプールを作成
  asio::ip::tcp::acceptor acc(pool, tcp::endpoint(tcp::v4(), 7650)); // スレッドプールをエグゼキューターとしてacceptorを作成
  auto fut = acc.async_accept(asio::use_future);
  fut.get(); 
  pool.join(); // joinする。get()が終わった後(=完了ハンドラの処理終了後)なので実行中のタスクは無くすぐに返るはず

スレッドプールとしてタスクの並列処理に利用する例

  asio::thread_pool pool(4);   // 4つのスレッドを持つスレッドプールを作成
  for(int i = 0; i < 30; ++i){
    // asio::post()でスレッドプールに実行する関数オブジェクトを渡せる
    asio::post(pool, [i](){    
        /* ↓ 何らかの重い処理とする */
      std::this_thread::sleep_for(std::chrono::seconds(1));});
  }
  pool.join(); // joinする。全てのタスクが終わるまでブロックされる

strand

asio::strandは、非同期操作の完了ハンドラをスレッドセーフに実行するためのエグゼキューターです。(※注 正確にはexecution context)このクラスを利用すると、1つのエグゼキューターが持つ完了ハンドラ群を複数のスレッド上で実行する場合も、その完了ハンドラは1つ1つ順番に実行されるようになります。これにより、非同期操作の完了ハンドラが同時に実行されることによる競合状態を回避することが出来ます。このような状況は、例えば1つのio_contextに対して複数のスレッドからrun()を呼び出す場合などに発生します。

1つのasio::io_contextを複数のスレッドから呼び出す際にasio::strandを使う例

asio::io_context io_context;
for(int i = 0; i < 20; ++i)
{
  asio::post(io_context,[i](){
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "task" << i << " done" << std::endl;});
}
// 5個のスレッドからrun()を呼ぶ。完了ハンドラは5個のスレッドで実行される。
// このままだとどこかのタイミングでstd::coutが競合状態になる可能性がある。
for(int i = 0; i < 5; ++i)
  std::thread([&](){io_context.run();}).detach();

// strandの作成
asio::strand<asio::io_context::executor_type> strand(asio::make_strand(io_context));
// strandを引数としてpost()する。
for(int i = 0; i < 20; ++i)
{
  asio::post(strand,[i](){
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "task" << i << " done" << std::endl;});
}
// 5個のスレッドがio_context::run()を呼ぶ。
// post()した関数オブジェクトはそれぞれ5個のスレッドのどこかで実行されるが、
// 同時には実行されず、順番に実行されるようになる。
// std::coutの競合状態を回避できた。
for(int i = 0; i < 5; ++i)
  std::thread([&](){io_context.run();}).detach();

system_executor

system_executorはそのシステムのエグゼキューター、つまりグローバルなエグゼキューターです。(注※ 正確にはexecution context)これはシングルトンで、プログラム中のどこからでも同じエグゼキューターを取得できます。このエグゼキューターで実行される関数オブジェクトは、暗黙的にどこかのバックグラウンドのスレッドで実行されます。 例えば、複数のスレッドでこのエグゼキューターを取得して、それぞれのスレッドから関数オブジェクトをasio::postすると、どこかのスレッドでそれらのオブジェクトが処理されます。それらのタスクの実行順序は規定されず、当然ながら同期も取られません。競合状態を避けたいならば、asio::strandを利用するか、自分で排他制御を行う必要があります。ここで気を付ける必要があるのは、asio::ip::tcp::socketなどのI/Oオブジェクトです。これらはスレッドセーフでは無いので、system_executorをエグゼキューターに設定する場合はasio::strandを利用しなくてはいけません。

asio::system_executorを利用する例

asio::system_executor sys_exe;
asio::post(sys_exe, [](){std::cout << "Name :: Mika Jogasaki" << std::endl;});
// 以下のように構築したものを直接渡すのも可能
asio::post(asio::system_executor(),[](){std::cout << "Name :: Azusa Miura" << std::endl;});

asio::strandを使って上記を安全に出力する例

asio::system_executor sys_exe;
auto strand = asio::make_strand(sys_exe);
asio::post(strand, [](){std::cout << "Name :: Fuka Toyokawa" << std::endl;});
asio::post(strand, [](){std::cout << "Name :: Ryu Kimura" << std::endl;});

asio::requireでエグゼキューターの動作を変更する

Asioには、requireというカスタマイゼーションポイントがあります。これを利用すると、エグゼキューターにプロパティを設定する事ができ、これによっても動作を変更することができます。 プロパティは、リファレンスにあるPropertiesという項目に書いてあるクラス群です。 asio::requireを使ってProperty Objectをエグゼキューターに適用する事で、動作を変更できます。基本的にはこちらのプロパティをいじる方法よりもexecutor_work_guardstrandなどを直接利用する方が便利な場合が多いですが、一応紹介しておきます。

io_context::runがキューが空になっても自動終了しないように設定する。実際の所、asio::executor_work_guardの方が便利なのであまり使わなくて良いと思われる。

asio::io_context io_context;
// requireで、io_contextの持つエグゼキューターにoutstanding_work::tracked_tを設定する。
auto exe1 = asio::require(io_context.get_executor(), asio::execution::outstanding_work.tracked);
// ↑ の設定を解除する
auto exe2 = asio::require(io_context.get_executor(), asio::execution::outstanding_work.untracked);

エグゼキューターに渡された関数オブジェクトの呼び出しが完了するまでエグゼキューターの実行関数がブロックするように設定する。これは、asio::post等や非同期操作から完了ハンドラなどをエグゼキューターに渡した時に、それらが実行完了するまでそこでブロックされることを意味する。

asio::io_context io_context;
// requireで、io_contextの持つエグゼキューターにblocking::always_tを設定する。
auto blocking_exe = asio::require(io_context.get_executor(), asio::execution::blocking.always);
// この設定をすると、post()でタスクが終わるまで約2秒以上ブロックされるようになる
asio::post(blocking_exe, [](){std::this_thread::sleep_for(std::chrono::seconds(2));});
// ブロックしないように設定する
auto normal_exe = asio::require(io_context.get_executor(), asio::execution::blocking.never);
// 通常通り、このpostはすぐに返る
asio::post(normal_exe, [](){std::this_thread::sleep_for(std::chrono::seconds(2));});

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

完了トークンアダプター(Completion Token Adapters)

完了トークンアダプター(Completion Token Adapters)は、完了トークンに対して追加の設定のようなものを付与するために利用するものです。関連特性の紹介をした記事などで出てきた、asio::bind_executorなどがこれに該当します。 完了トークンアダプターの主な用途は以下の2つです。

  1. 完了トークンに対して何らかの特性を付与して新たな完了トークンを生成
  2. 完了シグネチャや、完了ハンドラに渡される引数を変更する

1.の用途としてはbind_executorbind_allocatorbind_cancellation_slotbind_immediate_executorなどがあります。これらは名前の通りの関連特性を完了トークンに付与して新たな完了トークンを作り出します。

asio::bind_executorで非同期操作に対してstrandをエグゼキューターとして設定する例

// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
my_socket.async_read_some(my_buffer,
    asio::bind_executor(my_strand,
      [](asio::error_code error, std::size_t bytes_transferred)
      {
        // ...
      }));

2.の用途としては、エラーを別の特定の変数にリダイレクトするredirect_errorや、完了ハンドラの引数を1つのタプルにするas_tupleappendprependconsignなどがあります。

asio::redirect_error

asio::redirect_errorでエラーを変数に格納する例

// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
asio::error_code ec;  // 操作完了まで有効である必要あり
my_socket.async_read_some(my_buffer,
    asio::redirect_error(
      [](std::size_t bytes_transferred) // エラーは完了ハンドラの引数ではなくなる
      {
        // ...
      }, ec)); //このecにエラーが格納される。

// futureやco_awaitなどで結果を受け取る場合は、動作は変更されない。
// 以下では規定通りfutureから例外が投げられる。
auto fut = my_socket.async_read_some(my_buffer,
    asio::redirect_error(
      asio::use_future, ec)); // この場合はecに格納されない。

asio::as_tuple

asio::as_tupleで完了ハンドラの引数をタプルにまとめる例

// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
std::future<std::tuple<asio::error_code,size_t>> read_result = 
    socket.async_read_some(buffer,asio::as_tuple(asio::use_future)); //as_tupleアダプタを使う
auto [e,n] = read_result.get();   // エラーを例外ではなく変数で受け取れるようになる
std::cout << e.message() << std::endl; // エラーを表示

asio::append

asio::appendで完了ハンドラに追加の引数を渡す例
appendの活用例としてはこのようなものがあるようです。

// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
timer.async_wait(
    asio::append(
      [](asio::error_code ec, int i) // intの引数を追加
      {
        // ...
      },
      765346876  // 完了ハンドラに渡される値
    )
  );

// 本来future<void>だが、future<int>になる
std::future<int> f = timer.async_wait(
    asio::append(
      asio::use_future,
      315283   // futureに渡される値
    )
  );

asio::prepend

asio::prependで完了ハンドラの引数の最初に追加の引数を設定する例
(asio::appendの最初に追加するバージョン)

timer.async_wait(
    asio::prepend(
      [](int i, std::error_code ec) // 第一引数にint追加
      {
        // ...
      },
      765
    )
  );

asio::consign

asio::consignで完了ハンドラに変数を付属させる例
これは主にあるオブジェクトのコピーをハンドラが呼ばれるまで生き残らせるために使います。
例えば、スマートポインタや、何らかの保持しておきたいリソースなどに使えます。

// タイマーを作る
auto timer1 = std::make_shared<asio::steady_timer>(my_io_context);
timer1->expires_after(std::chrono::seconds(1));
timer1->async_wait(
    asio::consign(
      [](std::error_code ec)
      {
        // ...
      },
      timer1  //タイマーのポインタのコピーを保持させる。
              //これにより、元のtimer1が破棄されても問題無く動作する。
    )
  );

// use_future版
auto timer2 = std::make_shared<asio::steady_timer>(my_io_context);
timer2->expires_after(std::chrono::seconds(30));
std::future<void> f =
  timer2->async_wait(
    asio::consign(
      asio::use_future,
      timer2
    )
  );

完了トークンからの関連特性の取得

前項では完了トークンに関連特性を設定するbind_○○を紹介しましたが、完了トークンなどから現在結びついている関連特性を取得する方法もあります。(取り除くのではなく、今何が設定されているのか確認する方法です) asio::get_associated_allocatorasio::get_associated_cancellation_slotasio::get_associated_executorasio::get_associated_immediate_executorが相当します。 これらのカスタマイゼーションポイントは、前回記事で紹介したasio::async_composeでの非同期操作の作成時や、開始関数を連鎖させる際などに利用可能です。strandを構築するときなどにも利用できます。

【完了ハンドラからエグゼキューターを取り出す例】

asio::io_context io_c;
// 完了ハンドラにエグゼキューターをバインド
auto handler = asio::bind_executor(
              io_c.get_executor()
              ,[](asio::error_code ec){std::cout << ec.message() << std::endl;});
// 完了ハンドラに結びつけられたエグゼキューターを取得
auto handler_exe = asio::get_associated_executor(handler);
// 完了ハンドラから取得したエグゼキューターでstrandを構築
asio::strand<decltype(handler_exe)> strand(handler_exe);

ちなみに、asio::io_contextasio::ip::tcp::socketasio::steady_timerなどのオブジェクトから結びつけられたエグゼキューターを取得したい場合は、それぞれのオブジェクトが持つget_executor()メソッドを利用します。それらに対してはasio::get_associated_executorは残念ながら使えません。

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

エグゼキューターにタスクを渡す方法

エグゼキューターは、非同期操作の完了ハンドラを処理するだけでなく、勿論ユーザーが渡したタスクを実行するためにも利用できます。最も分かりやすい利用例としては、スレッドプールであるエグゼキューターのasio::thread_poolに対して、何らかの重いタスクを渡してバックグラウンドの別スレッドで処理をするなどがあります。ここでは、何らかの関数オブジェクトをエグゼキューターに渡して実行してもらう方法をいくつか紹介します。代表的なフリー関数は、asio::postasio::dispatchasio::deferなどがあります。これらは実行方法は違えど、渡した関数オブジェクトを指定したエグゼキューターで実行するものです。以下にそれぞれについて紹介します。

asio::post

asio::post()でトークン(関数オブジェクト)をサブミットする例
関数オブジェクトはエグゼキューターの実行キューに入れられ、post()から戻る前に現在のスレッドから呼び出される事はない ※defer()post()の違いは下記

asio::io_context io_context;
auto token = [](){std::cout << "name1 :: idol" << std::endl;};
// postする
asio::post(io_context,token);
// tokenにエグゼキューターをバインドして渡す事も可能
asio::post(asio::bind_executor(io_context,token));
io_context.run();

asio::defer

asio::defer()でトークン(関数オブジェクト)をサブミットする例
関数オブジェクトはエグゼキューターの実行キューに入れられ、defer()から戻る前に現在のスレッドから呼び出される事はない ※defer()post()の違いは下記

asio::io_context io_context;
auto token = [](){std::cout << "name2 :: m@ster" << std::endl;};
// deferする
asio::defer(io_context,token);
// tokenにエグゼキューターをバインドして渡す事も可能
asio::defer(asio::bind_executor(io_context,token));
io_context.run();

※ [deferとpostの違い]
これら2つは、ユーザーからのエグゼキューターに対してタスクのキューイングの方法の希望が違います。deferを使ってサブミットしたタスクは、エグゼキューターはキューイングを延期するなどして最適化を行う事ができます。逆にpostを使ってサブミットしたタスクは、エグゼキューター側はキューイングを延期などしてはならず、遅延せず実行されなければなりません。基本的に、post()の方が利用する事が多いかなと思います。

asio::dispatch

asio::dispatchを使ってトークン(関数オブジェクト)をサブミットする例
関数オブジェクトは可能な場合は現在のスレッドから呼び出される。すぐに可能ではない場合は、今後の実行のためにキューイングされる。

asio::io_context io_context;
auto token1 = [](){std::cout << "person1 :: Azusa Miura" << std::endl;};
auto token2 = [](){std::cout << "person2 :: Mika Jogasaki" << std::endl;};
// dispatchする
asio::dispatch(io_context,token1);
// tokenにエグゼキューターをバインドして渡す事も可能
asio::dispatch(asio::bind_executor(io_context,token2));
io_context.run();

asio::spwanを利用したスタックフルコルーチンを用いた非同期操作の実行

この章で紹介したフリー関数群とは少し毛色が違いますが、asio::spawnを使うと、スタックフルコルーチン*1を使って非同期操作を実行できます。 asio::spawnはオーバーロードが6個程ありますが、本章で紹介した他の関数の使い方と近いのは以下のシグネチャのものです。

template<
    typename ExecutionContext,
    typename F,
    typename CompletionToken = default_completion_token_t<            typename ExecutionContext::executor_type>>
auto spawn(
    ExecutionContext & ctx,
    F && function,
    CompletionToken && token = default_completion_token_t< typename ExecutionContext::executor_type >(),
    constraint_t< is_convertible< ExecutionContext &, execution_context & >::value >  = 0);

ここで、ctxはasio::io_contextasio::thread_poolなどです。functionはコルーチンの関数で、tokenは完了トークンとなります。

コルーチンの関数は、以下のシグネチャである必要があります。

void function(basic_yield_context<Executor> yield);

ちなみに、大抵の場合はbasic_yield_contextそのままではなく、以下のエイリアスが利用されます。

typedef basic_yield_context< any_io_executor > yield_context;

また、完了トークンも少し違うシグネチャです。

void handler(std::exception_ptr);

利用例は、以下のようになります。

// コルーチン関数を定義
void do_echo(asio::yield_context yield)
{
  try
  {
    char data[128];
    for (;;)
    {
      std::size_t length =
        my_socket.async_read_some(
          asio::buffer(data), yield);
          // ↑ この関数でyield

      asio::async_write(my_socket,
          asio::buffer(data, length), yield);
          // ↑ この関数でもyield
    }
  }
  catch (std::exception& e)
  {
    // ...
  }
}
// 完了トークンにdetachedを渡して、こちらに制御を戻さないことも可能。
asio::spawn(my_strand, do_echo, asio::detached);
//           ↑socketはstrand越しに操作した方が良い

本項目に関連する公式のサンプルコード

Asio

-https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html - ページ内のExecutorsの項目にあるサンプルコード類では、postdispatchdeferなどを利用しています。その他の多くのサンプルコードでも本項目の機能は利用されています。

Boost.Asio

バッファ

Asioで非同期I/Oを利用する際、たいていの状況でバッファを利用することになります。特に、読み込み動作ではバッファにデータが格納されるというのがユーザーから見た主要な副作用になります。また、非同期の読み取り操作の完了ハンドラは、呼び出された瞬間に既にバッファへのデータの格納が終了しているという特性があります。これにより、バッファを再利用したり、データレースを簡単に避けられるようになっています。非同期の書き込み動作でも、書き込みデータをバッファに格納したのち送信します。

Asioにおけるバッファの基本形は、以下の2つのように表すことができます。
(※ 実際には利便性のためクラスになっています)

typedef  std :: pair < void *,  std :: size_t >  mutable_buffer ; 
typedef  std :: pair < const  void *,  std :: size_t >  const_buffer ;

上記を見ると分かるように、バッファはポインタとバイト単位のサイズで構成されるタプルとして表せることが分かります。また、変更可能(mutable)なものと、変更不可能(const)なものの2種類があることも分かります。
実際には上記はそれぞれ、asio::const_bufferasio::mutable_bufferとして定義されています。変更可能なものは主にデータの受信・読み取り用、変更不可のものは主にデータの送信・書き込みに利用します。

また、上記はScatter-Gather I/Oに利用することもできます。上記の2種類のバッファを、std::vectorstd::liststd::arrayboost::arrayなど、std:: bidirectional_iterator<>をもつコンテナに格納することでscatter/gather操作用のバッファ(ConstBufferSequenceMutableBufferSequence)として利用することができます。

バッファは、asio::buffer関数を使うと簡単に構築できます。

バッファの基本的な使用方法は以下のようになります。

// mutable_bufferの構築
char data[128];
std::vector<char> vec(256);
std::array<char, 256> arr;   // char,vector,arrayなどから構築可能
asio::mutable_buffer charbuf = asio::buffer(data, sizeof(data));
asio::mutable_buffer vecbuf = asio::buffer(vec);
asio::mutable_buffer arrbuf = asio::buffer(arr);
void* vdata;
size_t vdata_size;
asio::mutable_buffer vecbuf2 = asio::buffer(vdata,vdata_size) // voidポインタとサイズからも構築可

// const_bufferの構築
const char cdata[8] = "765876";    // constなデータを渡すとconst_bufferになる
asio::const_buffer  cb1 = asio::buffer(cdata); 
const std::vector<char> cvec = {1,2,3};
asio::const_buffer cbuf2 = asio::buffer(cvec);

// const_bufferはリテラルで構築することも可能
using namespace asio::buffer_literals;
asio::const_buffer b1 = "hello"_buf;
asio::const_buffer b2 = 0xdeadbeef_buf;
asio::const_buffer b3 = 0x01234567'89abcdef'01234567'89abcdef_buf;
asio::const_buffer b4 = 0b1010101011001100_buf;

// データのアクセス
void * mbuf_ptr = vecbuf.data();  // voidポインタを得る(mutable)
size_t mbuf_size = vecbuf.size(); // サイズを取得(mutable)

const void * cbuf_ptr = cb1.data();     // voidポインタを得る(const)
size_t cbuf_size = cb1.size();    // サイズを取得(const)

// I/Oでの利用
size_t bytes_transferred = socket.receive(bufs1);
size_t bytes_transferred = socket.send(cb1);  // const_bufferを利用してデータ送信

// Scatter-Gather I/O用のバッファ
char d1[128];
std::vector<char> d2(128);
boost::array<char, 128> d3;
// Scatter-Gather用のmutable_buffer
boost::array<mutable_buffer, 3> bufs1 = {
  asio::buffer(d1),
  asio::buffer(d2),
  asio::buffer(d3) };
size_t bytes_transferred = socket.receive(bufs1);  // Scatter-Gather用バッファを受け取るオーバーロードがあるメソッドや関数に渡せば利用可能

// Scatter-Gather用のconst_buffer
std::vector<const_buffer> bufs2;
bufs2.push_back(asio::buffer(d1));
bufs2.push_back(asio::buffer(d2));
bufs2.push_back(asio::buffer(d3));
size_t bytes_transferred = socket.send(bufs2);  // Scatter-Gather用バッファを受け取るオーバーロードがあるメソッドや関数に渡せば利用可能

ここで一点注意する点は、これらのバッファの構築元となったコンテナ類はバッファの利用が終了するまで破棄してはいけないことです。バッファを構築した事に満足して生存期間を終わらせたりしないようにしましょう。

上記までで紹介したのは基本となるバッファですが、Asioにはasio::dynamic_bufferという、バッファのサイズを自動で調整してくれるクラスも用意されています。これを使うと、バッファのサイズを気にせずにデータの受信や送信が行えます。asio::read_until()などが例ですが、行う操作によってはバッファとしてこのasio::dynamic_bufferしか利用できないものもあります。dynamic_bufferの具体的な型としては、asio::dynamic_string_bufferasio::dynamic_vector_bufferの2種類があります。これらは、内部で利用するコンテナが違うだけで基本的に同じように利用できます。

【dynamic_bufferの構築】

// dynamic_bufferから両方構築可能
std::string str_data;
std::vector<uint8_t> vec_data;
asio::dynamic_string_buffer dbuf1 = asio::dynamic_buffer(str_data);  // std::stringから構築
asio::dynamic_vector_buffer dbuf2 = asio::dynamic_buffer(vec_data);  // std::vector<>から構築

構築は非常に簡単なのですが、dynamic_bufferは少し罠があります。このバッファに対する要件が現在DynamicBuffer_v1DynamicBuffer_v2の2種類あります。リンク先のページを見ていただくと分かるように、仕様が異なっています。Asioとしてはv1の方は互換性のために残しているようで、これから利用するのであればv2を利用することが推奨されているようです。例としてasio::dynamic_vector_bufferのリファレンスには、メソッド毎にV1とv2での違いが書いてあります。現状では、v1とv2の両方に対応したオーバーロードが用意されていますが、v2の方の機能のみ使っていく方が好ましいと思われます。

【利用例(v2のみ)】

std::string data;
asio::dynamic_string_buffer buf = asio::dynamic_buffer(data);
// read_untilは特定のデリミタまで読み込むための関数
size_t length = asio::read_until(socket,buf,'x'); // 読み込んだデータの長さを返す
auto buf_data = buf.data(0,length); // バッファのシーケンスが返ってくる。
auto buf_itr = buf_data.begin();    // バッファのイテレータを取得
asio::mutable_buffer mbuf = *buf_itr; // イテレータでmutable_bufferにアクセス
// read_untilで読み込まれたデリミタまでのデータが表示されるはず
std::cout << std::string(reinterpret_cast<char*>(mbuf.data()),length) << std::endl;

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

I/O用のフリー関数

Asioには、この項目以降に紹介する様々なプロトコルに対する個別のクラス等が用意されていますが、それら個別のメソッドを利用せず共通のフリー関数を使ってI/Oを行う事も可能です。

以下に、I/Oを行うことができるフリー関数を列挙します。 - asio::async_read - asio::async_read_at - asio::async_read_until - asio::async_write - asio::async_write_at - asio::read - asio::read_at - asio::read_until - asio::write - asio::write_at

これらは、async_がついている関数が非同期版で、ついていないものは同期版です。 また、_atがついているものは、ファイル等においてオフセットを指定し、その位置から読み書きをはじめるためのものです。_untilがついているものは、指定した区切り文字まで読み込むためのものです。_until系の関数はバッファがDynamicBufferである必要があります。これらは要件を満たすI/Oクラス、つまりソケットクラスやファイルクラスに対して利用する事ができます。あるI/Oクラスに対して対象のフリー関数が利用できるかどうかは、オーバーロード一覧を見れば分かります。これらの関数は基本的にはストリーム方式のI/Oに対して利用可能です。例としてasio::async_readは、AsyncReadStreamという要件を満たすI/Oクラスなら利用できます。これは簡単で、そのI/Oクラスがasync_read_someメソッドを持っていれば良いだけです。なので、async_readはTCP・Unix domainソケットクラス(ストリーム方式)やシリアルポートクラスなどに対して利用する事ができます。逆にUDPソケットやICMPソケットなどのデータグラム方式のI/Oクラスはasync_read_someメソッドを持っていないので、それらに対しては利用する事ができません。

【利用例】

// 接続済みのTCPソケット(接続済みである必要がある)
asio::ip::tcp::socket connected_tcp_socket;
// 接続済みのUNIXドメインソケット
asio::local::stream_protocol::socket connected_unix_socket;
// 接続済みのシリアルポート
asio::serial_port connected_serial_port;
// UDPソケット
asio::ip::udp::socket udp_socket;

char data[1024];
auto readbuf = asio::buffer(data, sizeof(data));
using namespace asio::buffer_literals;
auto writebuf = "hello_free_function"_buf;
// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
auto handler = [](asio::error_code ec, std::size_t bytes_transferred){}; 

asio::read(connected_tcp_socket,readbuf);
asio::async_read(connected_tcp_socket,readbuf,handler);  // async_○○は完了ハンドラが必要
asio::write(connected_tcp_socket,writebuf);
asio::async_write(connected_tcp_socket,writebuf,handler);// async_○○は完了ハンドラが必要

// これはI/Oクラス(UDPソケットクラス)が要件を満たしておらず、コンパイルエラーになる
// asio::read(udp_socket,readbuf);

// read_until系はdynamic_bufferを利用する必要がある
std::string str_data;
auto dy_buf = asio::dynamic_buffer(str_data);
// _が出るまで読み込む.writebufの内容を受信した場合"hello_"を受信する.
size_t l = asio::read_until(connected_tcp_socket,dy_buf,'_');
// (※) 受信後のバッファの扱いはバッファの項目を参照

接尾語なしのread/writeはバッファとして、通常のconst_buffermutable_bufferとそれのScatter-Gather用であるConstBufferSequenceMutableBufferSequence、加えてDynamicBufferも利用することができます。

【利用例】

// MutableBufferSequenceの構築
char d1[128];
std::vector<char> d2(128);
std::array<char, 128> d3;
std::array<asio::mutable_buffer, 3> bufs1 = {
  asio::buffer(d1),
  asio::buffer(d2),
  asio::buffer(d3) };
// DynamicBufferの構築
std::string data;
asio::dynamic_string_buffer dy_buf = asio::dynamic_buffer(data);

// いつもと同じようにバッファを渡せばよい
size_t length = asio::read(socket,bufs1,asio::transfer_exactly(300));
length = asio::read(socket,dy_buf,asio::transfer_exactly(128));

また、CompletionConditionを指定することで動作を指定することができます。 CompletionConditionは、読み込みや書き込みの完了条件を指定するためのもので、以下のようなものがあります。

ソケットクラスが持っているI/Oのメソッドでは、読める所まで読んだ時点で完了ハンドラを呼ぶ動作をするものが多いですが、上記のような設定と併せてフリー関数を利用すると、より細かくI/Oの動作を制御することができます。

【利用例】

// 64バイト以上読み込むまで、もしくはエラーが発生するまで読み込みを続ける
asio::async_read(connected_tcp_socket,cbuf,asio::transfer_at_least(64),handler);
// 全てのデータが転送されるまで、もしくはエラーが発生するまで読み込みを続ける
asio::read(connected_tcp_socket,cbuf,asio::transfer_all());
asio::write(connected_tcp_socket,writebuf,asio::transfer_all());
// ぴったり4バイト転送されるまで、もしくはエラーが発生するまで読み込みを続ける
asio::async_write(connected_tcp_socket,writebuf,asio::transfer_exactly(4),handler);

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

ネットワーク

ネットワークに関連する操作はある意味Asioのメインコンテンツとも言えるもので、豊富に機能があります。ここでは全ては紹介できませんが、代表的なものをいくつか紹介します。プロトコルはTCP、UDP、ICMPの3種に関する機能がデフォルトで用意されています。また、それに加えてプラットフォームによってはUnixドメインソケットに関するAPIも実装されています。

Asioのネットワーク機能は、主にasio::ip::asio::generic::という2つの名前空間に分かれています。基本となるのは、asio::ip::の名前空間内にあるもので、こちらは更にip::icmp::ip::tcpip::udpip::address_v4などに分かれています。それらでは名前のプロトコルに対するソケットやアドレスが提供されています。asio::generic名前空間ではジェネリックなAPIが用意されています。

公式にはネットワークに関する豊富なサンプルがあります。 Asio版

Boost版

アドレス

asio::ip::addressはipアドレスを扱うためのクラスです。ipv4のアドレスを表すクラス(asio::ip::address_v4)とipv6アドレスを表すクラス(asio::ip::address_v6)もあります。addressからはaddress_v4address_v6への変換が可能です。また、ipv4のネットワークを表す事ができるip::network_v4ip::network_v6というクラスもあります。こちらはネットマスク等を設定できます。

【利用例】

// 構築
// 構築する。ヘルパー関数を使うと簡単。
asio::ip::address adr = asio::ip::make_address("192.168.1.1");
asio::ip::address_v4::bytes_type bytes_v4 = {127, 0, 0, 1};
asio::ip::address_v4 adr4 = asio::ip::make_address_v4(bytes_v4);
asio::ip::address_v6 adr6 = asio::ip::make_address_v6("::1");
// 特性の確認もできる
std::cout << (adr6.is_loopback() ? "this is loopback" : "not loopback") << std::endl;

// アドレスとプリフィックス長を指定してnetwork_v4を構築
auto addr_v4 = asio::ip::make_address_v4("10.0.0.0");
asio::ip::network_v4 netv4(addr_v4,24);

// アドレスとネットマスクからnetwork_v4を作成
asio::ip::address_v4 mask = asio::ip::make_address_v4("255.255.0.0");
asio::ip::network_v4 network2(addr_v4, mask);
// 特性の確認もできる
std::cout << (network2.is_host() ? "this is host address" : "not host") << std::endl;

TCP

AsioのTCP機能には、ソケットであるip::tcp::socketとソケットの接続を補助してくれるip::tcp::acceptor、エンドポイントを表すip::tcp::endpointやリゾルバクラスip::tcp::resolver等があります。他のI/Oクラスと同じように、非同期・同期の両方の操作をサポートしています。

ip::tcp::endpointは、その名の通りエンドポイントを表すクラスで、IPアドレスとポート番号の組み合わせを保持します。このクラスは、接続先の指定などに利用します。

【利用例】

// anyアドレスのエンドポイントを構築
asio::ip::tcp::endpoint ep_v4(asio::ip::tcp::v4(), 7650);
asio::ip::tcp::endpoint ep_v6(asio::ip::tcp::v6(), 3460);
// 特定のIPアドレスのエンドポイントを構築
asio::ip::tcp::endpoint ep_v4_2(asio::ip::make_address_v4("192.168.5.123"), 7650);
asio::ip::tcp::endpoint ep_v6_2(asio::ip::make_address_v6("::1"), 3460);

ip::tcp::acceptorはソケットの接続受け入れを行うためのクラスです。このクラスがもつasync_acceptacceptメソッドはacceptが成功すると接続済みのソケットを返してくれます。(async_acceptの場合は完了ハンドラの引数や、指定された参照先に渡します) ユーザーは、その返ってきた接続済みソケットを使って送受信の操作を行うことができます。非同期版のasync_acceptでは、完了ハンドラ内で接続済みソケットが利用可能な事を利用して、連鎖的にread/writeを開始する事ができます。この手法は、3記事目で示した、非同期操作の連鎖です。完了ハンドラ内で次の操作の開始関数を呼ぶこの典型的な方法は、接続失敗した時の場合分けなどを非常に簡潔に書けるため、非同期なacceptでは可能な限り利用した方が良いと思います。ちなみに、ソケットクラスが持つconnectのためのメソッド、async_connectでもこの連鎖は有効です。

【acceptorの利用例】

asio::io_context io_context;
// エンドポイントの構築(anyアドレス)
asio::ip::tcp::endpoint ep(tcp::v4(), port);
// acceptorの構築
asio::ip::tcp::acceptor acceptor(io_context, ep);

// ハンドラに接続済みソケットを渡す際の完了ハンドラ
// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
void accept_handler(const asio::error_code& error,
    asio::ip::tcp::socket peer)
{
  if (!error)
  {
    // Accept succeeded.
    // peerは接続済みのソケット
    peer.async_read_some(...); // async_read_someの利用法は後述
  }
}
acceptor.async_accept(accept_handler); // 非同期accept開始

asio::ip::tcp::socket peer2(io_context); // 接続済みソケットを受け取るための変数
// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
acceptor.async_accept(peer2,[&](const asio::error_code& error){
  if(!error)
  {
    // Accept succeeded.
    // この時点でpeer2は接続済みのソケットが入っている
    peer2.async_read_some(...); // async_read_someの利用法は後述
  }
});

asio::ip::tcp::socket peer3(io_context); // 接続済みソケットを受け取るための変数
acceptor.accept(peer3); // 同期accept

ip::tcp::socketはその名の通りTCPソケットのクラスです。受け入れ側では接続済みのこのクラスがacceptorから返されますが、接続要求する側では、async_connectconnectメソッドを利用して接続要求をします。

【connectの例】

asio::io_context my_context;
// ipv4ソケットの構築
asio::ip::tcp::socket socket_v4(my_context,asio::ip::tcp::v4());
// ipv6ソケットの構築
asio::ip::tcp::socket socket_v6(my_context,asio::ip::tcp::v6());

// 接続先を指定するエンドポイントを構築
asio::ip::tcp::endpoint endpoint(
    asio::ip::address::from_string("1.2.3.4"), 12345);
// 接続要求を行う(同期)
socket_v4.connect(endpoint);

// 接続要求を行う(非同期)
// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
void connect_handler(const asio::error_code& error)
{
  if (!error)
  {
    // 接続成功
  }
}
socket_v4.async_connect(endpoint, connect_handler);

ソケットが持つ読み書きの機能は以下の通りです。ここで注意しなくてはいけないのは、非同期・同期、読み込み・書き込みに関わらず、これらの関数は全てのデータを転送するとは限らないことです。1バイト以上の転送ができた時点で終了する可能性があります。もしも、指定したバイト数まで確実に読んでから終了してほしい、などの希望がある場合はフリー関数のI/O操作を利用してください。また、読み込みはread_somereceive、書き込みはwrite_somesendの2種類が用意されていますが、それらはそれぞれほぼ同じ動作をします。しかし基本的には、TCPソケットではread_somewrite_someを利用する事が多いと思います。

非同期操作

同期操作

【ソケットの読み書きの例】

// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
// 完了ハンドラ
void handler(
  const asio::error_code& error,
  std::size_t bytes_transferred //転送したバイト数
){
  if(!error)
  {
    // 転送成功
  }
  else
  {
    // 転送失敗
  }
}

// 非同期版 (ソケットは接続済)
socket.async_read_some(asio::buffer(data, size), handler);
socket.async_write_some(asio::buffer(data, size), handler);

// 同期版 (ソケットは接続済)
socket.write_some(asio::buffer(data, size));
socket.read_some(asio::buffer(data, size));

名前解決をしたい場合は、resolverクラスを利用します。本記事では紹介しませんが、boostjpなどで利用方法が紹介されています。

ソケットには、オプションを設定することもできます。 以下は設定可能なオプションの一部です。一覧はTCPソケットクラスのドキュメントを参照してください。

【利用例】

// acceptorのオプションを設定する
asio::ip::tcp::acceptor acceptor(my_context);
asio::socket_base::reuse_address option(true);
acceptor.set_option(option);

// ソケットのオプションを設定する
asio::ip::tcp::socket socket(my_context);
asio::socket_base::keep_alive option(true);
socket.set_option(option);

最後に、TCPでの一連の簡単な通信サンプルを紹介します。

【TCPでのデータ送受信の例】

// #define USE_BOOST_ASIO_LIB //Boost版を利用する場合はコメント解除orコンパイル時にマクロ定義

#ifdef USE_BOOST_ASIO_LIB
#include "boost/asio.hpp"
using namespace boost;
using error_code_t = boost::system::error_code;
#else
#include "asio.hpp"
using error_code_t = asio::error_code;
#endif // USE_BOOST_ASIO_LIB

#include <iostream>
#include <future>
#include <string>

using asio::ip::tcp;

class Client
{
    asio::io_context &io_context_;
    // ソケット
    tcp::socket send_socket_;
    // エンドポイントを表す
    tcp::endpoint endpoint_;

public:
    Client(asio::io_context &ioc, const short port)
                            // アドレスとポートからエンドポイントを構築
        : io_context_(ioc), endpoint_(asio::ip::make_address_v4("127.0.0.1"), port)
            , send_socket_(io_context_) {}

    // connect開始
    void start_connect()
    {
        send_socket_.async_connect(endpoint_, [this](const error_code_t &err)
                                   { connect_handler(err); });
    }

    // send開始
    void start_send()
    {
        std::cout << "Sending data..." << std::endl;
        const std::string send_data("Data dayo.");
        send_socket_.async_send(asio::buffer(send_data),
                                [this](const error_code_t &err, const size_t bytes_transferred)
                                {
                                    send_handler(err, bytes_transferred);
                                });
    }

    // connect完了ハンドラ
    void connect_handler(const error_code_t &err)
    {
        if (!err)
        {
            std::cout << "Connect succeeded!!!" << std::endl;
            start_send(); // 接続が完了したので、次は送信を開始する
        }
        else
        {
            // 失敗した場合は次の操作を開始しない
            std::cout << "Connect failed..." << std::endl;
        }
    }

    // send完了ハンドラ
    void send_handler(const error_code_t &err, const size_t bytes_transferred)
    {
        if (!err)
        {
            std::cout << "Send succeeded!!! Send " 
                        << bytes_transferred << "bytes" << std::endl;
        }
        else
        {
            std::cout << "Send failed..." << std::endl;
        }
    }
};

class Server
{
    asio::io_context &io_context_;
    tcp::acceptor acceptor_;
    tcp::socket socket_;
    std::string buff_;

public:
    Server(asio::io_context &io_context, const short port)
        : io_context_(io_context),
          acceptor_(io_context, tcp::endpoint(tcp::v4(), port)),
          socket_(io_context), buff_(){}

    // accept開始
    void start_accept()
    {
        acceptor_.async_accept(
            socket_,
            [this](const error_code_t &error)
            {
                accept_handler(error);
            });
    }

private:
    // accept完了ハンドラ
    void accept_handler(const error_code_t &error)
    {
        if (!error)
        {
            std::cout << "Accept and connection established !!!" << std::endl;
            start_receive();        // acceptが完了したので、recvを開始する
        }
        else
        {
            // 失敗した場合は次の操作を開始しない
            std::cout << "Failed to establish connection..." << std::endl;
        }
    }

    // メッセージのrecv開始
    void start_receive()
    {
        // バッファをクリア
        buff_.clear();
        buff_.resize(1024);
        // 非同期receiveの開始
        socket_.async_receive(
            asio::buffer(buff_.data(), buff_.size()),
            [this](const error_code_t &error, const size_t bytes_transferred)
            {
                receive_handler(error, bytes_transferred);
            });
    }

    // recv完了ハンドラ
    void receive_handler(const error_code_t &error, const size_t bytes_transferred)
    {
        if (!error)
        {
            std::cout << "Receive succeeded!!! Read " << bytes_transferred 
                        << " bytes." << std::endl;
            std::cout << "Recv data ===> " << buff_ << std::endl;
        }
        else
        {
            std::cout << "Receive failed..." << std::endl;
        }
    }
};

int main(int argc, const char **argv)
{
    if (argc < 2)
    {
        std::cout << "usage: ./tcp_sample port_number" << std::endl;
        return -1;
    }
    const int port = std::stoi(argv[1]);
    asio::io_context io_context;
    asio::signal_set sig(io_context,SIGINT);  //SIGINTで終了するため

    Server server(io_context, port);
    server.start_accept(); // accept開始

    Client client(io_context, port);
    client.start_connect(); // connect開始

    auto guard = asio::make_work_guard(io_context);   //io_contextの追加設定
    sig.async_wait([&guard](auto err,auto num){guard.reset();});
    auto th = std::async(std::launch::async, [&io_context]()
                         { io_context.run(); });

    th.get();
    return 0;
}

UDP

UDPはTCPとほとんど同じです。違いはacceptorが存在しないことくらいです。ソケットであるip::udp::socketとエンドポイントを表すip::udp::endpointやリゾルバクラスip::udp::resolver等があります。他のI/Oクラスと同じように、非同期・同期の両方の操作をサポートしています。

エンドポイントはほぼ完全にTCPと同じです。UDPの場合は、asio::ip::udp::endpointを利用します。

// anyアドレスのエンドポイントを構築
asio::ip::udp::endpoint ep_v4(asio::ip::udp::v4(), 7650);
asio::ip::udp::endpoint ep_v6(asio::ip::udp::v6(), 3460);
// 特定のIPアドレスのエンドポイントを構築
asio::ip::udp::endpoint ep_v4_2(asio::ip::make_address_v4("192.168.5.123"), 7650);
asio::ip::udp::endpoint ep_v6_2(asio::ip::make_address_v6("::1"), 3460);

こちらはudpですので当然接続処理を行う必要はありません。しかし、場合によってはソケットのバインドを行う必要があります。bindはbindメソッドを利用するか、コンストラクタにエンドポイントを渡すことで行えます。

asio::io_context io_context;
asio::ip::udp::endpoint ep_v4(asio::ip::udp::v4(), 7650);

asio::ip::udp::socket socket_v4(io_context,ep_v4); //ep_v4にbindして構築
asio::ip::udp::socket socket_v4_2(io_context); //bindせずに構築
socket_v4_2.bind(ep_v4); //後でep_v4にbind

ソケットの読み書きは、async_receiveasync_send_toを利用します。フリー関数の項目でも書きましたが、こちらはTCPと違って○○_some系のメソッドを持っておらず、receivesend系だけです。その代わり、async_send_toasync_receive_fromメソッドを持っています。これらは、送信先や受信元のエンドポイントを指定して送受信を行うメソッドです。単なるreceive系とsend系のメソッドは、connectasync_connectを行った後に利用できるメソッドです。connectと言ってもTCPのように接続する訳ではないですが、接続先のエンドポイントを先に指定する事で毎回送受信先を指定する必要がなくなります。

【利用例】

// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
void handler(
  const asio::error_code& error, 
  std::size_t bytes_transferred // 転送したバイト数
)
{
  if(!error)
  {
    // 転送成功
  }
  else
  {
    // 転送失敗
  }
}
// 相手のエンドポイント
asio::ip::udp::endpoint destination(
    asio::ip::address::from_string("1.2.3.4"), 12345);
// 非同期で相手を指定して送信
socket.async_send_to(
    asio::buffer(data, size), destination, handler);

// 相手のエンドポイント
asio::ip::udp::endpoint sender_endpoint(
    asio::ip::address::from_string("1.2.3.4"), 12345);
// 非同期で相手を指定して受信
socket.async_receive_from(
   asio::buffer(data, size), sender_endpoint, handler);

// 同期で相手を指定して受信
socket.receive_from(
    asio::buffer(data, size), sender_endpoint);
// 同期で相手を指定して送信
socket.send_to(
    asio::buffer(data, size), destination);

// あらかじめ接続先を指定しておく
socket.connect(destination);
// 接続先を指定せずに送受信可能になる
socket.send(asio::buffer(data, size));
socket.receive(asio::buffer(data, size));
socket.async_send(asio::buffer(data, size), handler);
socket.async_receive(asio::buffer(data, size), handler);
// receiveの場合は、bindさえしていればconnectや相手の指定をせずに受信することもできる

ソケットには、オプションを設定することもできます。 以下は設定可能なオプションの一部です。一覧はUDPソケットクラスのドキュメントを参照してください。

【利用例】

// ソケットのオプションを設定する
asio::ip::udp::socket socket(my_context);
asio::socket_base::linger option(true, 30);
socket.set_option(option);

こちらも、名前解決をしたい場合は、resolverクラスを利用します。本記事では紹介しませんが、boostjpなどで利用方法が紹介されています。

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

File I/O

ファイルI/Oに関する非同期操作も用意されています。ストリーム(asio::stream_file)とランダムアクセス(asio::random_access_file)のファイルAPIが用意されています。pipeへの書き込み(basic_writable_pipe)と読み込み(basic_readable_pipe)もありますが、ここでは紹介しません。基本的には、フリー関数のI/O関数や、ソケット等とほぼ同じAPIでI/Oを行う事ができます。ファイルを開けば後は同じであるため、ここではファイルを開く方法だけ紹介します。

【ファイル操作の例】

asio::io_context io_context;
// 書き込み専用ファイルを開く
asio::stream_file write_file(io_context,"path/to/file",
                        asio::stream_file::write_only   // 書き込み専用
                        | asio::stream_file::create     // ファイルが存在しない場合は新規作成
                        | asio::stream_file::truncate); // ファイルを空にして開く

// 読み込み専用ファイルを開く
asio::stream_file read_file(io_context,"path/to/file",
                        asio::stream_file::read_only); // 読み込み専用

// 上記以外には以下の設定が存在する
// append              追記モードで開く
// exclusive           新しいファイルを必ず作成する。createと同時に利用する。
// read_write          読み書きモードで開く
// sync_all_on_write   書き込み操作によってファイルのデータとメタデータが自動的にディスクに同期されるようにする。

// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
auto token = [](asio::error_code ec,size_t bytes_transferred){};
std::string data = "Hello file";
// 非同期で書き込みを行う
write_file.async_write_some(asio::buffer(data.data(),data.size()),token);

std::vector<char> buf(128);
// 非同期で読み込みを行う
read_file.async_read_some(asio::buffer(buf),token);

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

タイマー

Asioにはタイマーも用意されています。タイマーにはdeadline_timerhigh_resolution_timersteady_timersystem_timerの4種類があります。これらは、参考にするクロックが違うタイマーです。それぞれ順番に、boost::posix_timestd::chrono::high_resolution_clockstd::chrono::steady_clockstd::chrono::system_clockがタイマーの内部で利用されるクロックです。基本的にどのタイマーも同じメソッドを持っています。

【タイマーの操作例】

asio::io_context ioc;
// タイマーの構築
asio::steady_timer timer(ioc);

// 相対時間でタイマーを設定
timer.expires_after(std::chrono::seconds(5));

// 絶対時間でタイマーを設定
timer.expires_at(std::chrono::steady_clock::now() + std::chrono::seconds(2));

auto token = [](asio::error_code ec){std::cout << "Timer expired !!!" << std::endl;};
// 非同期に待機し、時間が経過するとハンドラが実行されるように設定
timer.async_wait(token);
// 他と同じようにrun()しないとハンドラは実行されないので注意
ioc.run()
// 時間になるまで同期的に待機する
timer.wait();

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

シグナルハンドリング

シグナルハンドリングはasio::signal_setを利用すると実現できます。使い方としては、キャッチしたいシグナルを設定し、その後async_waitするという単純なものです。エグゼキューターの設定にもよると思いますが、恐らく基本的にはハンドラの中でasync-signal-safeを意識しなくても平気だと思われます。(ドキュメントにはasync-signal-safeに関する一切の記載がありません。もしかするとエグゼキューターにasio::execution::blocking.alwaysを設定していると危ないかもしれない?)

【シグナルハンドリングの例】

// ハンドラ。sig_numにはキャッチした番号が渡される。
// Boost版の場合はasio::error_codeの代わりにboost::system::error_codeを使う
auto handler = [](asio::error_code ec,int sig_num){
    if(!ec)
    {
      // シグナルを適切にキャッチした場合にここに入る
    }
};
asio::io_context io_context;
// SIGINTをキャッチするように設定
asio::signal_set signals(io_context,SIGINT);
// キャッチ対象を複数指定する事も可能
asio::signal_set many_signals(io_context,SIGINT,SIGTERM);
// 非同期に待機する
signals.async_wait(handler);

本項目に関連する公式のサンプルコード

Asio

Boost.Asio

その他の機能

本項目では、上記までで紹介できなかった機能をまとめて簡単に紹介します。

SSL
AsioにはSSLサポートのためのクラスが実装されています。これらを利用すると、TCPソケット等のストリームに暗号化通信を構築できます。この機能を利用するにはOpenSSLが必要です。  

関連リファレンス

シリアルポート Asioにはシリアルポートを扱うためのクラスも用意されています。これを利用すると、シリアルポートに対して非同期・同期I/Oを行う事ができます。シリアルデバイスを開けば、Asioの典型的なI/O操作が簡単に利用できます。RS485やRS232などの多くのプロトコルに対応しています。ちなみに筆者はJetsonからロボットのシリアルサーボを制御するのにこちらの機能をよく利用しています。

関連リファレンス

Windows向け機能 本記事でUnix domainソケットがあると紹介しましたが、Windows固有の機能に関するクラスも少し実装されています。オブジェクトハンドルに関連するクラスがいくつかあります。

関連リファレンス

(番外編) GENERATING_DOCUMENTATION マクロ

Asio(Boost.Asio)のソースコードを読んでいるとGENERATING_DOCUMENTATIONというマクロが度々登場します。このマクロの典型的な例は少し長いですが以下のようなものです。

#if defined(GENERATING_DOCUMENTATION)

template <typename CompletionToken,
    completion_signature... Signatures,
    typename Initiation, typename... Args>
void_or_deduced async_initiate(
    Initiation&& initiation,
    type_identity_t<CompletionToken>& token,
    Args&&... args);

#else // defined(GENERATING_DOCUMENTATION)

template <typename CompletionToken,
    ASIO_COMPLETION_SIGNATURE... Signatures,
    typename Initiation, typename... Args>
inline auto async_initiate(Initiation&& initiation,
    type_identity_t<CompletionToken>& token, Args&&... args)
  -> constraint_t<
    detail::async_result_has_initiate_memfn<
      CompletionToken, Signatures...>::value,
    decltype(
      async_result<decay_t<CompletionToken>, Signatures...>::initiate(
        static_cast<Initiation&&>(initiation),
        static_cast<CompletionToken&&>(token),
        static_cast<Args&&>(args)...))>
{
  return async_result<decay_t<CompletionToken>, Signatures...>::initiate(
      static_cast<Initiation&&>(initiation),
      static_cast<CompletionToken&&>(token),
      static_cast<Args&&>(args)...);
}

上記を見ると、同じような宣言がマクロによって2つ場合分けしてあることがあります。実は、このマクロが有効な方は説明用の宣言で、ドキュメント生成の際のみ有効になるものです。つまり、実際の宣言はelse節の方に書かれていることになります。ですので、簡単にシグネチャを知りたい場合などはマクロが有効な方の宣言、実際の実装を追っていきたい場合はelse節の方を読むことをおすすめします。ちなみに筆者は昔このマクロの意味が分からず、わざわざboostのslackで質問してしまいました。

Asio(またはBoost.Asio)を利用したOSS・サンプルコードや参考になるwebページ等

本連載ではこれまでいくつかのサンプルコードを掲載してきましたが、参考になるコードは多ければ多いほど良い(?)ので筆者が見つけたリポジトリやwebページを以下にいくつか列挙します。参考になれば幸いです。

OSS等のリポジトリ

サンプルコードが掲載されたwebページ

公式リファレンスで読むと理解が深まるページ


*1:このスタックフルコルーチンはBoost.Coroutineがベースとなっています