非同期なgetaddrinfo


昨今のネットワーク通信系では、ホスト名変換はほぼほぼ`getaddrinfo(3)`で変換するのが一択の様な気がします。

- RFC3943(2133/2553)でも定義されているPOSIX.1-2001標準の関数である
- スレッドセーフである(IPv4時代に多く使われた`gethostbyname(3)`はスレッドセーフではない)
- RFC2553で定義されたIPv6対応の`getipnodebyname(3)`はRFC3943への改版時に破棄され、最近のglibcでも削除されている(代替えが無い)

これらの理由から(IPv4通信しかしないとしても)余り他を選ぶ必要は無いのかな…と。


とはいえ、意外と使いづらい点も多く、そのうちの１つが「DNSサーバとの通信が不安定な時などに`getaddrinfo(3)`呼び出しから中々戻ってこないことがある」というところです。

iOSなどではwatchdogの関係で、メインスレッドでは使うな、とまで言われてたりしますね…。


※個人的には、色々お任せ出来る反面、色々やり過ぎててちょっと重たい関数になってる気もしつつ…
※とか言いながらhints.ai_family == AF_LOCALで servname にパス名与えたらsockaddrにパッキングしてくれてもいいのに…と思ったりもするのだけどw


ということで、非同期に`getaddrinfo(3)`する方法の１つとして、glibc拡張(libanl)の`getaddrinfo_a(3)`を紹介してみます。

**※glibc拡張なのでLinux以外では使用できません**


# getaddrinfoのサンプル

ひとまず通常の`getaddrinfo(3)`のサンプルをこんな感じで。

```cpp:lookup.c
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>

void
GetAddrInfo(const char *nodename, const char *servname,
	const struct addrinfo *hints, struct addrinfo **res)
{
	int r = getaddrinfo (nodename, servname, hints, res);
	switch (r) {
	case 0: // SUCCESS
		break;
	case EAI_SYSTEM:
		perror ("getaddrinfo");
		exit (71);
		/* NOTREACHED */
	default: // ANY ERROR
		fprintf (stderr, "getaddrinfo : %s(%d)\n", gai_strerror(r), r);
		exit (71);
		/* NOTREACHED */
	}
	return ;
}

void
lookup(const char * nodename)
{
	struct addrinfo	hints = {AI_ADDRCONFIG, AF_UNSPEC, SOCK_STREAM, 0};
	struct addrinfo *res = NULL;

	GetAddrInfo (nodename, NULL, &hints, &res);
	printf ("getaddrnfo(%s) :\n", nodename);
	for (struct addrinfo *info = res; info; info = info->ai_next) {
		char nhost[NI_MAXHOST] = "";
		getnameinfo (info->ai_addr, info->ai_addrlen, nhost, sizeof(nhost), NULL, 0, NI_NUMERICHOST);
		printf ("\t%s\n", nhost);
	}
	freeaddrinfo (res);
	return ;
}

int
main (int argc, char *argv[])
{
	for (int i=1; i<argc; ++i) {
		lookup (argv[i]);
	}
	return 0;
}
```

単純にパラメータ入力されたホスト名のIP変換して出力するだけですがサンプルとしては充分でしょう。

```bash:実行イメージ
$ cc -o lookup lookup.c
$ ./lookup www.example.org example.com
getaddrnfo(www.example.org) :
	2606:2800:220:1:248:1893:25c8:1946
	93.184.216.34
getaddrnfo(example.com) :
	2606:2800:220:1:248:1893:25c8:1946
	93.184.216.34
$ 
```

ここで`getaddrinfo(3)`をラッピングしている`GetAddrInfo`関数を弄ることでバリエーションを作ってみます

# getaddrinfo_aのサンプル(同期)

とりあえず置き換えてみます。

```cpp:GetAddrInfoA
void
GetAddrInfoA (const char * nodename, const char *servname
	const struct addrinfo *hints, struct addrinfo **res)
{
	struct gaicb	cb = {nodename, servname, hints, NULL};
	struct gaicb	*reqp = &cb;

	int r = getaddrinfo_a (GAI_WAIT, & reqp, 1, NULL);
	switch (r) {
	case 0: // SUCCESS
		break;
	case EAI_SYSTEM:
		perror ("getaddrinfo_a");
		exit (71);
		/* NOTREACHED */
	default: // ANY ERROR
		fprintf (stderr, "getaddrinfo_a : %s(%d)\n", gai_strerror(r), r);
		exit (71);
		/* NOTREACHED */
	}
	*res = cb.ar_result;
	return ;
}
```

