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

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

タグ:
第3回 IPv6に対応したTCP echoサーバプログラムの作成
(2005.11.28)
NTT情報流通プラットフォーム研究所
加藤淳也

はじめに

 第2回ではIPv6対応TCP echoクライアントの作成を通じて、プロトコルに依存しないプログラミング方法について解説しました。第3回ではIPv6対応のTCP echoサーバの作成を行います。IPv6, IPv4など特定のプロトコルへの依存性をなるべく排除したプログラム作成を目指します。

IPv6対応 TCPサーバの基本設計

 IPv6, IPv4の両方に対応したサーバを動作させるためには大まかに2つの設計が考えられます。Webサーバの例を図1に示します。左側の(1)ではIPv4の接続を受け入れるhttpdとIPv6の接続を受け入れるhttpd6が独立したプロセスとして動作しコンテンツを共有しています。右側の(2)では単一のhttpdプロセスがIPv6, IPv4の接続要求をどちらも受け入れています。

図1:IPv6に対応したWebサーバの設計
図1:IPv6に対応したWebサーバの設計

 TCP echoサーバの作成についてもプロトコル依存性をなるべく排除するため、今回は図1の右側(2)と同じ構造を採用し、単一のプロセスがIPv6, IPv4どちらの接続でも受け入れられるようにします。本設計では将来IPv7のような新たなプロトコルが出現しても、ソースコードを改変することなく、もしくは軽微な改変で対応が容易になります。

サーバの処理の大まかな流れ

 サーバがIPv6, IPv4いずれの接続要求でも受け入れるためには、両者のプロトコルについてソケットを作成し接続を待ち受け(listen)する必要があります。第2回で紹介したTCP echoクライアントでは通信に利用するソケットはひとつだけでしたが、サーバではIPv6, IPv4の2つのソケットを同時に開いてクライアントからの接続を待ちます。サーバプログラムの基本的な構造を図2に示します。

図2: TCPサーバの大まかな処理
図2: TCPサーバの大まかな処理

 ワイルドカードアドレスはIPv6では "::", IPv4では"0.0.0.0"であり、待ち受けソケットにこのアドレスをbindするとサーバのどのインターフェイスからでもクライアントを受け入れることができます。ブロック(1)ではADDRINFOリストを作成していますが、ここでもgetaddrinfo()関数を使用します。

 第1回、第2回では名前解決のAPIとして使用していましたが、サーバにおいてワイルドカードアドレスを使う場合はホスト名からIPアドレスへの名前解決は行う必要がありません。IPアドレスは"::"または"0.0.0.0"であることがわかっているためです。したがって第1回、第2回に登場したgetaddrinfo()関数とは異なる動作が必要となります。下記のようにgetaddrinfo()関数にホスト名を渡さず(NULLを渡す)、ヒント情報のフラグにAI_PASSIVEを設定しておきます。

hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;

e = getaddrinfo(NULL, "echo", &hints, &ai0);

 このフラグをつけて呼び出されたgetaddrinfo()はOSが持つプロトコルの種類を調べて、その数に応じADDRINFO構造体のリストを作成します。例えばOSがIPv6とIPv4のプロトコルスタックを持っていた場合、getaddrinfo()関数は図3に示すようなリストを作り出します。ホスト名を解決して得られたアドレスを並べたものではなく、OSが持っているプロトコルの数だけワイルドカードアドレスが並んでいることに注意してください。

図3:AI_PASSIVEフラグつきのgetaddrinfo()関数が生成したADDRINFO構造体のリスト
  ai0
   |
   |
   ↓   
