| 第2回 IPv6に対応したTCP echoクライアントプログラムの作成 | (2005.11.14) |
加藤淳也
NTT情報流通プラットフォーム研究所
はじめに
第1回では開発環境の整備方法と、ホスト名からIPv6アドレスを解決する簡単なプログラムの作成を行いました。第2回ではIPv6に対応したTCP echoクライアントプログラムの作成を行い、実際に動作させます。なお次回にはサーバの作成を予定しています。TCP echoプロトコル(RFC862)はきわめて単純で、クライアントがサーバに対して文字列を送信し、サーバは受信した文字列をそっくりそのままクライアントに返送するものです。今回はTCP echoプログラムの作成を通じて、IPv6対応のクライアントの基本的な動作の理解を狙います。
IPv6対応(プロトコル非依存)プログラム
本シリーズで取り扱うIPv6対応プログラムは、原則としてIPv4とIPv6の両者に対応したプログラムのことです。一般に、両者に対応したTCPクライアントプログラムとはIPv6, IPv4のうち利用可能なプロトコルを自動的に判別して接続する機能を持っています。多くの場合IPv6が優先して選択され、IPv6で接続できなければIPv4に切り替わる動作となります。中には、IPv6プロトコルでの動作が必須である特殊なプログラムも存在しますが、TCP echoをはじめ、Webサーバとブラウザ、メール(SMTP, POP, IMAP)サーバとメールクライアントなど、TCPを使う多くのプログラムはIPv6, IPv4の適切な切り替えをサポートしています。
今回の解説の本質はプロトコル非依存プログラミングです。IPv6に対応させることだけを目的としません。もし将来IPv7など新たなプロトコルが出現しても、そのまま動作させることができる、あるいは軽微な改変だけで動作させることができるプログラムの作成方法を習得します。
IPv6対応 TCPクライアントの基本構造
今回はサンプルプログラムとしてTCP echoクライアントを作成します。図1には今回作成したクライアントプログラムの実行例(※1)を示しています。なおクライアントのソースコードはリスト2、コンパイル方法は図4に示しています。コマンドライン引数にサーバのホスト名を与えてコンソールから文字列を入力すると、その内容がサーバからそっくりそのまま返信(エコーバック)される単純なプログラムです。図2にTCP echoクライアントの大まかなプログラム構造を示します。
図1:tcp-echo-clientの実行例
| C:\>tcp-echo-client.exe tcpecho.example.jp connected |
|
| 0123456789ABCD | ←キーボードから入力した文字列 |
| 0123456789ABCD | ←サーバから受信した文字列 |
| EFGHIJKLMNOPQR | ←キーボードから入力した文字列 |
| EFGHIJKLMNOPQR | ←サーバから受信した文字列 |
| ^Z | ←入力終了 |
| C:\> | |
図2:IPv6対応 TCP echoクライアントの基本構造
ポイントはブロック(1)においてサーバのホスト名を名前解決した結果、複数のIPアドレスが得られた場合、それらを連ねてアドレス情報のリストを作ることです。例えば、もし接続先のサーバが下記の2つのIPアドレスを持っている場合は
- IPv4アドレス 192.168.1.80
- IPv6アドレス 2001:db8:1::80
生成されるリストは
アドレス情報 -------------------> アドレス情報 -------------> NULL
+プロトコル種類: IPv6 +プロトコル種類: IPv4
+アドレス: 2001:db8:1::80 +アドレス: 192.168.1.80
|
|
TCP echoクライアントのソースコード リスト2にTCP echoクライアントのソースコードを示します。図2で示した、基本構造がリスト2のソースコード上ではどのように実現されているか順を追って見ていきます。 ●16~21行目の処理。TCP echoクライアントはコマンドラインからサーバのホスト名を受け取り、これをnodename変数で参照可能とします。 |
016: /** コマンドライン引数の処理(サーバ名をコマンドラインから読み取る) */
017: if (argc != 2) {
018: fprintf(stderr, "syntax: tcp-echo-client servername\n");
019: exit(1);
020: }
021: nodename = argv[1];
|
|
●23~27行目の処理。WinSockの初期化を行います。使用するWinSockのバージョンは2.2です。 |
023: /** WinSockの初期化 */
024: if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
025: fprintf(stderr, "can not initilize WinSock\n");
026: exit(1);
027: }
|
●29~39行目の処理。この部分は図2におけるブロック(1)に該当する処理です。 |
029: /** 名前解決とADDRINFOリストの生成 */
030: memset(&hints, 0, sizeof(hints));
031: /* IPv6/IPv4 */
032: hints.ai_family = AF_UNSPEC;
033: /* ソケットのタイプとしてストリーム型(TCP)を指定 */
034: hints.ai_socktype = SOCK_STREAM;
035: /* 名前解決を行いADDRINFOリストを生成。ai0がリストの先頭を表す */
036: if (e = getaddrinfo(nodename, servname, &hints, &ai0)) {
037: fprintf(stderr, "%s\n", gai_strerror(e));
038: exit(1);
039: }
|
ここで最も重要となるgetaddrinfo()関数について解説します。この関数は名前解決を行う機能を持っています。 getaddrinfo()は4つの引数を取り、第1引数にはホスト名、第2引数にはサービス名を渡します。 ここでサービス名とはポート番号につけられた名前のことで、echoのポート番号は7番, httpならば80番、smtpならば25番などが代表的です。UNIXならば/etc/servicesに、WindowsではC:\WINDOWS\system32\drivers\etc\servicesファイルに対応関係が表記されています(※2)。 次にgetaddrinfo()関数にとって入力と出力のいずれの用途にも用いられADDRINFO構造体について解説します。getaddrinfo()関数への入力として使用される場合は関数の動作を決めるヒント情報として扱われ、関数からの出力として利用する場合は、アドレス情報のリストを受け取るために使われます。リスト1はADDRINFOの定義を示したものです。ADDRINFO構造をgetaddrinfo()に対する入力とし使う場合、構造体の各メンバは次のような意味を持ちます。 リスト1:ADDRINFO構造体のメンバ |
struct addrinfo
{
int ai_flags;
int ai_family; /* AF_UNSPEC, AF_INET6, or AF_INET */
int ai_socktype; /* SOCK_STREAM, SOCK_DGRAM */
int ai_protocol; /* IPPROTO_IP, IPPROTO_TCP, IPPROTO_UDP */
size_t ai_addrlen; /* ai_addrの大きさ */
char *ai_canonname;
struct sockaddr *ai_addr; /* ソケットアドレスへのポインタ */
struct addrinfo *ai_next; /* ADDRINFOのリストを構成するた */
/* めの連結ポインタ */
};
|
リストの先頭の要素については
socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol) ...... (S1)
という呼び出しは
socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)
と同じ意味として展開されます。
|
|
|
connect( (S1)で生成したソケット, ai->ai_addr, ai->ai_addrlen ) |
|
●41~61行目の処理。この処理は図2におけるブロック(3)に該当する処理となります。forループによりai0で示されるADDRINFO構造体のリストの要素を先頭から順に処理していきます。変数aiは現在注目するリストの要素を指し示します。46行目でソケットの生成を行います。(参考:WinSockとバークレーソケットの違い その1)
|
041: /** サーバへの接続できるまで、各ADDRINFOリストの要素の先頭から */
042: /** 順に試行する */
043: for (ai = ai0; ai; ai = ai->ai_next) {
044: /* ソケットの生成を試みる。生成に失敗したら */
045: /* ADDRINFOリストの次の要素で接続を試行する */
046: s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
047: if (s == INVALID_SOCKET)
048: continue;
049:
050: /* サーバへの接続を試みる。接続に失敗したら */
051: /* ADDRINFOリストの次の要素で接続を試行する */
052: if (connect(s, ai->ai_addr, ai->ai_addrlen) == SOCKET_ERROR) {
053: closesocket(s);
054: s = INVALID_SOCKET;
055: continue;
056: }
057:
058: /* ここではすでに接続が成功しているので、以降の接続試行は打ち切る */
059: printf("connected\n");
060: break;
061: }
|
もし何らかの理由により、先頭の要素(IPv6)で接続に失敗してしまったら、55行目のcontinueによりADDRINFOリストの次の要素(IPv4)での接続を試みます。continueでの再試行の前に接続できなかったソケットは閉じておき、sの値をINVALID_SOCKETに値を戻しておきます(参考:WinSockとバークレーソケットの違い その3)。 ここで、改めて41~61行目の処理を見ると、IPv4やIPv6など特定のプロトコルに依存する記述はありません。例えばsocket()関数の第1引数にはプロトコル名を指定しますが、プログラマがプロトコル名をハードコーディングはしていません。IPv6, IPv4のうちどのプロトコルが使用可能かgetaddrinfo()関数が調べ、通信できる可能性があるプロトコルだけADDRINFO構造体の要素を作り出します。socket()関数の第1引数には、getaddrinfo()関数がai_family変数に設定した値をそのままに渡しています。したがってプログラマがプロトコルに依存する処理を記述する必要はありません。 ●65~70行目の処理。ADDRINFOリストのすべての要素(IPv6, IPv4のいずれでも)について接続が失敗した場合は、サーバにはどの方法を用いても接続できないと判断しプログラムの終了処理を行います。IPv6, IPv4のいずれかでconnect()関数が成功していればこの部分の処理はスキップされます。 |
063: /** ADDRINFOリストのすべての要素に対して */
064: /** 接続が失敗した場合はsの値はINVLIAD_SOCKETである */
065: if (s == INVALID_SOCKET) {
066: freeaddrinfo(ai0);
067: WSACleanup();
068: fprintf(stderr, "can not connect server(%s)\n", nodename);
069: exit(1);
070: }
|
|
072: /** クライアント・サーバ間のI/O処理 */
073: /** サーバへの文字列の送信と、サーバからの文字列の受信 */
074: while (fgets(linebuf, sizeof(linebuf), stdin) != NULL) {
075:
076: /* 標準入力から獲得した文字列をサーバへ送信 */
077: if (send(s, linebuf, strlen(linebuf), 0) == SOCKET_ERROR) {
078: fprintf(stderr, "send error\n");
079: exit(1);
080: }
081:
082: /* サーバから返信された文字列を受信し画面に表示 */
083: if (recv(s, linebuf, sizeof(linebuf), 0) == SOCKET_ERROR) {
084: fprintf(stderr, "recv error\n");
085: exit(1);
086: }
087: printf(linebuf);
088:
089: }
|
●91~93行目の処理。不要となったADDRINFOのリストの開放を行います。36行目で得たADDRINFOリストのメモリ領域はgetaddrinfo()が内部で生成したものであり、プログラマが事前に確保したものではありませんでした。したがって不要になったらfreeaddrinfo()でその領域を開放する必要があります。さらにWSACleanup()関数によりWinSockのクリーンナップを行いプログラムを終了します。 |
091: freeaddrinfo(ai0); 092: WSACleanup(); 093:} |
|
TCP echoクライアントのまとめ TCP echoクライアントを作成する際に、IPv6に対応させるためのポイントは、getaddrinfo()関数による名前解決とADDRINFO構造体のリストの作成です。ADDRINFO構造体の内部にプロトコルに依存する情報とサーバへの接続に必要な情報を詰め込むのはgetaddrinfo()関数であり、プログラマがこれらの情報を生成する必要はありません。プロトコルに依存する情報の生成をすべてシステムに任せることで、プロトコル非依存のプログラム作成が可能です。またADDRINFO構造のリストを接続できるまで順に処理することで、IPv6で接続を試み、失敗したらIPv4に自動的にフォールバックするプログラムが実現できます。次回は、引き続きIPv6に対応したTCP echoサーバの作成を行います。 リスト2:tcp-echo-client.c |
#include ‹winsock2.h›
#include ‹ws2tcpip.h›
#include ‹stdio.h›
int main(int argc, char *argv[])
{
WSADATA wsaData;
char *nodename;
char *servname = "echo";
ADDRINFO hints;
LPADDRINFO ai, ai0;
int e;
SOCKET s;
char linebuf[BUFSIZ];
/** コマンドライン引数の処理(サーバ名をコマンドラインから読み取る) */
if (argc != 2) {
fprintf(stderr, "syntax: tcp-echo-client servername¥n");
exit(1);
}
nodename = argv[1];
/** WinSockの初期化 */
if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
fprintf(stderr, "can not initilize WinSock¥n");
exit(1);
}
/** 名前解決とADDRINFOリストの生成 */
memset(&hints, 0, sizeof(hints));
/* IPv6/IPv4 */
hints.ai_family = AF_UNSPEC;
/* ソケットのタイプとしてストリーム型(TCP)を指定 */
hints.ai_socktype = SOCK_STREAM;
/* 名前解決を行いADDRINFOリストを生成。ai0がリストの先頭を表す */
if (e = getaddrinfo(nodename, servname, &hints, &ai0)) {
fprintf(stderr, "%s¥n", gai_strerror(e));
exit(1);
}
/** サーバへの接続できるまで、各ADDRINFOリストの要素の先頭から */
/** 順に試行する */
for (ai = ai0; ai; ai = ai->ai_next) {
/* ソケットの生成を試みる。生成に失敗したら */
/* ADDRINFOリストの次の要素で接続を試行する */
s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (s == INVALID_SOCKET)
continue;
/* サーバへの接続を試みる。接続に失敗したら */
/* ADDRINFOリストの次の要素で接続を試行する */
if (connect(s, ai->ai_addr, ai->ai_addrlen) == SOCKET_ERROR) {
closesocket(s);
s = INVALID_SOCKET;
continue;
}
/* ここではすでに接続が成功しているので、以降の接続試行は打ち切る */
printf("connected¥n");
break;
}
/** ADDRINFOリストのすべての要素に対して */
/** 接続が失敗した場合はsの値はINVLIAD_SOCKETである */
if (s == INVALID_SOCKET) {
freeaddrinfo(ai0);
WSACleanup();
fprintf(stderr, "can not connect server(%s)¥n", nodename);
exit(1);
}
/** クライアント・サーバ間のI/O処理 */
/** サーバへの文字列の送信と、サーバからの文字列の受信 */
while (fgets(linebuf, sizeof(linebuf), stdin) != NULL) {
/* 標準入力から獲得した文字列をサーバへ送信 */
if (send(s, linebuf, strlen(linebuf), 0) == SOCKET_ERROR) {
fprintf(stderr, "send error¥n");
exit(1);
}
/* サーバから返信された文字列を受信し画面に表示 */
if (recv(s, linebuf, sizeof(linebuf), 0) == SOCKET_ERROR) {
fprintf(stderr, "recv error¥n");
exit(1);
}
printf(linebuf);
}
freeaddrinfo(ai0);
WSACleanup();
}
|
図4:tcp-echo-client.cのコンパイル方法 |
C:\>cl tcp-echo-client.c ws2_32.lib Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 13.10.3077 for 80x86 Copyright (C) Microsoft Corporation 1984-2002. All rights reserved. tcp-echo-client.c Microsoft (R) Incremental Linker Version 7.10.3077 Copyright (C) Microsoft Corporation. All rights reserved. /out:tcp-echo-client.exe tcp-echo-client.obj ws2_32.lib C:\> |
|
|
※1 次回、Windowsで動作するTCP echoサーバの作成も行いますが、多くのOSにはTCP echoサーバの機能が備わっているのでクライアントの動作確認のために使うことができます。例えばFreeBSDならばinetd.confの設定に対して、 |
echo stream tcp nowait root internal echo stream tcp6 nowait root internal |
上記の2つの行からコメントを外すとIPv6, IPv4共にTCP echoサーバの機能を有効化することができます。 ※2 IPv4プログラミングではgetservbyname()関数によりポート名からポート番号を解決していました。 さらに第3引数ではADDRINFO型のhints変数を渡しています。ADDRINFO構造体はgetaddrinfo()から名前解決の結果を受け取るためにも使用しますが、getaddrinfo()の動きを制御するヒント情報としても使用します。ADDRINFO型の構造をリスト1に示します。 |



