Edited at

非同期なgetaddrinfo

More than 1 year has passed since last update.

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


  • RFC3943(2133/2553)でも定義されているPOSIX.1-2001標準の関数である

  • スレッドセーフである(IPv4時代に多く使われたgethostbyname(3)はスレッドセーフではない)

  • RFC2553で定義されたIPv6対応のgetipnodebyname(3)はRFC3943への改版時に破棄され、最近のglibcでも削除されている(代替えが無い)

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

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

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

※個人的には、色々お任せ出来る反面、色々やり過ぎててちょっと重たい関数になってる気もしつつ…

※とか言いながらhints.ai_family == AF_LOCALで servname にパス名与えたらsockaddrにパッキングしてくれてもいいのに…と思ったりもするのだけどw

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

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


getaddrinfoのサンプル

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


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変換して出力するだけですがサンプルとしては充分でしょう。


実行イメージ

$ 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のサンプル(同期)

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


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のサンプル(非同期)


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 timeoutgai_suspend(3)の引数に充てることで、指定時間のタイムアウト検出を行うことが出来るようになります。

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

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

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


実装としては

詳細は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まで待つ)

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