ADDRINFO ------------------------> ADDRINFO ----------------------> NULL
  ai_family = AF_INET6               ai_family = AF_INET
  ai_socktype = SOCK_STREAM          ai_socktype = SOCK_STREAM
  ai_protocol = IPPROTO_TCP          ai_protocol = IPPROTO_TCP
  ai_addr                            ai_addr
    ->SOCKADDR_IN6                     ->SOCKADDR_IN
       +sin6_addr = "::"                  +sin_addr = "0.0.0.0"
       +sin6_port = 7 (echo)              +sin_port = 7 (echo)

 図3で生成した、ADDRINFO構造体のリストのすべての要素(すべてのプロトコル)に対してソケットの生成を行い、サーバからの接続を待ち受け(listen)を行います。

 次に図2のブロック(3)では入出力の多重化を行います。IPv6, IPv4いずれかでクライアントから接続があれば、接続を受け入れますが、どちらのプロトコルが先に接続されるか順序も決まっているわけでもなく、またIPv6の接続を受け入れている間に、IPv4で接続要求が来るかもしれません。もしくはIPv6での複数の接続要求が同時に到着するかもしれません。ブロック(3)におて必要となる処理は、IPv6, IPv4どちらでも複数の接続要求を同時に処理する多重化です。例えば、ほとんどすべてのWebサーバはブラウザからの複数の接続要求が同時に到着しても返答を行うことができます。多重化はIPv6プロトコルに依存した処理ではなく、従来から用いられてきた手法によって実現可能です。実現方法はいくつか考えられますが、今回のサンプルプログラムではUNIXで多く用いられているselect()関数と、Windowsスレッドを使って実装を行っています。

IPv6対応TCP echoサーバのソースコード

 リスト1にTCP echoサーバのソースコードを示します。図2で示した、基本構造がソースコード上ではどのように実現されているか順を追って見ていきます。

●20~24行目の処理。WinSockの初期化を行います。使用するWinSockのバージョンは2.2です。

020:    /** WinSockの初期化 */
021:    if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
022:        fprintf(stderr, "can not initilize WinSock\n");
023:        exit(1);
024:    }

●26~37行目の処理。図2におけるブロック(1)の処理に該当します。ここでは33行目でヒント情報にAI_PASSIVEフラグを設定しています。34行目で第1引数のホスト名をNULLとして、getaddrinfo()関数を呼び出すと、OSにインストールされているプロトコルの種類に応じてADDRINFO構造体のリストが生成されます。IPv6を有効化したWindows XPならば、図3に示す構造が生成され、もしIPv6を有効化していない(IPv4のみ)のWindows XPならば、図3の内、リストの2番目の要素(IPv4に関するADDRINFO)だけが返されます。このリストの先頭がai0変数により参照可能となります。

026:    /** ワイルドカードアドレスのADDRINFOリストを生成する */
027:    /** ai_familyにAF_UNSPECを指定しておくと、IPv6とIPv4 */
028:    /** のワイルドカードアドレスそれぞれを要素とした長さ */
029:    /** 2のリストが生成される */
030:    memset(&hints, 0, sizeof(hints));
031:    hints.ai_family = AF_UNSPEC;
032:    hints.ai_socktype = SOCK_STREAM;
033:    hints.ai_flags = AI_PASSIVE;
034:    if (e = getaddrinfo(NULL, servname, &hints, &ai0)) {
035:        fprintf(stderr, "%s\n", gai_strerror(e));
036:        exit(1);
037:    }

●39~74行目の処理。図2におけるブロック(2)の処理に該当します。このforループでは26~37行目の処理で得られたADDRINFO構造体のリストのすべての要素に対して、待ち受け用のソケットの生成を行っています。もしOSがIPv6, IPv4の両者をプロトコルスタックを持っていた場合はIPv6, IPv4の両方の待ち受けソケットが生成されることになります。第2回で紹介したTCPクライアントは、通信のために、ADDRINFO構造体リストの中から最初に接続できたソケットを1つだけ使いますが、サーバではリストのすべての要素についてソケットを生成します。

46~48行目でソケットの生成を行っています。socket()関数に対する引数はgetaddrinfo()関数が生成した変数をそのまま渡しているため、プログラマがプロトコルに関する情報をソースコード上に直に書くことはありません。52~55行目で、ワイルドカードアドレスをソケットにbindしています。ここでもプロトコルごとにソケットアドレスは異なるため(IPv6では::、IPv4は0.0.0.0)、本来はプログラマが処理を書き分けるのですが、ai_addr変数によってプロトコルの違いが完全に隠蔽されています。

61~65行目ではソケットをlisten()関数により待ち受け状態に遷移させ、クライアントからの接続を待ち受けます。

68行目で生成と待ち受け状態までの遷移を完了したソケットについてはその数をnsocks変数にカウントしています。通常、nsocksはADDRINFO構造体リストの要素数、すなわち通常はOSが持つプロトコルの数と等しくなります。