ここでは`GAI_WAIT`指定なので`getaddrinfo_a(3)`で待ち合わせが行われます。

単純に構造体でパラメータを指定するだけで他は変わらない感じ。


# getaddrinfo_aのサンプル(非同期)

```cpp:GetAddrInfoAsync
void
GetAddrInfoAsync (const char * nodename, const char *servname
	const struct addrinfo *hints, struct addrinfo **res,
	struct timespec *timeout)
{
	struct gaicb          cb = {nodename, servname, hints, NULL};
	struct gaicb       *reqp = &cb;
	struct gaicb const *refp = &cb;

	int r = getaddrinfo_a (GAI_NOWAIT, & reqp, 1, NULL);
	switch (r) {
	case 0: // SUCCESS
		break;
	case EAI_SYSTEM:
		perror ("getaddrinfo_a");
		exit (71);
		/* NOTREACHED */
	default: // ANY ERROR
		fprintf (stderr, "getaddrinfo_a : %s(%d)\n", gai_strerror(r), r);
		exit (71);
		/* NOTREACHED */
	}
	r = gai_suspend (&refp, 1, timeout);
	switch (r) {
	case 0: // 1 requests done
	case EAI_ALLDONE:
		*res = cb.ar_result;
		break;
	case EAI_AGAIN: // TIMEOUT
		break;
	case EAI_SYSTEM:
		perror ("gai_suspend");
		exit (71);
		/* NOTREACHED */
	default: // ANY ERROR
		fprintf (stderr, "gai_suspend : %s(%d)\n", gai_strerror(r), r);
		exit (71);
		/* NOTREACHED */
	}
	return ;
}
```

`GetAddrInfoAsync`に追加したパラメータ`struct timespec  timeout`を`gai_suspend(3)`の引数に充てることで、指定時間のタイムアウト検出を行うことが出来るようになります。

ここで、`getaddrinfo_a(3)/gai_suspend(3)`のパラメータはそれぞれ通常のポインタの配列(reqp)とポインタ定数の配列(refp)で型が違う事に注意が必要です(非常に紛らわしい…)

ここでは1リクエストしか指定していないので`gai_suspend(3) := 0`の場合も終了とみなしてしまっていますが、複数のリクエストを起動する場合にはこの辺りの処理はもっと複雑(少なくともループしてチェックする必要がある)になります。

あくまでも`gai_suspend(3) := 0`の場合は１つ以上のリクエストが終わった事を通知しているだけで、並行に問い合わせている別のリクエストの処理は動き続けている事に注意が必要です(詳細は後述)


# 実装としては

詳細はglibcのソース(resolv/gai_xxx.c)を確認してもらった方が良いのですが、端的に言えば`getaddrinfo_a(3)`の中でスレッドを起動して、そのスレッドで`getaddrinfo(3)`を実施していますw

なので、`GAI_WAIT`やら`gai_suspend(3)`やらで諸々の処理を行っているのは、単にそれらのスレッド間での通信手順です。
※`pthread_cond_timedwait(3)`などを使ったりするのでタイムアウト指定が出来る

なので、Linux以外の環境で／Linuxだけど多負荷の通信制御するのでスレッドリソースを管理したい、などの理由があれば同様の処理を作ることになるのかな(^_^;)

ところで、手元にあるCentOS 7.3(glibc-2.17)で`pthread_cond_timedwait(3)`で`ETIMEDOUT`が返ってこない(`EAI_SYSTEM/errno=0`になる)のは何故だろう…orz


# その他

今回のサンプルでは`getaddrinfo(3)`の置き換えという感じで1つのリクエストしか処理してませんが、実際にはこれは複数のリクエストを並行して問い合わせる様な動作を行うことも出来ます。

- `gai_suspend(3)`が0を返した場合、自分が出したどのリクエストに対して応答があったのかよくわからないので、`gai_error(3)`で各リクエストの状態を確認して見つける
- 例えばいずれかのリクエストが戻ってきた時点で`gai_cancel(3)`で他のリクエストを破棄する(しないで待っても良いけど)
- 破棄したモノも含め(破棄しないでも)全てのリクエストが戻るのを待つ(`gai_suspend(3) := EAI_ALLDONE`まで待つ)

などなどで、少し面白い部分もあるので、そのうち余裕があれば新しく書くかも…