asio 01
はじめに
boost の asio がいつの間にか バージョン 1.0 になっているじゃないか。
実際にソケットを使うプログラムでどのくらい使えるのかがわからなかったので、少し試してみることにした。
まずは、名前解決からだ。
うんちく
名前解決を侮ってはいけない。もしもあなたが HTTP クライアントプログラムを書くとしよう。もちろん、アクセス先のサーバは落ちる(頻発しないかもしれないが、必ず落ちるだろう)。そういったことを考慮して、サーバ側では可用性を高めるための手段が講じられる。そのひとつとして、DNS ラウンドロビンがある。DNS ラウンドロビンは、ひとつのドメイン名に複数の IP アドレスを割り当てることによって実現される。
あなたが書いた HTTP クライアントがそういったことを考慮していないと、具体的には名前解決の結果として複数の IP アドレスが返ってくることを考慮していないと、サービス提供側の努力もむなしく、うまくアクセスできないことになる。
こういったことを考慮しておけば、結果としてサービス提供側にやさしく、HTTP クライアントプログラムの利用者にも優しいプログラムを書くことができる。
なぜこういったことを書いたかというと、名前解決を実現するための関数である getaddrinfo や gethostbyname を使ったサンプルの多くは
ひとつの IP アドレスが返ってくることしか考慮されていないことが多いからだ。(ちなみにこの後出てくる asio のサンプルコードではちゃんと考慮されている、すばらしい)気持ちはわかる。確かにちゃんとコードを書くのは面倒だ。だいたいソケット関数の多くは厳格に書かなくても、ネットワークの機嫌がよければおおよそうまく動いてしまう。しかし、ネットワークの機嫌がよくないと途端に牙をむき出す。最終的に、あなたはもっと面倒なことになる。
前置きが長くなった。では、asio を使った名前解決のコードを見てみよう。名前解決に特化したチュートリアルやサンプルが見つけられなかったので、同期的に名前解決をするコードを書いてみた。
サンプルコード
asio はマルチプラットフォームに対応したポータブルなライブラリだが、今回は私の好きな Windows 環境で動かすことを想定している。
#define _WIN32_WINNT 0x400 #include <iostream> #include <boost/array.hpp> #include <asio.hpp> using asio::ip::tcp; int main( int argc, char* argv[] ) { try { if ( argc != 3 ) { std::cerr << "Usage: resolver <host> <service>" << std::endl; return 1; } asio::io_service io_service; tcp::resolver resolver( io_service ); tcp::resolver::query query( argv[1], argv[2] ); tcp::resolver::iterator endpoint_iterator = resolver.resolve( query ); tcp::resolver::iterator end; // print query info. std::cout << query.host_name() << std::endl; std::cout << query.service_name() << std::endl; std::cout << std::endl; while ( endpoint_iterator != end ) { // print endpoint info. tcp::endpoint endpoint = endpoint_iterator->endpoint(); std::cout << " capacity: " <<endpoint_iterator->endpoint().capacity() << std::endl; std::cout << " data : " <<endpoint_iterator->endpoint().data() << std::endl; std::cout << " port : " <<endpoint_iterator->endpoint().port() << std::endl; std::cout << " size : " <<endpoint_iterator->endpoint().size() << std::endl; // print address info. asio::ip::address address = endpoint.address(); std::cout << " address : " << address.to_string() << std::endl; std::cout << " is_v4 : " << address.is_v4() << std::endl; std::cout << " is_v6 : " << address.is_v6() << std::endl; std::cout << std::endl; endpoint_iterator++; } } catch ( std::exception& e ) { std::cerr << e.what() << std::endl; } return 0; }
argv の内容チェックはしていないが、それは勘弁していただきたい。
冒頭部のプリプロセッサの指定は、Windows 2000 以下でも asio がうまく動くための指定だ。Windows 2000 以降では IPv6 がサポートされはじめ、gethostbyname ではなく getaddrinfo という API が登場した。ただし、これは古い環境では利用することができない。しかし、この指定をしておくと、利用できれば getaddrinfo を、そうでなければ gethostbyname を自動的に選択してくれる。ちなみに、コードを追って見たところ、この自動選択は多少問題があるように思える。
ちなみに、resolver.resolve() というところで getaddrinfo は gethostbyname が実行され、結果は内部に保存され、endpoint_iterator 経由で結果にアクセスできるという仕組みになっているので、このコードではそこ以外に通信が絡むところはない。
まとめ
asio による名前解決は、すっきりきれいにコードが書けるのでとてもよさそうだ。
懸念点
最後に、実際にコードを追ってみて気になったことを挙げておく。
- _WIN32_WINNT という Windows プログラムにとって重要なプリプロセッサを指定しないと使えないということ
- これはちょっと残念だった
- getaddrinfo のアドレスを DLL から取得する際の対象が、ws2_32.dll のみであるということ
- 一部の Windows 2000 では wshipv6.dll にしかない
- これは PostgreSQL にも実装されている個人的には有名なロジックである
- 自力でゴリゴリ実装したときよりもバイナリサイズがいくらか大きくなる
- asio に限らず boost 全般に共通している
この 3 点を気にしなくてもいいプログラムであれば、問題なく使えるだろう。
asio 02
はじめに
前回作った名前解決をするコードを、asio の真価を発揮させて非同期にしてみよう。
うんちく
もしあなたが名前解決をするコードを asio を使わずに getaddrinfo や gethostbyname を使って書いたことがあるのなら、以下のようなことをご存じかもしれない。
- getaddrinfo、gethostbyname は connect、send、recv などの関数と違って非同期にすることができないということ
- gethostbyname が返すバッファの扱いには十分注意が必要
ちなみに、gethostbyname に関しては既によく論じられている。
- Geekなぺーじ : gethostbynameの落とし穴
- Manpage of GETHOSTBYNAME
- gethostbyname macro (wsipv6ok.h) | Microsoft Docs
対策状況はそれぞれ以下のようになっているようだ。
- getaddrinfo、gethostbyname は connect、send、recv などの関数と違って非同期にすることができないということ
- 内部でワーカースレッドを作りそこから呼び出す
- よくある手なのでやっぱりそうだよね、という感想
- 内部でワーカースレッドを作りそこから呼び出す
- gethostbyname が返すバッファの扱いには十分注意が必要
サンプルコード
#define _WIN32_WINDOWS 0x0400 #define _WIN32_WINNT 0x400 #include <iostream> #include <boost/array.hpp> #include <asio.hpp> using asio::ip::tcp; void on_resolve( const asio::error_code& error, tcp::resolver::iterator endpoint_iterator ) { if ( ! error ) { tcp::resolver::iterator end; while ( endpoint_iterator != end ) { tcp::endpoint endpoint = endpoint_iterator->endpoint(); std::cout << " capacity: " <<endpoint_iterator->endpoint().capacity() << std::endl; std::cout << " data : " <<endpoint_iterator->endpoint().data() << std::endl; std::cout << " port : " <<endpoint_iterator->endpoint().port() << std::endl; std::cout << " size : " <<endpoint_iterator->endpoint().size() << std::endl; // print address info. asio::ip::address address = endpoint.address(); std::cout << " address : " << address.to_string() << std::endl; std::cout << " is_v4 : " << address.is_v4() << std::endl; std::cout << " is_v6 : " << address.is_v6() << std::endl; std::cout << std::endl; endpoint_iterator++; } } else { std::cerr << error.message() << std::endl; } } int main( int argc, char* argv[] ) { try { if ( argc != 3 ) { std::cerr << "Usage: resolver <host> <service>" << std::endl; return 1; } asio::io_service io_service; tcp::resolver resolver( io_service ); tcp::resolver::query query( argv[1], argv[2] ); // print query info. std::cout << query.host_name() << std::endl; std::cout << query.service_name() << std::endl; std::cout << std::endl; // start to resolve. resolver.async_resolve( query, on_resolve ); io_service.run(); } catch ( std::exception& e ) { std::cerr << e.what() << std::endl; } return 0; }
まとめ
asio の名前解決は、名前解決関数にまつわる問題を回避して安全に使うことができる。
それと、ソケット関数と asio の対応表があったので必要に応じて参考にすること。
懸念点
今回は特になし。
アプリケーション設計のガイドライン
名前解決によって複数のエンドポイントを取得できたときの振る舞いをどうするか
- 最初の一つだけを処理対象とする(非推奨)
- 全てを処理対象とする(推奨)
非同期で名前解決を行った場合にどれだけ待機するか
- 名前解決処理が何らかの処理結果を返すまで待機する(推奨)
- アプリケーションが定めた時間だけ待つ(非推奨)
- ネットワークの状態が思わしくないときに反応が遅いからといって単純に待機時間を短縮すればいいというわけではない
- ソフトウェアファイアウォールによって警告ダイアログが出ているだけの場合もある
- 名前解決に関する詳しく知識がない場合は極力これを避けること
- その場合処理結果が帰ってくるまでの時間は OS の実装に依存する
- 名前解決処理が何らかの処理結果を返すのを待機しつつユーザからキャンセル指示があった場合は即終了する
- かといってすぐにプログラムを終了してはいけない
- 名前解決処理は途中で中断できない
- ユーザからキャンセル指示があった場合は「終了中」状態に移行しすべての処理が正常に終わるまで待機してからプログラムを終了する
複数の IP アドレスからどれを選択するか
次回に向けて
さぁ次はソケットを作っていよいよ通信といきたいところだが、それはサンプルが豊富なので割愛し、非同期を扱う上で欠かすことのできない io_service について取り上げる。
asio 03
はじめに
今回はコードを非同期化する上で肝となる io_service について取り上げる。
うんちく
前回までサンプルコードについて一切解説を入れていなかったが、前回密かに以下のような呼び出しを行っている。
io_service.run();
これを呼び出すと、何が起こるのだろうか。以下のことが起こる。
- 呼び出し前までにたまっていた非同期呼び出し処理が動き出す
- 非同期処理が終わるまで待つ
- イベントループが実行される
なので、非同期処理をいくら呼び出しても、io_service::run が実行されないと何も起こらない。逆に、非同期処理を呼び出す前に io_service::run が実行されてしまうとすぐに処理が完了してしまう。前回のコードから、io_service::run の行をコメントアウト実行してみるとよく分かるはずだ。さらにいうなら、コメントアウトしたすぐ下に数秒間待機するコード(Windows なら Sleep など)を書くとさらに納得できるだろう。
- 非同期処理を呼び出した後実際の処理が完了せずにすぐに終了してしまうコード
#define _WIN32_WINDOWS 0x0400 #define _WIN32_WINNT 0x400 #include <iostream> #include <boost/array.hpp> #include <asio.hpp> using asio::ip::tcp; void on_resolve( const asio::error_code& error, tcp::resolver::iterator endpoint_iterator ) { if ( ! error ) { tcp::resolver::iterator end; while ( endpoint_iterator != end ) { tcp::endpoint endpoint = endpoint_iterator->endpoint(); std::cout << " capacity: " << endpoint_iterator->endpoint().capacity() << std::endl; std::cout << " data : " << endpoint_iterator->endpoint().data() << std::endl; std::cout << " port : " << endpoint_iterator->endpoint().port() << std::endl; std::cout << " size : " < <endpoint_iterator->endpoint().size() << std::endl; // print address info. asio::ip::address address = endpoint.address(); std::cout << " address : " << address.to_string() << std::endl; std::cout << " is_v4 : " << address.is_v4() << std::endl; std::cout << " is_v6 : " << address.is_v6() << std::endl; std::cout << std::endl; endpoint_iterator++; } } else { std::cerr << error.message() << std::endl; } } int main( int argc, char* argv[] ) { try { if ( argc != 3 ) { std::cerr << "Usage: resolver <host> <service>" << std::endl; return 1; } asio::io_service io_service; tcp::resolver resolver( io_service ); tcp::resolver::query query( argv[1], argv[2] ); // print query info. std::cout << query.host_name() << std::endl; std::cout << query.service_name() << std::endl; std::cout << std::endl; // start to resolve. resolver.async_resolve( query, on_resolve ); //io_service.run(); } catch ( std::exception& e ) { std::cerr << e.what() << std::endl; } return 0; }
- 非同期処理を呼び出してからいくら待っても実際の処理が実行されないコード
#define _WIN32_WINDOWS 0x0400 #define _WIN32_WINNT 0x400 #include <iostream> #include <boost/array.hpp> #include <asio.hpp> using asio::ip::tcp; void on_resolve( const asio::error_code& error, tcp::resolver::iterator endpoint_iterator ) { if ( ! error ) { tcp::resolver::iterator end; while ( endpoint_iterator != end ) { tcp::endpoint endpoint = endpoint_iterator->endpoint(); std::cout << " capacity: " << endpoint_iterator->endpoint().capacity() << std::endl; std::cout << " data : " << endpoint_iterator->endpoint().data() << std::endl; std::cout << " port : " << endpoint_iterator->endpoint().port() << std::endl; std::cout << " size : " << endpoint_iterator->endpoint().size() << std::endl; // print address info. asio::ip::address address = endpoint.address(); std::cout << " address : " << address.to_string() << std::endl; std::cout << " is_v4 : " << address.is_v4() << std::endl; std::cout << " is_v6 : " << address.is_v6() << std::endl; std::cout << std::endl; endpoint_iterator++; } } else { std::cerr << error.message() << std::endl; } } int main( int argc, char* argv[] ) { try { if ( argc != 3 ) { std::cerr << "Usage: resolver <host> <service>" << std::endl; return 1; } asio::io_service io_service; tcp::resolver resolver( io_service ); tcp::resolver::query query( argv[1], argv[2] ); // print query info. std::cout << query.host_name() << std::endl; std::cout << query.service_name() << std::endl; std::cout << std::endl; // start to resolve. resolver.async_resolve( query, on_resolve ); //io_service.run(); Sleep( 60 * 1000 ); } catch ( std::exception& e ) { std::cerr << e.what() << std::endl; } return 0; }
そこで問題になるのが、マルチスレッドアプリケーションなどで io_service::run が実行される後にアプリケーションが非同期処理を行うにはどうするか、ということだ。これは、io_service::work というクラスを使うことで対応できる。
- 非同期処理を呼び出さなくてもずっと io_service::run がずっと待機してくれるコード
#define _WIN32_WINDOWS 0x0400 #define _WIN32_WINNT 0x400 #include <iostream> #include <boost/array.hpp> #include <asio.hpp> using asio::ip::tcp; void on_resolve( const asio::error_code& error, tcp::resolver::iterator endpoint_iterator ) { if ( ! error ) { tcp::resolver::iterator end; while ( endpoint_iterator != end ) { tcp::endpoint endpoint = endpoint_iterator->endpoint(); std::cout << " capacity: " << endpoint_iterator->endpoint().capacity() << std::endl; std::cout << " data : " << endpoint_iterator->endpoint().data() << std::endl; std::cout << " port : " << endpoint_iterator->endpoint().port() << std::endl; std::cout << " size : " << endpoint_iterator->endpoint().size() << std::endl; // print address info. asio::ip::address address = endpoint.address(); std::cout << " address : " << address.to_string() << std::endl; std::cout << " is_v4 : " << address.is_v4() << std::endl; std::cout << " is_v6 : " << address.is_v6() << std::endl; std::cout << std::endl; endpoint_iterator++; } } else { std::cerr << error.message() << std::endl; } } int main( int argc, char* argv[] ) { try { if ( argc != 3 ) { std::cerr << "Usage: resolver <host> <service>" << std::endl; return 1; } asio::io_service io_service; asio::io_service::work work( io_service ); tcp::resolver resolver( io_service ); tcp::resolver::query query( argv[1], argv[2] ); // print query info. std::cout << query.host_name() << std::endl; std::cout << query.service_name() << std::endl; std::cout << std::endl; // start to resolve. //resolver.async_resolve( query, on_resolve ); io_service.run(); } catch ( std::exception& e ) { std::cerr << e.what() << std::endl; } return 0; }
このコードでなぜ io_service::run がずっと待機してくれるのかというと、関連づけられた io_service::work が破棄されるまで待機してくれるという作りになっているからだ。これで、アプリケーションが非同期処理を呼び出すタイミングに関わらずに待機することができた。そうすると、今度は非同期処理が完了しても待機が終了しない、という状態になる。
- 非同期処理が完了しても io_service::work によっていつまでたっても終了しないコード
#define _WIN32_WINDOWS 0x0400 #define _WIN32_WINNT 0x400 #include <iostream> #include <boost/array.hpp> #include <asio.hpp> using asio::ip::tcp; void on_resolve( const asio::error_code& error, tcp::resolver::iterator endpoint_iterator ) { if ( ! error ) { tcp::resolver::iterator end; while ( endpoint_iterator != end ) { tcp::endpoint endpoint = endpoint_iterator->endpoint(); std::cout << " capacity: " << endpoint_iterator->endpoint().capacity() << std::endl; std::cout << " data : " << endpoint_iterator->endpoint().data() << std::endl; std::cout << " port : " << endpoint_iterator->endpoint().port() << std::endl; std::cout << " size : " << endpoint_iterator->endpoint().size() << std::endl; // print address info. asio::ip::address address = endpoint.address(); std::cout << " address : " << address.to_string() << std::endl; std::cout << " is_v4 : " << address.is_v4() << std::endl; std::cout << " is_v6 : " << address.is_v6() << std::endl; std::cout << std::endl; endpoint_iterator++; } } else { std::cerr << error.message() << std::endl; } } int main( int argc, char* argv[] ) { try { if ( argc != 3 ) { std::cerr << "Usage: resolver <host> <service>" << std::endl; return 1; } asio::io_service io_service; asio::io_service::work work( io_service ); tcp::resolver resolver( io_service ); tcp::resolver::query query( argv[1], argv[2] ); // print query info. std::cout << query.host_name() << std::endl; std::cout << query.service_name() << std::endl; std::cout << std::endl; // start to resolve. resolver.async_resolve( query, on_resolve ); io_service.run(); } catch ( std::exception& e ) { std::cerr << e.what() << std::endl; } return 0; }
これに対処するにはアプリケーションが終了を望んだタイミングで io_service::work を破棄する必要がある。
- 10 秒後にユーザがアプリケーションの終了を望んだと仮定して終了するコード
#define _WIN32_WINDOWS 0x0400 #define _WIN32_WINNT 0x400 #include <iostream> #include <boost/array.hpp> #include <boost/bind.hpp> #include <boost/thread.hpp> #include <asio.hpp> using asio::ip::tcp; void on_resolve( const asio::error_code& error, tcp::resolver::iterator endpoint_iterator ) { if ( ! error ) { tcp::resolver::iterator end; while ( endpoint_iterator != end ) { tcp::endpoint endpoint = endpoint_iterator->endpoint(); std::cout << " capacity: " << endpoint_iterator->endpoint().capacity() << std::endl; std::cout << " data : " << endpoint_iterator->endpoint().data() << std::endl; std::cout << " port : " << endpoint_iterator->endpoint().port() << std::endl; std::cout << " size : " << endpoint_iterator->endpoint().size() << std::endl; // print address info. asio::ip::address address = endpoint.address(); std::cout << " address : " << address.to_string() << std::endl; std::cout << " is_v4 : " << address.is_v4() << std::endl; std::cout << " is_v6 : " << address.is_v6() << std::endl; std::cout << std::endl; endpoint_iterator++; } } else { std::cerr << error.message() << std::endl; } } void app_thread( std::auto_ptr<asio::io_service::work>& work ) { Sleep( 10 * 1000 ); work.reset(); } int main( int argc, char* argv[] ) { try { if ( argc != 3 ) { std::cerr << "Usage: resolver <host> <service>" << std::endl; return 1; } asio::io_service io_service; std::auto_ptr<asio::io_service::work> work( new asio::io_service::work( io_service ) ); // run application thread. boost::thread t( boost::bind( app_thread, boost::ref( work ) ) ); tcp::resolver resolver( io_service ); tcp::resolver::query query( argv[1], argv[2] ); // print query info. std::cout << query.host_name() << std::endl; std::cout << query.service_name() << std::endl; std::cout << std::endl; // start to resolve. resolver.async_resolve( query, on_resolve ); io_service.run(); t.join(); } catch ( std::exception& e ) { std::cerr << e.what() << std::endl; } return 0; }
ここまで理解できたら、途中でユーザが終了を望んだときに速やかに終了することができるコードを書くことができる。上のサンプルコードでは、asio 以外に boost::thread、boost::bind、std::auto_ptr などが登場しているが、asio でうまくコードを書くにはどれも欠かすことのできないものだ(使わなくてもできるが困難を極めるだろう)。
なお、他にも io_service::run のイベントループを止める方法はいくつかある。たとえば、io_service::stop というものがあるが、これはイベントループに終了依頼をするだけで、待機処理は含まれていない。これは英語があまり理解できなくてもコードを見ても明らかである。Windows の場合以下に要になっている。
// Stop the event processing loop. void stop() { if (::InterlockedExchange(&stopped_, 1) == 0) { if (!::PostQueuedCompletionStatus(iocp_.handle, 0, 0, 0)) { DWORD last_error = ::GetLastError(); asio::system_error e( asio::error_code(last_error, asio::error::get_system_category()), "pqcs"); boost::throw_exception(e); } } }
ちなみに、io_service::reset というものがあるが、これはイベントループを止めるという目的で使ってはならない。ドキュメントには以下のようにある。
This function must not be called while there are any unfinished calls to the run(), run_one(), poll() or poll_one() functions.
この関数は、run()、run_one()、poll()、poll_one() といった関数の呼び出しが完了していない場合は呼び出してはいけません。
というわけで、イベントループの終了をアプリケーションから要求する場合には、io_service::work を用いるべきだと考える。これは強制されているわけではないが、事実上の標準とみなせる。これを使うことで、イベントループの終了条件を容易かつ安全に複数個にすることができる。なぜこれが安全かというと、例えばユーザがアプリケーションの×ボタンを押してウィンドウが閉じられたという条件が、イベントループを止めるための決定条件にならないことがあるからだ。アプリケーションによっては×ボタンが押された後、サーバに何らかの情報を送信してから終了する、というようなことがある。このような場合には、io_service::stop を呼び出してしまうとその後の非同期処理が実行されなくなってしまう。それはまずいだろう。
では、io_service::reset はいつ使うべきなのか?それは、io_service のインスタンスを使い回して何度かイベントループを実施したいような場合だ。
つまり、io_service::stop は本当に通信を中断しなければならないときは呼ばないようにするべきだと思う(ここまで長々と語っておきながらここにきて「思う」かよ、と思われた方、すみませんがその通りです)。
以上、ここまで述べてきた内容は、asio のリファレンスに詳しく記載されている。
補足
ここまで見てきて「こう書けばこう動くということは理解できても、なぜこうなるのかが理解できない」という方もいると思いますので補足しておきます。まず、io_service とは何なのか、ということですが、これは I/O 完了ポートを使ったイベントループを提供してくれるクラスです。io_service::work は、io_service クラスが終了する条件をインスタンスの生成・破棄という形で記述するためのクラスです。C++ はスコープによるリソース管理がとても美しく書ける言語なので、そこが活かされています。
実際、io_service::work クラスにはこれといった機能がありません。リファレンスを見ても基盤となる io_service を取得できる以外には何も機能がありません。コードを見てみると、基盤となる io_service のとあるカウンタを 1 増やしているだけです。これにより、io_service は io_service::work がすべて破棄されるまで io_service::run が完了しない、という処理を実現しています(もちろん内部では異常が発生した場合や io_service::stop が呼び出された場合には処理を抜けるようになっていますが)。
まとめ
- io_service::work を使って io_service::run が完了するタイミングを制御する
- io_service の振る舞いを十分に把握する
- io_service::stop を呼び出す必要があるか?
- io_service::reset を呼び出す必要があるか?
我ながら今回はあまり「まとめ」になっていません。
次回に向けて
ここまで asio を眺めてみると「うまくいけそうだ」と思って自分が持っているコードにも適用したくなってくるころだと思います。しかし、非同期処理を安定動作させるのは至難の業であり、その基盤となるライブラリを提供するのは困難を極めるはずです。つまり「asio は利用できるほどに安定しているのか?」ということを何かしら確認しなければいけません。次回は、asio の信頼性、asio でできないことについてできる限り検討してみます。