039:    /** ADDRINFOリストの各要素について、クライアントから */
040:    /** 接続を受け入れるソケットを生成する。ソケットの生 */
041:    /** 成に成功しても、処理を打ち切らず、リストのすべて */
042:    /** 要素について生成を行う */
043:    for (nsocks = 0, ai = ai0; ai; ai = ai->ai_next) {
044:        /* ソケットの生成を試みる。生成に失敗したら */
045:        /* ADDRINFOリストの次の要素で接続を試行する */
046:        s[nsocks] = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
047:        if (s[nsocks] == INVALID_SOCKET)
048:            continue;
049:
050:        /* ソケットをbindする。bindに失敗したら */
051:        /* ADDRINFOリストの次の要素で接続を試行する */
052:        if (bind(s[nsocks], ai->ai_addr, ai->ai_addrlen) == SOCKET_ERROR) {
053:            closesocket(s[nsocks]);
054:            s[nsocks] = INVALID_SOCKET;
055:            continue;
056:        }
057:
058:        /* ソケットをlisten状態に遷移させ、クライアントからの */
059:        /* 接続待ち受け状態に入る。listenに失敗したらADDRINFO */
060:        /* リストの次の要素で接続を試行する */
061:        if (listen(s[nsocks], 5) == SOCKET_ERROR) {
062:            closesocket(s[nsocks]);
063:            s[nsocks] = INVALID_SOCKET;
064:            continue;
065:        }
066:
067:        /* 待ち受けソケットの生成が成功したら、その数をカウントアップ */
068:        nsocks++;
069:

●75~81行目の処理。ひとつも待ち受けソケットが生成できなかった場合は、サーバとして機能できませんので処理を打ち切ります。通常ここでエラーが発生することはまれです。

076:    /** 待ち受けソケットをひとつも生成できない場合はnsocksは0である */
077:    /** IPv6, IPv4のいずれでも生成できないためサーバ処理を打ち切る  */
078:    if (nsocks == 0) {
079:        fprintf(stderr, "can not create listen socket with any protocol\n");
080:        exit(1);
081:    }

●83~86行目の処理。図2におけるブロック(3)の下準備を行う部分です。今回のサンプルコードではクライアントからの接続要求処理の多重化にselect()関数(※脚注)を使用しますが、WinSock APIでもUNIXのもつそれと同等の機能が提供されています。ここではrfd0に対して、接続要求が来たことを監視するソケットを登録します。IPv6が有効化されたWindows XPにおいて、通常はnsocksは2となります。ここでIPv6, IPv4のそれぞれに対する2つのソケットが監視対象として登録されます。

083:    /** クライアントからの接続を待ち受けるソケットをrfd0に登録 */
084:    FD_ZERO(&rfd0);
085:    for (i = 0; i < nsocks; ++i)
086:        FD_SET(s[i], &rfd0);

●88~123行目の処理。図2のブロック(3)に該当する処理です。90行目のwhile文が無限ループを構成しておりサーバはクライアントからの要求をひたすら受け続けます。99行目ではselect()関数は、83~86行目で登録したソケットのいずれかひとつが読み取り可能になるまで待ちます。この場合は別の言葉で言い換えると、IPv6, IPv4のいずれかの待ち受けソケットに対してクライアントからの接続要求があるまでselect()関数のところで待機します。

088:    /** クライアント接続があったら受け入れる */
089:    /** 1つの接続に対して、スレッドを1つ生成してデータの送受信を行う */
090:    while (1) {
091:        fd_set rfd;
092:        SOCKET iosock;
093:        SOCKADDR_STORAGE ss;
094:        int sslen;
095:
096:        /* IPv4, IPv6いずれかの待ち受けソケットに対して */
097:        /* クライアントから接続があるまで待つ */
098:        rfd = rfd0;
099:        if (select(FD_SETSIZE, &rfd, NULL, NULL, NULL) == SOCKET_ERROR) {
100:            fprintf(stderr, "select error\n");
101:            exit(1);
102:        }
 .
 .
 .
123:    }

●104~122行目の処理。IPv6, IPv4のどちらでもクライアントからの接続要求があると、99行目のselect()関数は待ちから復帰します。その際にどのソケットが読み取り可能になったか(接続要求が来たか)をrfdに記録します。107行目のFD_ISSET()マクロにより読み取り可能になったソケットを判別し、111行目でそのソケットへの接続要求を受理(accept)します。このときにaccept()関数が返す値が、データの受送信(I/O)を行うためのソケットです。これをiosock変数に代入しています。ところで、111行目の第2引数には接続してきたクライアントを示すソケットアドレスが格納されます。下記のコードでは93行目のSOCKADDR_STORAGE型の変数に値を格納させています。第2引数の意味の詳細については「SOCKADDR_STORAGE構造体について」の節で解説します。

118行目では実際にI/Oを行うスレッドを生成して、データの受送信(I/O)用のソケットの値(iosock変数)を引き渡しています。スレッドの実体はtcp_echo_io()関数です。クライアントからの接続要求があるたびに、スレッドがひとつ生成されます。UNIXならばスレッドの代わりにfork()によりプロセスを生成してI/Oを多重化することもよく行われています。

ここで述べた多重化の方法はIPv6プロトコルに依存するものではなく、IPv4専用のプログラムでも古くから行われている方法です。特定のプロトコルに依存するような記述はありません。

090:    while (1) {
091:        fd_set rfd;
092:        SOCKET iosock;
093:        SOCKADDR_STORAGE ss;
094:        int sslen;
 .
 .
104:        /* クライアントとデータを受送信するI/Oスレッドを生成し */
105:        /* 接続要求があったソケットをそのスレッドに引き渡す */
106:        for (i = 0; i < nsocks; ++i)
107:            if (FD_ISSET(s[i], &rfd)) {
108:
109:                /* クライアントからの接続を受け入れ */
110:                sslen = sizeof(ss);
111:                iosock = accept(s[i], (LPSOCKADDR)&ss, &sslen);
112:                if(iosock == INVALID_SOCKET) {
113:                    fprintf(stderr, "accept error\n");
114:                    exit(1);
115:                }
116:
117:                /* I/Oを行うスレッドを生成する */
118:                if (_beginthread(tcp_echo_io, 0, &iosock) == -1) {
119:                    fprintf(stderr, "can not create thread\n");
120:                    exit(1);
121:                }
122:            }
123:    }

●126~158行目の処理。118行目で生成されるデータ受送信(I/O)用のスレッドの本体です。クライアントからの接続要求が1つあるたびに、1つこのスレッドを生成します。処理の内容は非常に単純で、クライアントから文字列を受信し(141行目recv()関数)、その内容をそっくりそのまま返信(147行目send()関数)するだけです。

126:/** クライアントとI/Oを行うソケットを受け取り */
127:/** データの受送信を行うI/Oスレッドの本体 */
128:/** 引数のargはI/Oを行うソケットへのポインタ */
129:void tcp_echo_io(void *arg)
130:{
131:    SOCKET s;
132:    char buf[BUFSIZ];
133:    int bufsiz;
134:
135:    printf("start thread\n");
136:
137:    /** クライアントとI/Oを行うソケットの獲得 */
138:    s = *(SOCKET *)arg;
139:
140:    /** クライアントからの文字列を受信と返信(サーバからの送信)処理 */
141:    while ((bufsiz = recv(s, buf, sizeof(buf) - 1, 0)) != 0) {
142:        if (bufsiz == SOCKET_ERROR)
143:            break;
144:        buf[bufsiz] = '\0';
145:        printf("recv string: %s\n", buf);
146:
147:        if (send(s, buf, bufsiz, 0) == SOCKET_ERROR)
148:            break;
149:        printf("send string: %s\n", buf);
150:
151:    }
152:

>SOCKADDR_STORAGE構造体について

093:        SOCKADDR_STORAGE ss;
094:        int sslen;
 .
 .
109:                /* クライアントからの接続を受け入れ */
110:                sslen = sizeof(ss);
111:                iosock = accept(s[i], (LPSOCKADDR)&ss, &sslen);

111行目のaccept()関数において第2引数にはSOCKADDR_STORAGEへのポインタを指定していました。今回提示したサンプルプログラムではss変数は参照していませんが、接続してきたクライアントのIPアドレスを得る場合などは、クライアントのソケットアドレスの内容を参照する必要があります。さて111行目のaccept()関数が呼び出しが完了したとき、s[i]がIPv6かIPv4のソケットであるかによって、ssに設定される内容が異なります。

図4:IPv6とIPv4のソケットアドレスの比較
図4:IPv6とIPv4のソケットアドレスの比較

 図4はIPv4のソケットアドレスを格納するSOCKADDR_INと、IPv6のソケットアドレスを格納するSOCKADDR_IN6の構造を比較したものです。内部の構造も異なる上、SOCKADDR_IN6構造体のほうがサイズが大きいことがわかります。accept()は呼び出してみるまでIPv6かIPv4かどちらの接続を受け入れるはわかりませんので、第2引数に与えるバッファはSOCKADDR_IN6構造体を格納するのに十分な大きさを用意しておく必要があります。もし仮にSOCKADDR_INが収まるだけのバッファをaccept()関数の第2引数に渡して、呼び出しを行ってしまうと、SOCKADDR_IN構造体に後続するメモリ領域が破壊されてしまいます。

図5:SOCKADDR_STORAGEの構造
図5:SOCKADDR_STORAGEの構造

 ここで図5に示すSOCKADDR_STORAGE構造体を導入します。SOCKADDR_STORAGE構造体はOSが持つすべてのプロトコルのソケットアドレスをすっぽり収めるだけの大きさを持ちます。WinSock2ではSOCKADDR_STORAGEは128バイトの大きさを持つので、SOCKADDR_INでもSOCKADDR_IN6のいずれのソケットアドレスでも領域内に収めることができます。さらにSOCKADDR_STORAGEの先頭のメンバはss_familyですが、図4と比較するとプロトコル種別を示す領域は同一となっています。下記のようにss_familyを調べることでSOCKADDR_STORAGEの内部に格納されているソケットアドレスの種類を知ることが可能です。ポインタを各プロトコルに応じたソケットアドレスへキャストすることで、内部のメンバへアクセス可能となります。

LPSOCKADDR_STORAGE ss;
LPSOCKADDR_IN sin;
LPSOCKADDR_IN6 sin6;

if (ss->ss_family == AF_INET)
   sin = (SOCKADDR_IN *)ss
else if (ss->ss_family == AF_INET6)
   sin6 = (SOCKADDR_IN6 *)ss

ここでIPv6とIPv4だけを扱うプログラムなら111行目のaccept()関数にSOCKADDR_IN6構造体のポインタを渡せば十分であると思うかもしれません。IPv6とIPv4だけをサポートするならば、それでも機能はしますが今回の解説で目指しているものはプロトコルに依存しないプログラムの作成です。もし仮に将来IPv7が出現してソケットアドレスの大きさ(sizeof(SOCKADDR_IN7))が100バイトになったとします。するとSOCKADDR_IN6のサイズは28バイトしかありませんのでメモリ破壊を引き起こします。SOCKADDR_STORAGE構造体はOSが持つあらゆるソケットアドレスをすっぽり格納できることを保証しますので、将来、新たなプロトコルのソケットアドレスが出現してもサイズの問題は生じません。

TCP echoプログラムの動作確認

 第2回で作成したクライアントプログラム(tcp-echo-client.exe)と、今回作成したサーバプログラムを組にして動作させて見ます。まず図9に示す手順でリスト1のtcp-echo-server.cのコンパイルを行ってください。tcp-echo-serverはスレッドを使っているため、マルチスレッド用ライブラリをリンクする必要があります。コンパイラのスイッチに"/MT"を付加してください。

 異なる2台のWindows XP PCを準備できれば理想ですが、1台のWindows XPでコマンドプロンプトを2つ開き、それぞれサーバとクライアントを立ち上げることも可能です。この場合はクライアントにサーバのアドレスとしてループバックアドレス(IPv6は"::1", IPv4では"127.0.0.1")を与えます。またtcp-echo-serverについてはWindows XP SP2上で初めて起動するとき図6の画面が表示されますので、Windows ファイヤウォールのブロック解除を行ってください。図8はtcp-echo-serverとtcp-echo-clientプログラムが文字列をやり取りしている様子です。IPv6, IPv4いずれでも動作しています。(参照:簡易 TCP/IP サービスについて)

図6:Windows セキュリティの重要な警告
図6:Windows セキュリティの重要な警告

図7:ネットワークサービス画面
図7:ネットワークサービス画面

図8:TCP echoプログラムの実行例
図8:TCP echoプログラムの実行例

第3回のまとめ

 今回IPv6に対応したTCP echoサーバを作成しましたが、IPv6に特化した設計を行ったわけではなく、プロトコルに依存する部分を極力排除するプログラム作成を行いました。プロトコルに非依存なプログラムを作成することで将来の出現するかもしれないプロトコルへの対応も容易となり、さらにプログラムがより簡潔に記述できることもわかりました。

 次回はWindowsプログラミングでは欠かせない、ウィンドウメッセージを利用した、非同期I/Oを用いてTCP echoクライアントとサーバを作成する方法を紹介します。

リスト1:tcp-echo-server.c
001:#include ‹winsock2.h›
002:#include ‹ws2tcpip.h›
003:#include ‹stdio.h›
004:#include ‹process.h› /* for _beginthread() */
005:
006:void tcp_echo_io(void *arg);
007:
008:int main(int argc, char *argv[])
009:{
010:    WSADATA wsaData;
011:    int i;
012:    char *servname = "echo";
013:    ADDRINFO hints;
014:    LPADDRINFO ai, ai0;
015:    int e;
016:    SOCKET s[64];
017:    int nsocks;
018:    fd_set rfd0;
019:        
020:    /** WinSockの初期化 */
021:    if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
022:        fprintf(stderr, "can not initilize WinSock\n");
023:        exit(1);
024:    }
025:
026:    /** ワイルドカードアドレスのADDRINFOリストを生成する */
027:    /** ai_familyにAF_UNSPECを指定しておくと、IPv6とIPv4 */
028:    /** のワイルドカードアドレスそれぞれを要素とした長さ */
029:    /** 2のリストが生成される */
030:    memset(&hints, 0, sizeof(hints));
031:    hints.ai_family = AF_UNSPEC;
032:    hints.ai_socktype = SOCK_STREAM;
033:    hints.ai_flags = AI_PASSIVE;
034:    if (e = getaddrinfo(NULL, servname, &hints, &ai0)) {
035:        fprintf(stderr, "%s\n", gai_strerror(e));
036:        exit(1);
037:    }
038:
039:    /** ADDRINFOリストの各要素について、クライアントから */
040:    /** 接続を受け入れるソケットを生成する。ソケットの生 */
041:    /** 成に成功しても、処理を打ち切らず、リストのすべて */
042:    /** 要素について生成を行う */
043:    for (nsocks = 0, ai = ai0; ai; ai = ai->ai_next) {
044:        /* ソケットの生成を試みる。生成に失敗したら */
045:        /* ADDRINFOリストの次の要素で接続を試行する */
046:        s[nsocks] = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
047:        if (s[nsocks] == INVALID_SOCKET)
048:            continue;
049:
050:        /* ソケットをbindする。bindに失敗したら */
051:        /* ADDRINFOリストの次の要素で接続を試行する */
052:        if (bind(s[nsocks], ai->ai_addr, ai->ai_addrlen) == SOCKET_ERROR) {
053:            closesocket(s[nsocks]);
054:            s[nsocks] = INVALID_SOCKET;
055:            continue;
056:        }
057:
058:        /* ソケットをlisten状態に遷移させ、クライアントからの */
059:        /* 接続待ち受け状態に入る。listenに失敗したらADDRINFO */
060:        /* リストの次の要素で接続を試行する */
061:        if (listen(s[nsocks], 5) == SOCKET_ERROR) {
062:            closesocket(s[nsocks]);
063:            s[nsocks] = INVALID_SOCKET;
064:            continue;
065:        }
066:
067:        /* 待ち受けソケットの生成が成功したら、その数をカウントアップ */
068:        nsocks++;
069:
070:        /* 生成した待ち受けソケットのプロトコル種別を表示する */
071:        printf("create %s listen socket\n",
072:               (ai->ai_family == AF_INET)  ? "IPv4" :
073:               (ai->ai_family == AF_INET6) ? "IPv6" : "Unknown");
074:    }
075:
076:    /** 待ち受けソケットをひとつも生成できない場合はnsocksは0である */
077:    /** IPv6, IPv4のいずれでも生成できないためサーバ処理を打ち切る  */
078:    if (nsocks == 0) {
079:        fprintf(stderr, "can not create listen socket with any protocol\n");
080:        exit(1);
081:    }
082:
083:    /** クライアントからの接続を待ち受けるソケットをrfd0に登録 */
084:    FD_ZERO(&rfd0);
085:    for (i = 0; i < nsocks; ++i)
086:        FD_SET(s[i], &rfd0);
087:
088:    /** クライアント接続があったら受け入れる */
089:    /** 1つの接続に対して、スレッドを1つ生成してデータの送受信を行う */
090:    while (1) {
091:        fd_set rfd;
092:        SOCKET iosock;
093:        SOCKADDR_STORAGE ss;
094:        int sslen;
095:
096:        /* IPv4, IPv6いずれかの待ち受けソケットに対して */
097:        /* クライアントから接続があるまで待つ */
098:        rfd = rfd0;
099:        if (select(FD_SETSIZE, &rfd, NULL, NULL, NULL) == SOCKET_ERROR) {
100:            fprintf(stderr, "select error\n");
101:            exit(1);
102:        }
103:
104:        /* クライアントとデータを受送信するI/Oスレッドを生成し */
105:        /* 接続要求があったソケットをそのスレッドに引き渡す */
106:        for (i = 0; i < nsocks; ++i)
107:            if (FD_ISSET(s[i], &rfd)) {
108:
109:                /* クライアントからの接続を受け入れ */
110:                sslen = sizeof(ss);
111:                iosock = accept(s[i], (LPSOCKADDR)&ss, &sslen);
112:                if(iosock == INVALID_SOCKET) {
113:                    fprintf(stderr, "accept error\n");
114:                    exit(1);
115:                }
116:
117:                /* I/Oを行うスレッドを生成する */
118:                if (_beginthread(tcp_echo_io, 0, &iosock) == -1) {
119:                    fprintf(stderr, "can not create thread\n");
120:                    exit(1);
121:                }
122:            }
123:    }
124:}
125:
126:/** クライアントとI/Oを行うソケットを受け取り */
127:/** データの受送信を行うI/Oスレッドの本体 */
128:/** 引数のargはI/Oを行うソケットへのポインタ */
129:void tcp_echo_io(void *arg)
130:{
131:    SOCKET s;
132:    char buf[BUFSIZ];
133:    int bufsiz;
134:
135:    printf("start thread\n");
136:
137:    /** クライアントとI/Oを行うソケットの獲得 */
138:    s = *(SOCKET *)arg;
139:
140:    /** クライアントからの文字列を受信と返信(サーバからの送信)処理 */
141:    while ((bufsiz = recv(s, buf, sizeof(buf) - 1, 0)) != 0) {
142:        if (bufsiz == SOCKET_ERROR
143:            break;
144:        buf[bufsiz] = '\0';
145:        printf("recv string: %s\n", buf);
146:
147:        if (send(s, buf, bufsiz, 0) == SOCKET_ERROR)
148:            break;
149:        printf("send string: %s\n", buf);
150:
151:    }
152:
153:    printf("end thread\n");
154:
155:    /** I/O用ソケットを閉じスレッドを終了させる */
156:    closesocket(s);
157:    _endthread();
158:}
図9:tcp-echo-server.cのコンパイル方法
C:\>cl /MT tcp-echo-server.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-server.c
Microsoft (R) Incremental Linker Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:tcp-echo-server.exe
tcp-echo-server.obj
ws2_32.lib

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

WinSock APIでもUNIX系のOSの持つselect()関数が利用可能ですが、I/Oを監視できる対象はソケットのみです。UNIXではファイルI/OとソケットのI/Oを区別することなく監視対象として登録することができますが、WinSock APIではできません。Windowsでのselect()関数はWinSock APIの一部として扱われているためです。


簡易 TCP/IP サービスについて

Windows XPには「簡易 TCP/IP サービス」機能(図7)があります。このサービスにはTCP echoサーバの機能も含まれているため(IPv4のみ)、今回作成したTCP echoサーバプログラムと同時に動かと衝突を起こしてしまいます。「簡易TCP/IP サービス」サービスをアンインストールするか無効化してください。

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

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

IPv6ブログ