asio 02

はじめに

前回作った名前解決をするコードを、asio の真価を発揮させて非同期にしてみよう。

うんちく

もしあなたが名前解決をするコードを asio を使わずに getaddrinfo や gethostbyname を使って書いたことがあるのなら、以下のようなことをご存じかもしれない。

  1. getaddrinfo、gethostbyname は connect、send、recv などの関数と違って非同期にすることができないということ
  2. gethostbyname が返すバッファの扱いには十分注意が必要

ちなみに、gethostbyname に関しては既によく論じられている。

  1. Geekなぺーじ : gethostbynameの落とし穴
  2. Manpage of GETHOSTBYNAME
  3. gethostbyname macro (wsipv6ok.h) | Microsoft Docs

対策状況はそれぞれ以下のようになっているようだ。

  1. getaddrinfo、gethostbyname は connect、send、recv などの関数と違って非同期にすることができないということ
    • 内部でワーカースレッドを作りそこから呼び出す
      • よくある手なのでやっぱりそうだよね、という感想
  2. gethostbyname が返すバッファの扱いには十分注意が必要
    • 必要なデータを内部にコピーしている(参照やシャローコピーではなくディープコピー)
    • gethostbyname が返すバッファは TLS に保存されているので複数スレッドからの同時アクセス対策はしていない
      • これは Windows の話なので、他のプラットフォームでどうなっているかは要確認
    • getaddrindo ではこういった問題は発生しない

サンプルコード

#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 の対応表があったので必要に応じて参考にすること。

  1. The BSD Socket API and Boost.Asio

懸念点

今回は特になし。

アプリケーション設計のガイドライン

名前解決によって複数のエンドポイントを取得できたときの振る舞いをどうするか
  1. 最初の一つだけを処理対象とする(非推奨)
  2. 全てを処理対象とする(推奨)
非同期で名前解決を行った場合にどれだけ待機するか
  1. 名前解決処理が何らかの処理結果を返すまで待機する(推奨)
  2. アプリケーションが定めた時間だけ待つ(非推奨)
    1. ネットワークの状態が思わしくないときに反応が遅いからといって単純に待機時間を短縮すればいいというわけではない
    2. ソフトウェアファイアウォールによって警告ダイアログが出ているだけの場合もある
    3. 名前解決に関する詳しく知識がない場合は極力これを避けること
    4. その場合処理結果が帰ってくるまでの時間は OS の実装に依存する
  3. 名前解決処理が何らかの処理結果を返すのを待機しつつユーザからキャンセル指示があった場合は即終了する
    1. かといってすぐにプログラムを終了してはいけない
    2. 名前解決処理は途中で中断できない
    3. ユーザからキャンセル指示があった場合は「終了中」状態に移行しすべての処理が正常に終わるまで待機してからプログラムを終了する
複数の IP アドレスからどれを選択するか
  1. 列挙される順に扱う(普通)
    1. 名前解決は DNS に変化がないと毎回同じ順序で結果を返す
    2. OS の実装に依るのか?
  2. 列挙されたものをシャッフルして扱う
    1. DNS ラウンドロビンが効いている場合は、サーバ側に優しいかもしれないし、そうでないかもしれない
    2. アクセス先のサーバによるところが大きいので、アクセス先の仕様によって決定するべきかもしれない
名前解決結果をキャッシュするか
  1. キャッシュする(非推奨)
    1. 単純にキャッシュしてしまうと DNS 変更にうまく対応できない
    2. DNS レコードに応じたキャッシュをしようとしてもそこまで低レベルな情報にアクセスする手段は提供されていない
  2. キャッシュしない(推奨はしないが非推奨ではない)

次回に向けて

さぁ次はソケットを作っていよいよ通信といきたいところだが、それはサンプルが豊富なので割愛し、非同期を扱う上で欠かすことのできない io_service について取り上げる。