WindowsでのIPv6プログラミング講座 第2回

WindowsでのIPv6プログラミング講座 第2回

タグ:
第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クライアントの基本構造
Platform SDKの入手とインストール

 ポイントはブロック(1)においてサーバのホスト名を名前解決した結果、複数のIPアドレスが得られた場合、それらを連ねてアドレス情報のリストを作ることです。例えば、もし接続先のサーバが下記の2つのIPアドレスを持っている場合は

  1. IPv4アドレス 192.168.1.80
  2. IPv6アドレス 2001:db8:1::80

 生成されるリストは

  アドレス情報 -------------------> アドレス情報 -------------> NULL
    +プロトコル種類: IPv6             +プロトコル種類: IPv4
    +アドレス:       2001:db8:1::80   +アドレス:       192.168.1.80


 となります。次に、ブロック(2)ではアドレス情報リストの先頭の要素から順にサーバへの接続を試行しています。成功したら以降の要素については試行を打ち切ることで、最初に成功したアドレスと通信と行います。もし上記のリストでIPv6で接続できれば、IPv4での接続試行は行いません。逆にIPv6で接続できなければ、IPv4での接続に自動的に処理が切り替わります。この動作をIPv6からIPv4へのフォールバック動作と呼び、TCPを利用する多くのIPv6対応プログラムの基本的な構造になっています。


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のリストを構成するた */
                                   /* めの連結ポインタ */
};


 ai_familyは通信に使用するプロトコルを指定します。(IPv6, IPv4など)プロトコルを問わない場合は、AF_UNSEPCを指定し、もし通信に対して特定のプロトコルの使用を強制したい場合はAF_INET6, AF_INETなどを与えます。

 ai_socktypeはSOCK_STREAM, SOCK_DGRAMなどトランスポート層のプロトコルを指定します。ai_protocolは29~39行目の処理では特に指定していませんが、ai_familyとai_socktypeの内容からgetaddrinfo()が適切なプロトコルを自動的に選びます。

 今回作成するTCP echoクライアントが利用するプロトコルやポート名(番号)は表1

表1:TCP echoクライアントの動作
サーバ名 nodename (コマンドラインから入力されたサーバ名)
サービス名 servname (servname変数が文字列"echo"を参照するように初期化済み)
使用するプロトコル 明示的に指定しない(IPv6が接続できなければIPv4を使う)
使用するトランスポートプロトコル TCP

に示したものを利用するため
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

 としてヒント情報(hints)を初期化し、36行目のgetaddrinfo()の呼び出しを行いました。ホスト名、サービス名の解決が成功すると、getaddrinfo()は内部でホスト名から得られたIPアドレスの数だけアドレス情報(ADDDRINFO)の要素を連ねてリストを作り出します。第4引数のai0はリストの先頭を示すよう値が初期化されます。ここでは仮にgetaddrinfo()の第1引数に与えられたTCP echoサーバが

  1. IPv4アドレス 192.168.1.80
  2. IPv6アドレス 2001:db8:1::80

という2つのIPアドレスを持っていたとして、生成されたリストの情報構造を図3に示します。

図3:getaddrinfo()関数が生成したADDRINFOリストの構造
Platform SDKの入手とインストール

 先頭の要素であるADDRINFO構造をみると、ai_family, ai_socktype,ai_protocolはそれぞれAF_INET6, SOCK_STREAM, IPPROTO_TCPに初期化されています。これらの3つの変数をsocket()関数に渡せばソケットを作り出すことができます。

  リストの先頭の要素については
    socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol) ...... (S1)
  という呼び出しは
    socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)
  と同じ意味として展開されます。


 次にai_addrはIPv6のソケットアドレス(SOCKADDR_IN6型)で初期化されており、ai_addrlenにはSOCKADDR_IN6構造体のサイズ(sizeof(SOCKADDR_IN6))が格納されています。なおADDRINFOリストの2番目の要素はIPv4(AF_INET)ですが、ai_addrの部分に注目するとIPv4のソケットアドレス(SOCKADDR_IN型)で初期化されています。図3でのSOCKADDR_IN6型のソケットアドレスの内部を観察すると、in6_addrとsin6_portには、IPv6アドレスとポート番号がすでに設定されています。プログラマはソケットアドレスの生成について気にする必要はなく、(S1)で生成したソケットに対して接続(connect)処理を行う必要があるときは、下記のように

  connect( (S1)で生成したソケット, ai->ai_addr, ai->ai_addrlen )


ai_addrとai_addrlen変数をconnect()関数に引き渡せばよいです。これはADDRINFOリストの2番目の要素(IPv4)を処理する場合も同様です。

●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:    }


 46行目から48行目では、もしOSがIPv6をサポートしていないなど、何らかの理由でソケットの生成に失敗したら、ADDRINFOリストの次の要素(つまりIPv4のソケット生成)を試します。ソケット生成が正しく行われたら、次はサーバへの接続を試みます。すでにソケットアドレス(ai_addr)はgetaddrinfo()により内容が構成されていますので、connect()関数にそのまま引き渡します。52行目のconnect()関数により接続が成功すれば、60行目のbreakによりforループを脱出し、直ちにサーバとの文字列受送信のI/O処理に遷移します(参考:WinSockとバークレーソケットの違い その2)。

 もし何らかの理由により、先頭の要素(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:    }


●72~89行目の処理。このブロックに処理が到達したときはクライアントとサーバ間にはすでにコネクションが確立しており、データの受送信が可能な状態となっています。IPv6, IPv4のプロトコルはソケットによって完全に隠蔽されており、ソースコード上にプロトコルに依存する処理は全くありません。このブロックでの処理の概要は、標準入力からEOF(^Z)が入力されるまで文字列を受け取り、send()関数でサーバへ送信、recv()関数でサーバからのエコーバックを受信して、画面にその結果を表示するものです。

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:\>

WinSockとバークレーソケットの違い その1

 WinSock APIではソケットはSOCKET型を使います。UNIXでのバークレーソケットではint型で表現されますが、SOCKET型の実態も整数型に過ぎません。しかし、UNIXではソケットとファイルI/Oを行うファイルディスクリプタが区別することなく統一的に扱うことができます。しかしWinSockではソケットはファイルI/Oのためのディスクリプタは区別されています。統一的に扱うWin32 APIも存在はしますが、UNIXのようにread()/write()関数を使ってデータの受送信を行うことはできません。このためSOCKET型とint型は厳格に区別して考えるべきです。

 またsocket()関数の成功・非成功の検査に対して、UNIXでは if (s < 0) {... } のように負の値であるの判定が多く用いられます。POSIXでは失敗時に、-1が返却されるとされています。なおWinSockではsocket()関数の失敗時は必ずINVALID_SOCKETの返却が仕様となっています。


WinSockとバークレーソケットの違い その2

 WinSock APIではconnect()関数は失敗時にSOCKET_ERRORを返します。
 listen(), bind(), send(), recv() などの関数も失敗時はSOCKET_ERRORを返します。バークレーソケットでは失敗時は-1を返しますのでWinSockとの違いには注意してください。


WinSockとバークレーソケットの違い その3

 WinSock APIではソケットのクローズのため、closesocket()関数を使用します。UNIXでのバークレーソケットではclose()関数を使用しますが、WinSockではclose()関数をソケットに対して使うことはできません。


※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に示します。

この記事のトラックバックURL

http://www.ipv6style.jp/trackback/204
Ads by Google

IPv6ブログ