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 による名前解決は、すっきりきれいにコードが書けるのでとてもよさそうだ。

懸念点

最後に、実際にコードを追ってみて気になったことを挙げておく。

  1. _WIN32_WINNT という Windows プログラムにとって重要なプリプロセッサを指定しないと使えないということ
    • これはちょっと残念だった
  2. getaddrinfo のアドレスを DLL から取得する際の対象が、ws2_32.dll のみであるということ
  3. 自力でゴリゴリ実装したときよりもバイナリサイズがいくらか大きくなる
    • asio に限らず boost 全般に共通している

この 3 点を気にしなくてもいいプログラムであれば、問題なく使えるだろう。

次回に向けて

ちなみに、名前解決をすることで、 socket 関数に必要な以下の三つを得ることができる。

  1. アドレスファミリ
  2. ソケットタイプ
  3. プロトコル

それではまた。