- はじめに
- エグゼキューター関連
- 完了トークンアダプター(Completion Token Adapters)
- エグゼキューターにタスクを渡す方法
- バッファ
- I/O用のフリー関数
- ネットワーク
- File I/O
- タイマー
- シグナルハンドリング
- その他の機能
- (番外編) GENERATING_DOCUMENTATION マクロ
- Asio(またはBoost.Asio)を利用したOSS・サンプルコードや参考になるwebページ等
執筆者:井上 叡
監修者:稲葉 貴昭・高橋 浩和
※ 「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_guard
やstrand
などを直接利用する方が便利な場合が多いですが、一応紹介しておきます。
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
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp14_examples.html
- ページ内のExecutorsの項目にあるサンプルコード類で、本項目の内容を利用しています。他にも色々なサンプルで利用されています。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp14_examples.html
- ページ内のExecutorsの項目にあるサンプルコード類で、本項目の内容を利用しています。他にも色々なサンプルで利用されています。
完了トークンアダプター(Completion Token Adapters)
完了トークンアダプター(Completion Token Adapters)は、完了トークンに対して追加の設定のようなものを付与するために利用するものです。関連特性の紹介をした記事などで出てきた、asio::bind_executor
などがこれに該当します。
完了トークンアダプターの主な用途は以下の2つです。
- 完了トークンに対して何らかの特性を付与して新たな完了トークンを生成
- 完了シグネチャや、完了ハンドラに渡される引数を変更する
1.の用途としてはbind_executor、bind_allocator、bind_cancellation_slotとbind_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_tuple、append、prepend、consignなどがあります。
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_allocator、asio::get_associated_cancellation_slot、asio::get_associated_executor、asio::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_context
やasio::ip::tcp::socket
、asio::steady_timer
などのオブジェクトから結びつけられたエグゼキューターを取得したい場合は、それぞれのオブジェクトが持つget_executor()
メソッドを利用します。それらに対してはasio::get_associated_executor
は残念ながら使えません。
本項目に関連する公式のサンプルコード
Asio
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp14_examples.html
- Operationsにあるサンプルコード類では、get_associated_executorなどを利用しています。その他の多くのサンプルコードでも本項目の機能は利用されています。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp14_examples.html
- Operationsにあるサンプルコード類では、get_associated_executorなどを利用しています。その他の多くのサンプルコードでも本項目の機能は利用されています。
エグゼキューターにタスクを渡す方法
エグゼキューターは、非同期操作の完了ハンドラを処理するだけでなく、勿論ユーザーが渡したタスクを実行するためにも利用できます。最も分かりやすい利用例としては、スレッドプールであるエグゼキューターのasio::thread_pool
に対して、何らかの重いタスクを渡してバックグラウンドの別スレッドで処理をするなどがあります。ここでは、何らかの関数オブジェクトをエグゼキューターに渡して実行してもらう方法をいくつか紹介します。代表的なフリー関数は、asio::post、asio::dispatch、asio::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_context
やasio::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の項目にあるサンプルコード類では、post
やdispatch
、defer
などを利用しています。その他の多くのサンプルコードでも本項目の機能は利用されています。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
- ページ内のExecutorsの項目にあるサンプルコード類では、
post
やdispatch
、defer
などを利用しています。その他の多くのサンプルコードでも本項目の機能は利用されています。
- ページ内のExecutorsの項目にあるサンプルコード類では、
バッファ
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_bufferとasio::mutable_bufferとして定義されています。変更可能なものは主にデータの受信・読み取り用、変更不可のものは主にデータの送信・書き込みに利用します。
また、上記はScatter-Gather I/Oに利用することもできます。上記の2種類のバッファを、std::vector
、std::list
、std::array
、boost::array
など、std::
bidirectional_iterator<>をもつコンテナに格納することでscatter/gather操作用のバッファ(ConstBufferSequenceとMutableBufferSequence)として利用することができます。
バッファは、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_buffer
とasio::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_v1とDynamicBuffer_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
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html
- ページ内のBuffers、Chat、Echoなどの項目のサンプルコード類でバッファが利用されています。その他の多くのサンプルコードでも本項目の機能は利用されています。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
- ページ内のBuffers、Chat、Echoなどの項目のサンプルコード類でバッファが利用されています。その他の多くのサンプルコードでも本項目の機能は利用されています。
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_buffer
とmutable_buffer
とそれのScatter-Gather用であるConstBufferSequence
とMutableBufferSequence
、加えて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
は、読み込みや書き込みの完了条件を指定するためのもので、以下のようなものがあります。
- transfer_all(https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/transfer_all.html) 全てのデータが転送されるまで、もしくはエラーが発生するまで、読み取り/書き込み動作を続行する。バッファが満杯になるまで転送するなどが可能。
- transfer_at_least(https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/transfer_at_least.html) 最小バイト数が転送されるか、もしくはエラーが発生するまで、読み取り/書き込み動作を続行する。
- transfer_exactly(https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/transfer_exactly.html) 指定したバイト数ぴったりの長さが転送されるまで、もしくはエラーが発生するまで読み取り/書き込み動作を続行する。対象の操作で必ず指定したバイト数読んでほしい場合などに利用できる。
ソケットクラスが持っている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
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html
- ページ内のChat、Porthopperなどの項目のサンプルコード類でフリー関数が利用されています。その他の多くのサンプルコードでも本項目の機能は利用されています。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
- ページ内のChat、Porthopperなどの項目のサンプルコード類でフリー関数が利用されています。その他の多くのサンプルコードでも本項目の機能は利用されています。
ネットワーク
ネットワークに関連する操作はある意味Asioのメインコンテンツとも言えるもので、豊富に機能があります。ここでは全ては紹介できませんが、代表的なものをいくつか紹介します。プロトコルはTCP、UDP、ICMPの3種に関する機能がデフォルトで用意されています。また、それに加えてプラットフォームによってはUnixドメインソケットに関するAPIも実装されています。
Asioのネットワーク機能は、主にasio::ip::
とasio::generic::
という2つの名前空間に分かれています。基本となるのは、asio::ip::
の名前空間内にあるもので、こちらは更にip::icmp::
、ip::tcp
、ip::udp
、ip::address_v4
などに分かれています。それらでは名前のプロトコルに対するソケットやアドレスが提供されています。asio::generic
名前空間ではジェネリックなAPIが用意されています。
公式にはネットワークに関する豊富なサンプルがあります。 Asio版
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp14_examples.html
Boost版
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp14_examples.html
アドレス
asio::ip::addressはipアドレスを扱うためのクラスです。ipv4のアドレスを表すクラス(asio::ip::address_v4)とipv6アドレスを表すクラス(asio::ip::address_v6)もあります。address
からはaddress_v4
とaddress_v6
への変換が可能です。また、ipv4のネットワークを表す事ができるip::network_v4、ip::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_acceptとacceptメソッドは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_connectかconnectメソッドを利用して接続要求をします。
【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_some
とreceive
、書き込みはwrite_some
とsend
の2種類が用意されていますが、それらはそれぞれほぼ同じ動作をします。しかし基本的には、TCPソケットではread_some
とwrite_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_receive
やasync_send_to
を利用します。フリー関数の項目でも書きましたが、こちらはTCPと違って○○_some
系のメソッドを持っておらず、receive
とsend
系だけです。その代わり、async_send_toやasync_receive_fromメソッドを持っています。これらは、送信先や受信元のエンドポイントを指定して送受信を行うメソッドです。単なるreceive
系とsend
系のメソッドは、connectかasync_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
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html
ページ内のEcho、Multicast、ICMPなどの項目に本項目の内容を利用しているサンプルコード類があります。
https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp14_examples.html
- ページ内のEchoなどの項目に本項目の内容を利用しているサンプルコード類があります。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
ページ内のEcho、Multicast、ICMPなどの項目に本項目の内容を利用しているサンプルコード類があります。
https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp14_examples.html
- ページ内のEchoなどの項目に本項目の内容を利用しているサンプルコード類があります。
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
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html
- ページ内のFilesの項目に本項目の内容を利用しているサンプルコード類があります。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
- ページ内のFilesの項目に本項目の内容を利用しているサンプルコード類があります。
タイマー
Asioにはタイマーも用意されています。タイマーにはdeadline_timer、high_resolution_timer、steady_timer、system_timerの4種類があります。これらは、参考にするクロックが違うタイマーです。それぞれ順番に、boost::posix_time
、std::chrono::high_resolution_clock
、std::chrono::steady_clock
、std::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
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html
- ページ内のInvocation、Operations、Timeoutsなどの項目に本項目の内容を利用しているサンプルコード類があります。
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp20_examples.html
- ページ内のInvocation、Operations、Coroutinesなどの項目に本項目の内容を利用しているサンプルコード類があります。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
- ページ内のInvocation、Operations、Timeoutsなどの項目に本項目の内容を利用しているサンプルコード類があります。
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp20_examples.html
- ページ内のInvocation、Operations、Coroutinesなどの項目に本項目の内容を利用しているサンプルコード類があります。
シグナルハンドリング
シグナルハンドリングは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
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp11_examples.html
- 本項目のHTTP Serverなどの項目に本項目の内容を利用しているサンプルコード類があります。
- https://think-async.com/Asio/asio-1.30.2/doc/asio/examples/cpp20_examples.html
- 本項目のCoroutinesなどの項目に本項目の内容を利用しているサンプルコード類があります。
Boost.Asio
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp11_examples.html
- 本項目のHTTP Serverなどの項目に本項目の内容を利用しているサンプルコード類があります。
- https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/examples/cpp20_examples.html
- 本項目のCoroutinesなどの項目に本項目の内容を利用しているサンプルコード類があります。
その他の機能
本項目では、上記までで紹介できなかった機能をまとめて簡単に紹介します。
SSL
AsioにはSSLサポートのためのクラスが実装されています。これらを利用すると、TCPソケット等のストリームに暗号化通信を構築できます。この機能を利用するにはOpenSSLが必要です。
関連リファレンス
シリアルポート Asioにはシリアルポートを扱うためのクラスも用意されています。これを利用すると、シリアルポートに対して非同期・同期I/Oを行う事ができます。シリアルデバイスを開けば、Asioの典型的なI/O操作が簡単に利用できます。RS485やRS232などの多くのプロトコルに対応しています。ちなみに筆者はJetsonからロボットのシリアルサーボを制御するのにこちらの機能をよく利用しています。
関連リファレンス
Windows向け機能 本記事でUnix domainソケットがあると紹介しましたが、Windows固有の機能に関するクラスも少し実装されています。オブジェクトハンドルに関連するクラスがいくつかあります。
関連リファレンス
- Windows固有機能の概要
- asio::windows::object_handle
- asio::windows::stream_handle
- asio::windows::random_access_handle
- asio::windows::overlapped_handle
(番外編) 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等のリポジトリ
- Asioを活用したMQTTクライアントライブラリ
- Boost.Beast (HTTTPやwebsocketのライブラリ、Boost.Asioを利用している)
- Asio公式が出しているAsioを利用しているソフトウェアのリスト
サンプルコードが掲載されたwebページ
- 公式のチュートリアル
- 公式のサンプルコード(オススメ!!)
- Asioの使い方をまとめている
- Boostjpのドキュメント
- AsioをC++11で活用する
- C++20でAsioを利用する方法を説明したビデオ
- Asio作者が作成したExecutorライブラリの説明スライド(AsioのExecutorと全く同じではないが、理解に役立つ)
- その他色々なwebページ・ドキュメント
- https://amedama1x1.hatenablog.com/entry/2014/09/20/102155
- https://amedama1x1.hatenablog.com/entry/2015/12/14/000000_1
- https://cppalliance.org/richard/2020/03/31/RichardsMarchUpdate.html
- https://myon.info/blog/2015/04/19/boost-asio-serial/
- https://myon.info/blog/2018/10/14/boost-asio-posix-stream_descriptor/
- https://nanxiao.gitbooks.io/boost-asio-network-programming-little-book/content/
- https://qiita.com/YukiMiyatake/items/5be12ea35894071d8de1
- https://qiita.com/YukiMiyatake/items/8f2ef7bd4e003629828c
- https://qiita.com/h_yasunori/items/a4e75507f72dead183f8
- https://qiita.com/legokichi/items/3365b25eea13c0f2bb51
- https://speakerdeck.com/redboltz/boost-dot-asioniokerucoroutinenohuo-yong-fa
- https://www.bit-hive.com/articles/20210226
- https://www.slideshare.net/slideshow/boost-asio/48240202#1
- https://zenn.dev/redboltz
- https://zenn.dev/redboltz/articles/net-cpp-slcoro-multi-wait2
公式リファレンスで読むと理解が深まるページ
- 各要素の解説
型の要件
- 公式リファレンス内の Type Requirements という項目内のページはある型の仕様を把握するのに役立ちます。大体読んでおくと利用する際に悩むことが少なくなると思います。
- 非同期操作の要件
- キャンセルスロットの要件
- エグゼキューターの要件
- ExecutionContextの要件
マクロ一覧
*1:このスタックフルコルーチンはBoost.Coroutineがベースとなっています