logo
Published on IPv6style (http://www.ipv6style.jp)

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

By admin
作成日時 2005-12-12 00:00
第4回 非同期I/Oを使ったTCP echoクライアントプログラムの作成
(2005.12.12)

NTT情報流通プラットフォーム研究所
加藤淳也

はじめに

 第4回では非同期I/Oを用いたソケットの扱い方を解説します。これまで取り上げたTCP echoプログラムはサーバ、クライアントともにコンソール上で動作するものでした。UNIXで経験を積んだプログラマには理解しやすかったと思います。今回は、よりWindowsらしいプログラムを作成するため、GUIによる操作を行うTCP echoクライアントを構築します。特にソケットの操作のために非同期型とよばれるアクセスモデルを使い、ウインドウメッセージの処理方法を中心に解説します。

 Windowsで動作するほとんどのプログラムは、通常GUIを持っており、プログラム構造は事象駆動型(イベントドリブン)のモデルを採用しています。あるアプリケーションが、図1に示すようにメインウインドウとボタンで構成されていたとします。

図:1 イベントの発生とウインドウプロシージャの呼び出し
図:1 イベントの発生とウインドウプロシージャの呼び出し [0]

 あらかじめメインウインドウにはイベントが発生したときに呼び出されるウインドウプロシージャ(コールバック関数)を登録しておきます。ウインドウ上で何らかのイベントが発生するとウインドウプロシージャが呼び出されます。イベントとは「ボタンが押された」「マウスポインタが入った」「ウインドウをリサイズした」「×ボタンを押してアプリケーションを終了しようとした」など、GUIにまつわる操作が例として挙げられます。イベントが発生するとアプリケーションの実行とは非同期にコールバック関数が呼び出されます。このときにどんなイベントが発生したか、その種類を知らせるためにウインドウプロシージャに対してウインドウメッセージが送られます。ウインドウプロシージャは受け取ったウインドウメッセージの中身を調べて、アプリケーションの動きを決めます。例えば「ボタンが押された」ことを示すメッセージを受信したならば、あらかじめボタンの押下に関連付けられたアクションを実行します。

同期型のソケットについて

 第3回までに扱ってきたソケットI/Oはすべて、同期型の(ブロッキング)ソケットと呼ばれるアクセスモデルに基づきます。もし仮にネットワークやサーバの負荷が著しく高く、サーバへの接続を開始してから接続完了までに1分近くかかったとします。同期型のソケットでは、図2の処理1が終了してから、処理2が開始されるまでプログラムは1分間待たされることになります。

図2:ブロッキングが発生するときの処理の流れ
図2:ブロッキングが発生するときの処理の流れ

 GUIを使ったプログラムではこのソケット処理におけるブロックが大きな問題を引き起こします。例えばソケットのI/Oが行われている最中は、他の処理が行われませんので、GUIが完全にフリーズする可能性があります。例えば次のような状況を考えてみてください。WebブラウザのアドレスバーにURLを入力後Enterを押下し、サーバと接続するまでに1分かかるとします。その1分の間はすべてのGUIの操作がフリーズしまい、読み込み中止ボタンを押すことも、ウインドウのリサイズや移動すらもできなくなってしまったら、GUIアプリケーションとして操作性が著しく損なわれてしまいます。したがって、ソケットのI/OとGUIの処理は多重化し同時に行う必要があります。

非同期I/Oによるソケットアクセスとウインドウメッセージの関係

 Windowsでは非同期I/Oによるソケットのアクセスモデルを提供しています。このモデルを使うと、ソケット操作関数を呼び出してもブロッキングが発生しません。ソケット操作関数を呼出した直後、直ちに復帰します。図3において処理1の後に呼び出されるconnect()関数は、実際の接続に時間がかかっても(つまり接続途中であっても)直ちに呼び出しから復帰します。

図3:非ブロッキングソケットによる処理の流れ
図3:非ブロッキングソケットによる処理の流れ

 しかし、処理途中の状態で関数が戻ってきてしまうと接続処理の結末が成功なのか失敗なのか判別することができません。そこで、WinSockはconnect()関数の処理が完全に終了した段階で、接続処理の成否を記したウインドウメッセージを非同期にアプリケーションに対して通知します。ウインドウプロシージャがメッセージを受け取り、内容を調べることでconnect()関数の成否を知ることが可能です。

 図4に非同期ソケットの処理例を示します。ボタンを押すとサーバへの接続開始処理が行われるアプリケーションです。ボタンが押下されると、「ボタンがクリックされた」ことを示すメッセージがウインドウプロシージャに通知されます(1)。ボタンに関連づけられたアクションとして接続開始処理が呼び出されます(2)。サーバへの接続処理を行いますがconnect()関数からは直ちに復帰しGUIの処理へ戻ります(3)。一定時間経過後に接続処理が終了すると、完了を示すイベントが発生し再度ウインドウプロシージャが呼び出されます(4)。このように非同期I/Oによるアクセスモデルは、GUI上で発生するイベントとソケットI/Oに基づくイベントを完全に統合して扱うことができ、GUIプログラムとの親和性がよいことがわかります。

図4: 非同期ソケットの処理例
図4: 非同期ソケットの処理例 [0]

非同期ソケットでの各関数の挙動

 非同期型ソケットを使用する場合、I/Oにより下記のウインドウメッセージが発生します。I/Oの種類ごとに同期・非同期を選択することができ、例えば、connect()関数は非同期に動作させるが、他の関数は同期的にブロックする動作をさせることも可能です。

非同期I/Oを使ったTCP echoクライアントの基本設計

図5:非同期I/O版TCP echoクライアントのスクリーンショット
図5:非同期I/O版TCP echoクライアントのスクリーンショット

 図5に今回作成するTCP echoクライアントのスクリーンショットを示します。このアプリケーションの動きは、TCP echoサーバに対して、Stringエデットボックスに記入された文字列を送信し、送り返されて(エコーバックされて)きた文字列を画面下部に表示するものです。送信開始のトリガはSENDボタンです。

 送信ボタンを押してから、エコーバックされた文字列を画面に表示するまでの大まかな流れは図6に示したとおりです。今回は簡単のため、非同期に動作する関数は connect(), recv()の2つのみとします。

図6:非同期I/O版TCP echoクライアントのおおまかな構造
図6:非同期I/O版TCP echoクライアントのおおまかな構造 [0]

 SENDボタンがクリックによりウインドウメッセージが発生してから、通信が完了するまでの流れは(1)~(8)までの通りです。

  1. SENDボタンがクリックされる
  2. 「名前解決処理」と実行する。ホスト名はServerエデットボックスから読み取って、名前解決の結果を元に接続処理の実行へ遷移する
  3. 接続処理の結果を待たずに、すぐにGUI処理へ復帰する
  4. 接続処理(connect()関数)が終了するとイベントが発生しウインドウプロシージャの該当する処理が呼び出される
  5. 送信処理(send()関数)を実行する。送信する文字列はStringエデットボックスから読み取る
  6. サーバからエコーバックが送り返されてきたため、受信バッファにデータが到着する。到着を示すイベントが発生しウインドウプロシージャの該当する処理が呼び出される
  7. 受信処理(recv()関数)を実行し、受信バッファから文字列を取り出す
  8. 受信文字列を画面に描画しGUI処理に戻る

ウインドウメッセージの構成

 ここでソケットI/Oによって発生するウインドウメッセージの構造を解説します。ウインドウメッセージはメッセージの種別のほかにパラメータを2つとることが可能です。図7にはソケットI/Oによるウインドウメッセージのパラメータ構造を示しました。あらかじめ指定したソケットI/Oが発生するとWM_SOCKET(※注2 [0])というメッセージが発生します。

図7:ソケットI/Oによるウインドウメッセージのパラメータ構造
図7:ソケットI/Oによるウインドウメッセージのパラメータ構造 [0]

 このとき、どのソケットでどんなI/Oが発生したか、またその結果が成功か失敗かを2つのパラメータ(wParam, lParam)で通知してくれます。wParam,lParamはウインドウメッセージの種類によって書き込まれるデータ構造が異なりますが、ソケットI/Oに関するメッセージの場合

SOCKET s = (SOCKET)wParam;
として該当のソケットを取り出し、
WSAGETSELECTERROR(lParam)マクロによりエラー状態
WSAGETSELECTEVENT(lParam)マクロによりメッセージ内容

を取り出すことができます。

ソースコードについての解説

非同期I/O版TCP echoクライアントのソースコードとコンパイル方法

 リスト1に非同期I/O版TCP echoクライアント全体のソースコードを、図8にコンパイル方法を示します。GUIを使ったプログラムなのでインポートライブラリとしてuser32.libとcomctl32.libをリンクしています。後者のコモンコントロールは、ステータスバーの表示のために使っています。

アプリケーションの立ち上げ時のウインドウの初期化処理

●85~103行目の処理。WindowsではWinMain()関数からアプリケーションの実行がスタートします。ここではWinSockの初期化(94行目WSAStartup()関数)、ウインドウクラスの登録(98行目InitApplication()関数)、ウインドウの作成と表示(102行目InitInstance()関数)を行っています。

080:/** Windowsプログラムのエントリポイント */
081:/*
082: * WinSockの初期化とGUIの初期化後にメッセージ
083: * 処理のためのメインループに入る
084: */
085:int WINAPI WinMain(HINSTANCE hInstance,
086:                   HINSTANCE hPrevInstance,
087:                   LPSTR lpCmdLine,
088:                   int nCmdShow)
089:{
090:    WSADATA wsaData;
091:    MSG msg;
092:
093:    /* WinSockの初期化処理 */
094:    if (WSAStartup(MAKEWORD(2, 2), &wsaData))
095:        return FALSE;
096:
097:    /* ウインドウクラスの登録 */
098:    if (!InitApplication(hInstance)) 
099:        return FALSE;
100:   
101:    /* GUIウインドウの作成と表示処理 */
102:    if (!InitInstance(hInstance, nCmdShow))
103:        return FALSE;
 .
 .
112:}

●116~133行目の処理。WinMain()関数から呼びされるInitApplication()関数の内部です。ここではウインドウクラスの登録を行います。ウインドウクラスとはウインドウの性質を規定するもので、アプリケーションの名前の定義や、アイコンやマウスカーソルの指定などを行います。また今回重要な点は、イベントが発生したときのアクションをWindowsに登録する点です。122行目でウインドウプロシージャの本体であるWndProc()関数へのポインタをウインドウクラスのパラメータとして設定しています。132行目のRegisterClass()関数によりウインドウクラスの登録を終えると、イベント発生時にはWndProc()関数が非同期的に呼び出されることになります。

115:/** ウインドウクラスの登録処理 */
116:BOOL InitApplication(HANDLE hInstance)
117:{
118:    WNDCLASS wc;
119:
120:    wc.style = CS_HREDRAW | CS_VREDRAW;
121:    /* ウィンドウプロシージャを登録する */
122:    wc.lpfnWndProc = (WNDPROC)WndProc;
123:    wc.cbClsExtra = 0;
124:    wc.cbWndExtra = 0;
125:    wc.hInstance = hInstance;
126:    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
127:    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
128:    wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW;
129:    wc.lpszMenuName =  NULL;
130:    wc.lpszClassName = TEXT("Async I/O TcpEchoClient");
131:
132:    return RegisterClass(&wc);
133:}

● 137~234行目の処理。CreateWindow()関数郡により、ウインドウを作成して画面に表示しています。図5で表示されているGUIはここで形成されます。

136:/** ウインドウの作成と表示処理 */
137:BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
138:{
139:    HWND hWnd;
140:
141:    g_hInst = hInstance; // グローバル変数にインスタンス処理を格納します。
142:
143:    /* 親ウインドウの作成 */
144:    hWnd = CreateWindow(TEXT("Async I/O TcpEchoClient"), ...);
 .
157:    /* 親ウィンドウ内にコントロールを作成する */
158:    hServerLabel = CreateWindow(TEXT("STATIC"), TEXT("Server"), ...);
166:    hServerEdit = CreateWindowEx(WS_EX_CLIENTEDGE, ...);
176:    hSendButton = CreateWindow(TEXT("BUTTON"), TEXT("SEND"), ...);
185:    hStringLabel = CreateWindow(TEXT("STATIC"), TEXT("String"), ...);
193:    hStringEdit = CreateWindowEx(WS_EX_CLIENTEDGE, ...);
203:    hRecvEdit = CreateWindowEx(WS_EX_CLIENTEDGE, ...);
217:    hStatusLabel = CreateWindowEx(0, ...);
 .
 .
 .
228:    SetFocus(hServerEdit);
229:
230:    ShowWindow(hWnd, nCmdShow);  /* ウインドウの表示 */
231:    UpdateWindow(hWnd);          /* ウインドウの最初の更新 */
232:
233:    return TRUE;
234:}

●106~109行目の処理。WinMain()関数の処理に戻ります。106行目のwhile()ループはイベント処理ループです。このループに突入するとユーザから与えられる入力や、ソケットI/Oなどのイベントを待ち受ける状態となります。イベントは、アプリケーションにフォーカスがあたる、アイコン化されるなど、プログラマが意識をしていない場面でも随時発生しています。デフォルトのアクションが内部で呼び出されて適切な処理が行われています。次にプログラマが処理すべきアクションはSENDボタンの押下とソケットI/Oです。この処理だけはウインドウプロシージャ内にプログラマが独自に処理を書き加えます。

085:int WINAPI WinMain(HINSTANCE hInstance,
086:                   HINSTANCE hPrevInstance,
087:                   LPSTR lpCmdLine,
088:                   int nCmdShow)
089:{
 .
 .
 .
105:    /* ウインドウメッセージ処理 */
106:    while (GetMessage(&msg, NULL, 0, 0)) {
107:        TranslateMessage(&msg);
108:        DispatchMessage(&msg);
109:    }
110:
111:    return (int)msg.wParam;
112:}
SENDボタン押下時に発生するウインドウメッセージ処理

●242~254行目の処理。次にユーザがSENDボタンが押下したとします。このときServerエデットボックスにサーバ名が、Stringエデットボックスに送信する文字列が入力されているものとします。SENDボタンが押されるとイベント(WM_COMMAND)が発生し、ウインドウプロシージャWndProc()が呼び出されます。251行目でウインドウメッセージの内容(uMsg)を調べると、ボタン押下(WM_COMMAND)であることがわかるので、252行目の分岐に処理が進み、さらに253~254行目でボタン名を特定します。

237:/** ウインドウプロシージャの本体 */
238:/*
239: * ボタン押下時のイベント、ソケットI/Oの処理が発生した時に、
240: * ウインドウメッセージがuMsg変数によって通知される。
241: */
242:LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
243:{
 .
 .
250:    /* 受信したウインドウメッセージの種類(uMsg)に応じて処理を振分け */
251:    switch (uMsg) {
252:    case WM_COMMAND:
253:        switch (LOWORD(wParam)) {
254:        case ID_SEND:

●256~261行目の処理。まず、ボタン押下直後はGUIに対する操作を行います。SENDボタンを無効化して、通信処理が行われている間は二重にボタンを押下できない状態とします。また、GUI最下段ステータスバーに「名前解決中」であることを示す通信状態を表示します。

255:            /* SENDボタンを無効化する */
256:            EnableWindow(hSendButton, FALSE);
257:
258:            /* ステータスバーに「名前解決中」の状態を表示する */
259:            SendMessage(hStatusLabel, SB_SETTEXT,
260:                        (WPARAM)(255 | 0),
261:                        (LPARAM)TEXT("Status: resolving servername"));

●264~269行目の処理。サーバ名を示す文字列をServerエデットボックスから取り出し、buf変数で参照可能な状態とします。Serverエデットボックスが空の場合はエラーを表示し初期状態に戻ります。

263:            /* Serverエデットボックスからサーバ名を取り出す */
264:            SendMessage(hServerEdit, WM_GETTEXT,
265:                        (WPARAM)sizeof(buf), (LPARAM)buf);
266:            if (strlen(buf) == 0) {
267:                NotifyError(hWnd, TEXT("invalid server name or address"));
268:                break;
269:            }

●272~278行目の処理。サーバ名(buf)の名前解決を行います。サーバ名が解決できない場合はエラーを表示し初期状態に戻ります。getaddrinfo()関数が成功した場合、g_ai0変数にADDRINFO構造体のリストがセットされます。

271:            /* サーバ名の名前解決を行う */
272:            memset(&hints, 0, sizeof(hints));
273:            hints.ai_family = AF_UNSPEC;
274:            hints.ai_socktype = SOCK_STREAM;
275:            if (getaddrinfo(buf, "echo", &hints, &g_ai0)) {
276:                NotifyError(hWnd, TEXT("server not found"));
277:                break;
278:            }
connect()関数による接続処理とウインドウメッセージの処理

●281~283行目の処理。名前解決が成功したら、引き続き接続処理に移ります。まずGUI最下段ステータスバーにconnect()関数が処理中であることを示す「接続中」の状態を表示しておきます。

280:            /* ステータスバーに「接続中」の状態を表示する */
281:            SendMessage(hStatusLabel, SB_SETTEXT,
282:                        (WPARAM)(255 | 0),
283:                        (LPARAM)TEXT("Status: connecting server"));

●286~293行目の処理。ConnectServer()関数を呼び出した直後に、293行目のbreak文を通して、ウインドウプロシージャを直ちに抜け出しGUIの処理に戻ります。ConnectServer()関数の内部は後述しますが、この関数はWinSockに接続処理を任せて、自分自身はすぐに復帰します。よってブロックが発生することはありません。

 なお、g_ai変数はは現在接続を獅ンているADDRINFOリストの要素として扱っています。初期値としてgetaddrinfo()関数が返したリストの先頭(g_ai0)を設定しておきます。今後、ConnectServer()関数内部でADDRINFOリストの先頭の要素から順に接続を試行する処理が行われます。ConnectServer()関数はTRUEかFALSEのBOOL値を返します。TRUEの場合はconnect()の処理が完了していないか、現在のg_aiの後ろにADDRINFO構造体の要素が残っている場合です。つまりTRUEが返ってきた場合は、接続処理の結果を待っている状態か、また接続を試行すべき対象がまだ残っているため致命的なエラーとして扱いません。一方、FALSEが返された場合は、g_aiの後ろに、接続を試行すべきADDRINFO構造体の要素が残っていません。IPv6→IPv4のフォールバックもすべての候補に対して接続を試みたものの、どれもconnect()できなかった状態です。FALSEが返った場合は致命的なエラーとして扱い、初期状態へ戻ります。

285:            /* 接続処理を開始する */
286:            g_ai = g_ai0;
287:            if (ConnectServer(hWnd) == FALSE) {
288:                NotifyError(hWnd, TEXT("connect error"));
289:                freeaddrinfo(g_ai0);
290:                break;
291:            }
292:
293:            break;

●433~478行目の処理。ConnectServer()関数の内部について解説にします。437行目の判定は現在注目しているADDRINFOリストが末尾に達しており、処理すべき残りの要素がないことを調べています。この場合は、接続処理はこれ以上継続できません。致命的な状態としてFALSEを返します。

423:/** サーバへの接続を行う処理 */
424:/* 戻り値
425: *   TRUE:  接続が成功した場合、もしくは接続処理が行われており結果が
426:            確定していない場合。後者の場合はウインドウメッセージ
427:            (FD_CONNECT)の結果をウインドウプロシージャWndProc内で検査
428:            することで接続処理の成否を判定する
429:     FALSE: ADDRINFOリストg_ai0のすべての要素に対して接続を試行したが、
430:            失敗した。FALSEが返された場合はIPv6→IPv4のフォールバック
431:            にも完全に失敗している
432: */
433:BOOL ConnectServer(HWND hWnd)
434:{
435:    SOCKET s;
436:    
437:    if (g_ai == NULL)
438:        return FALSE;
 .
 .
 .
478:}

●440~453行目の処理。このforループは第2回に紹介したTCP echoクライアントの構造と基本的に同じです。ADDRINFOリスト(g_ai0)の先頭から順に接続試行を行う点は基本的に変わりありません。非同期I/Oの場合は、448~449行目で生成したソケットを非同期モードに移行させています。WSAAsyncSelect()関数はソケットsに関する操作のうち、どのI/Oを非同期型に設定するか指定します。448~449行目の例では、第3引数にメッセージ名としてWM_SOCKET、第4引数にFD_CONNECT | FD_READを指定していますので、

  1. connect()処理が完了したとき
  2. プロトコルスタックの受信バッファにデータが到着したとき

 上記の1., 2.の事象が発生したときにウインドウメッセージ(WM_SOCKET)が発生します。なおこWM_SOCKETはプログラマ自身がユーザ定義可能なウインドウメッセージをベースに定義を与えています(47行目)。

046:/** ソケットI/Oに関するウインドウメッセージの定義 */
047:#define WM_SOCKET        (WM_USER+1)
 .
 .
 .
440:    for ( ; g_ai; g_ai = g_ai->ai_next) {
441:
442:        /* ソケットの生成 */
443:        s = socket(g_ai->ai_family, g_ai->ai_socktype, g_ai->ai_protocol);
444:        if (s == INVALID_SOCKET)
445:            continue;
446:
447:        /* ソケットを非同期モードにする */
448:        if (WSAAsyncSelect(s, hWnd, WM_SOCKET,
449:                           FD_CONNECT | FD_READ) == SOCKET_ERROR) {
450:            closesocket(s);
451:            s = INVALID_SOCKET;
452:            continue;
453:        }

●456~468行目の処理。connect()関数の処理も非同期I/Oでは重要な部分です。一見すると同期型のソケットの処理とほぼ同じように見えますが、conenct()関数においてブロックは発生せず、接続処理の途中であっても直ちに関数から復帰します。もしconnect()関数が戻り値としてエラー(SOCKET_ERROR)を返してきても、WSAGetLastError()関数(参照:WinSockとバークレーソケットの違い [0])を用いて詳細なエラーの状態を調べます。得られた状態がWSAEWOULDBLOCKであった場合は、connect()がまだバックグラウンドで処理を進めていることを示しています。接続処理の成否はこの時点では不明ですのでエラーとして扱ってはいけません。何もせずにConnectServer()関数から復帰しconnect()関数の処理結果(WinSockからのウインドウメッセージ(WM_SOCKET))を待ちます。なおConnectServer()関数から復帰すると、ウインドウプロシージャ(WndProc()関数)からもすぐに抜け出すので106~109行目のイベント処理ループへすぐに復帰します。

455:        /* 接続処理 */
456:        if (connect(s, g_ai->ai_addr, g_ai->ai_addrlen) == SOCKET_ERROR)
457:            /* エラーが返ってきても、エラーコードがWSAEWOULDBLOCK */
458:            /* ならば接続処理がバックグラウンドで行われている状態 */
459:            /* である。ウインドウメッセージ(FD_CONNECT)の到着を待 */
460:            /* つ。その他のエラーコードならば、このg_aiについての */
461:            /* 処理を打ち切る */
462:            if (WSAGetLastError() != WSAEWOULDBLOCK) {
463:                closesocket(s);
464:                s = INVALID_SOCKET;
465:                continue;
466:            }
467:
468:        break;
接続完了を知らせるウインドウメッセージWM_SOCKET(FD_CONNECT)の処理

 connect()の処理が終了するとWM_SOCKETウインドウメッセージが発生しますが、パラメータを2つ取っています。それぞれwParam, lParam変数で表現されます。これをウインドウプロシージャ(WndProc()関数)で処理する様子を追っていきます。

●300~317行目の処理。wParam変数にはソケットの記述子が渡されていますので、303行目のようにSOCKET型にキャストして記述子を取り出すことができます。また、309~310行目ではソケットI/Oが成功で終わったのか、失敗で終わったのかを判別しています。WSAGETSELECTERROR(lParam)マクロの値が0以外ならば、I/Oは失敗しています。WSAGETSELECTEVENT(lParam)マクロはI/Oの種類を取り出すことができます。通常はソケットI/Oにエラーが生じた場合は処理を打ち切り初期状態に戻しますが、connect()関数だけはエラーが発生しても処理を継続させています(310行目)。これはフォールバック(IPv6で接続できなければ、IPv4で接続しなおす)動作を実現するため、接続処理の結果の判定を316行目以降の分岐の中で行わせるためです。

242:LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
243:{
 .
251:    switch (uMsg) {
 .
300:    case WM_SOCKET:
301:        /** ソケットI/Oに関するイベントが発生した */
302:
303:        /* ソケットの記述子を取り出し */
304:        s = (SOCKET)wParam;
305:
306:        /* connect()関数以外のソケットI/Oの処理結果を調べる */
307:        /* WSAGETSELECTERROR(lParam)が非0(失敗時)は初期状態 */
308:        /* に戻る */
309:        if (   WSAGETSELECTERROR(lParam) != 0
310:            && WSAGETSELECTEVENT(lParam) != FD_CONNECT) {
311:            closesocket(s);
312:            NotifyError(hWnd, TEXT("socket error"));
313:            break;                       
314:        }
315:
316:        switch (WSAGETSELECTEVENT(lParam)) {
317:        case FD_CONNECT:

●320~336行目の処理。connect()関数の処理を行うブロックについて解説します。310~335行目のブロックはconnect()関数の処理結果が成功か失敗か判別する処理です。320行目でもし失敗と判定された場合はフォールバック処理が行われます。324行目ですでに接続に失敗してしまったソケットを閉じ、327~333行目で接続試行の対象を次のADDRINFOリストの要素に移し、再度ConnectServer()関数を呼び出します。ここでもConnectServer()関数はブロックしませんので、335行目のbreak文を通じてウインドウプロシージャを脱出し、再びWM_SOCKETウインドウメッセージ(種別FD_CONNECT)の到着を待つ状態に遷移します。

318:            /** 接続処理が完了したことが通知された */
319:
320:            if (WSAGETSELECTERROR(lParam) != 0) {
321:                /** connect()が失敗に終わったため、ソケットを閉じ再試行する */
322:
323:                /* 接続できなかったソケットを閉じる */
324:                closesocket(s);
325:
326:                /* 接続を再試行し、再度ウインドメッセージ(FD_CONNECT)を待つ */
327:                g_ai = g_ai->ai_next;
328:                if (ConnectServer(hWnd) == FALSE) {
329:                    closesocket(s);
330:                    NotifyError(hWnd, TEXT("connect error"));
331:                    freeaddrinfo(g_ai0);
332:                    break;
333:                }
334:
335:                break;
336:            }

●338~342行目の処理。338行目以降に処理が到達した場合は、connect()処理が成功して終了したことを示していますので、サーバに接続ができたことになります。すでに接続処理を終えていますので、342行目でADDRINFOリストを開放しておきます。

338:            /** この地点に到達していれば接続処理 */
339:            /** (connect()関数)が成功している */
340:
341:            /* ADDRINFOリストを開放する */
342:            freeaddrinfo(g_ai0);
サーバへの文字列送信処理

●345~356行目の処理。引き続きサーバへ文字列を送信する処理に移ります。まずはステータスバーの状態を変化させて、さらにStringエデットボックスから送信すべき文字列をbuf変数に読み取っておきます。

344:            /* ステータスバーに「送信中」の状態を表示する */
345:            SendMessage(hStatusLabel, SB_SETTEXT,
346:                        (WPARAM)(255 | 0),
347:                        (LPARAM)TEXT("Status: sending string"));
348:
349:            /* エデットボックスからサーバに送信する文字列を取得 */
350:            g_StringLen = SendMessage(hStringEdit, WM_GETTEXT,
351:                                      (WPARAM)sizeof(buf), (LPARAM)buf);
352:            if (g_StringLen == 0) {
353:                closesocket(s);
354:                NotifyError(hWnd, TEXT("empty string"));
355:                break;
356:            }

●359~369行目の処理。send()関数により文字列の送信(正確にはプロトコルスタックの送信バッファにデータをコピー)を行います。送信処理については非同期モードでは動作をさせていませんので、ウインドウメッセージが発生することはありません。このサンプルプログラムでは非常にまれですが、送信バッファに空きが出ない(サーバが過負荷でデータを受け取らない)状態だとsend()関数でブロックが発生します。buf変数の文字列の送信を終えたら、369行目のbreak文を通じて、ウインドウプロシージャ(WndProc()関数)からすぐに抜け出し106~109行目のイベント処理ループへ復帰してウインドウメッセージを待ち受ける状態になります。

358:            /* エデットボックスから取得した文字列をサーバへ送信 */
359:            for (len = 0; len < g_StringLen; ) {
360:                n = send(s, buf + len, g_StringLen - len, 0);
361:                if (n == SOCKET_ERROR) {
362:                    closesocket(s);
363:                    NotifyError(hWnd, TEXT("send Error"));
364:                    break;
365:                }
366:                len += n;
367:            }
368:            
369:            break;
サーバからの文字列受信処理

 サーバへの文字列送信処理が完了すると、サーバからは送信した内容と同じ文字列がエコーバックされてきます。GUIの処理も常に行わなければならず、クライアントプログラムはデータの到着を見張る処理に専念することはできません。サーバからのエコーバックがあるまでは106~109行目のイベントループの処理を続け、GUIへのアクションなども見張っています。

●251~372行目の処理。サーバから文字列を受信しプロトコルスタックが受信バッファにデータを置くと、WinSockはその旨を示すウインドウメッセージWM_SOCKET(種別FD_READ)を発生させます。これを受け取ったウインドウプロシージャ(WndProc()関数)は371行目の分岐を通して372行目以下に処理を遷移させます。

251
:    switch (uMsg) {
300:    case WM_SOCKET:
 .
316:        switch (WSAGETSELECTEVENT(lParam)) {
 .
371:        case FD_READ:
372:            /** 受信バッファにデータがあることが通知された */
 .
 .

●375~377行目の処理。ステータスバーを更新し、サーバから文字を受信している状態を表示します。

374:            /* ステータスバーに「受信中」の状態を表示する */
375:            SendMessage(hStatusLabel, SB_SETTEXT,
376:                        (WPARAM)(255 | 0),
377:                        (LPARAM)TEXT("Status: reciving string"));

●380~389行目の処理。プロトコルスタックの受信バッファにはデータがたまった状態であるので、recv()関数により受信処理を行います。ここで注意しなければいけない点は、仮に100バイトのデータを受信しようとしていたときに、受信バッファには50バイトしかデータがないとすると、recv()関数は50バイトしか読み取りを行いません。残りの50バイトが受信バッファに到着したときに再びWM_SOCKETウインドウメッセージ(種別FD_READ)が発生します。ウインドウメッセージを受け取ったら、再度recv()関数を呼び出してデータ残りの受信処理を完了させる必要があります。ここでg_RecvLen変数は受信を完了したサーバから文字列の合計の長さを記憶している変数で、g_String変数はサーバから受信を期待する文字列の長さです。もし想定する文字数を受け取ってない場合は、ウインドウプロシージャから復帰してWM_SOCKETメッセージ(種別FD_READ)を待ちます(388~389行目)。

379:            /* 受信バッファからデータを受け取る */
380:            n = recv(s, buf + g_RecvLen, g_StringLen - g_RecvLen, 0);
381:            if (n == 0 || n == SOCKET_ERROR) {
382:                closesocket(s);
383:                NotifyError(hWnd, TEXT("recv error"));
384:            }
385:
386:            /* 到着を待つデータがあるか調べる */
387:            g_RecvLen += n;
388:            if (g_RecvLen < g_StringLen)
389:                return 0;

●392~403行目の処理。受信が完了した文字列を画面に表示して、通信を終了します。非同期型の設定を解除してソケットを閉じています(396~397行目)。最後にGUIと受信カウンタ(g_RecvLen)を初期状態に戻して、106~109行目のイベントループに復帰しています。

391:            /* 受信した文字列を画面に表示 */
392:            buf[g_RecvLen] = '\0';
393:            SendMessage(hRecvEdit, WM_SETTEXT, (WPARAM)0, (LPARAM)buf);
394:
395:            /* ソケットを閉じて通信終了 */
396:            WSAAsyncSelect(s, hWnd, WM_SOCKET, 0);
397:            closesocket(s);
398:            EnableWindow(hSendButton, TRUE);
399:            SendMessage(hStatusLabel, SB_SETTEXT,
400:                        (WPARAM)(255 | 0), (LPARAM)TEXT(""));
401:            g_RecvLen = 0;
402:
403:            break;

名前解決処理の非同期化の必要性

 実際にサンプルプログラムを動作させた方はお気づきかもしれませんが、SENDボタンを押下した直後に行われる名前解決処理で、GUIが一瞬フリーズします。ステータスバーに「Status: resolving servername」が表示されている間は、GUIは一切の操作を受け付けません。これはgetaddrinfo()を呼び出している間は、アプリケーション全体がブロックされてしまい、GUIにかかわる処理が実行できないためです。WinSock APIにはWSAAsyncGetHostByName()関数のような、ブロックを起こさずに名前解決を行うAPIが標準で提供されており、非同期型のソケットを使うプログラムは名前解決処理も非同期版のAPIを使うことが定石になっています。しかしIPv6に対応したAPIが存在しないため、同期版の名前解決関数とスレッドを使って、自前で書き起こす必要があります。紙面の都合から今回は解説しませんでしたが、非同期版のgetaddrinfo()/getnameinfo()関数の作成について、改めて取り上げたいと思います。

まとめ

 第4回は非同期I/Oによるソケットの扱い方を解説しました。今回紹介したTCP echoクライアントはGUIの部分のコードが大きくなってしまうため、一見すると複雑なプログラムに見えたかもしれません。しかしソケットのI/Oだけに注目すると、非同期I/OのアクセスモデルはGUIが発生するウインドウメッセージと統一的に扱うことができるため、GUIプログラムとの親和性が高いです。また、非同期I/OはWindows特有の機構ではあるものの、特定のプロトコルに依存する記述はまったくありません。connect()関数周辺のフォールバック処理を書き足すだけで、IPv6, IPv4、また将来出現するプロトコルまで汎用的に対応できる構造となっています。次回は非同期I/Oを使ったTCP echoサーバの作成方法を解説します。

リスト1:tcp-echo-client-asyncio.c [0]

001:/* 
002: * how to compile:
003: * C:¥›cl tc-pecho-client-asyncio.c user32.lib comctl32.lib ws2_32.lib
004: */
005:
006:#define STRICT
007:#include ‹winsock2.h›
008:#include ‹ws2tcpip.h›
009:#include ‹windows.h›
010:#include ‹commctrl.h›
011:
012:/** GUIに関する定義 */
013:/*
014: * GUI構成
015: *
016: *             +----------------------------------------------+
017: *             |□Aync I/O TcpEchoClient                _□×|
018: *             +----------------------------------------------+
019: * hServerLabel→Server [________hServerEdit___________]      |
020: * hStringLabel→String [________hStringEdit___________][SEND]←hSendButton
021: *             |----------------------------------------------|
022: *             |[                                          ]↑|
023: *             |[                 hRecvEdit                ]↓|
024: *             +----------------------------------------------+
025: *             | hStatusLabel                                 |
026: *             +----------------------------------------------+
027: */
028:
029:HINSTANCE g_hInst; /* 現在のインターフェイス */
030:
031:/* GUIを構成するコンポーネントのウインドウハンドル */
032:HWND hServerLabel; /* Serverラベル */
033:HWND hServerEdit;  /* サーバ名を入力するエデットボックス */
034:HWND hStringLabel; /* Stringラベル */
035:HWND hStringEdit;  /* サーバへ送信する文字列を入力するエデットボックス */
036:HWND hSendButton;  /* 文字列送信ボタン */
037:HWND hRecvEdit;    /* サーバから受信した文字列を表示するエデットボックス */
038:HWND hStatusLabel; /* クライアントの動作状況を表示するステータスバー */
039:
040:#define ID_EDIT_SERVER        1000
041:#define ID_SEND                1001
042:#define ID_EDIT_VIEW        1002
043:#define ID_STATUS        1003
044:#define ID_STATIC        1004
045:
046:/** ソケットI/Oに関するウインドウメッセージの定義 */
047:#define WM_SOCKET        (WM_USER+1)
048:
049:
050:/** 名前解決の結果を格納する */
051:LPADDRINFO g_ai0; /* getaddrinfo()関数が返したリストの先頭を保持 */
052:LPADDRINFO g_ai;  /* 現在接続処理中のADDRINFOを示す */
053:/* 
054: *                1             2            3
055: *  g_ai0 --› ADDRINFO0 --› ADDRINFO --› ADDRINFO --› NULL
056: *                            ^             ^
057: *                            |  .........  |
058: *   g_ai  -------------------+ - - - - - - +
059: *                                 2番目が失敗したら3番目を指し示す
060: *
061: *    g_aiは現在接続を試行しているADDRINFO要素
062: *    接続(connect)試行のたびにNULLに向かってリストの要素をたどる
063: *    接続(connect)に成功したら、以降のADDRINFO要素は参照しない
064: */
065:
066:
067:/** send()/recv()に関する送信バッファのカウンタ */
068:int g_StringLen;   /* サーバへ送信する・サーバから受信する文字列の長さ */
069:int g_RecvLen = 0; /* サーバから受信済みの文字列の長さ */
070:
071:
072:/** 関数プロトタイプ宣言 */
073:BOOL InitApplication(HANDLE hInstance);
074:BOOL InitInstance(HINSTANCE hInstance, int nCmdShow);
075:LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
076:BOOL ConnectServer(HWND hWnd);
077:void NotifyError(HWND hWnd, TCHAR *errmsg);
078:
079:
080:/** Windowsプログラムのエントリポイント */
081:/*
082: * WinSockの初期化とGUIの初期化後にメッセージ
083: * 処理のためのメインループに入る
084: */
085:int WINAPI WinMain(HINSTANCE hInstance,
086:                   HINSTANCE hPrevInstance,
087:                   LPSTR lpCmdLine,
088:                   int nCmdShow)
089:{
090:    WSADATA wsaData;
091:    MSG msg;
092:
093:    /* WinSockの初期化処理 */
094:    if (WSAStartup(MAKEWORD(2, 2), &wsaData))
095:        return FALSE;
096:
097:    /* ウインドウクラスの登録 */
098:    if (!InitApplication(hInstance)) 
099:        return FALSE;
100:   
101:    /* GUIウインドウの作成と表示処理 */
102:    if (!InitInstance(hInstance, nCmdShow))
103:        return FALSE;
104:
105:    /* ウインドウメッセージ処理 */
106:    while (GetMessage(&msg, NULL, 0, 0)) {
107:        TranslateMessage(&msg);
108:        DispatchMessage(&msg);
109:    }
110:
111:    return (int)msg.wParam;
112:}
113:
114:
115:/** ウインドウクラスの登録処理 */
116:BOOL InitApplication(HANDLE hInstance)
117:{
118:    WNDCLASS wc;
119:
120:    wc.style = CS_HREDRAW | CS_VREDRAW;
121:    /* ウィンドウプロシージャを登録する */
122:    wc.lpfnWndProc = (WNDPROC)WndProc;
123:    wc.cbClsExtra = 0;
124:    wc.cbWndExtra = 0;
125:    wc.hInstance = hInstance;
126:    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
127:    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
128:    wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW;
129:    wc.lpszMenuName =  NULL;
130:    wc.lpszClassName = TEXT("Async I/O TcpEchoClient");
131:
132:    return RegisterClass(&wc);
133:}
134:
135:
136:/** ウインドウの作成と表示処理 */
137:BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
138:{
139:    HWND hWnd;
140:
141:    g_hInst = hInstance; // グローバル変数にインスタンス処理を格納します。
142:
143:    /* 親ウインドウの作成 */
144:    hWnd = CreateWindow(TEXT("Async I/O TcpEchoClient"),
145:                        TEXT("Async I/O TcpEchoClient"),
146:                        WS_OVERLAPPEDWINDOW | WS_VISIBLE,
147:                        CW_USEDEFAULT, 0,
148:                        320, 160,
149:                        NULL,
150:                        NULL,
151:                        g_hInst,
152:                        NULL);
153:
154:    if (!hWnd)
155:        return FALSE;
156:
157:    /* 親ウィンドウ内にコントロールを作成する */
158:    hServerLabel = CreateWindow(TEXT("STATIC"), TEXT("Server"),
159:                                WS_CHILD | WS_VISIBLE | SS_CENTER,
160:                                5, 5,
161:                                60, 25,
162:                                hWnd,
163:                                (HMENU)ID_STATIC,
164:                                g_hInst,
165:                                NULL);
166:    hServerEdit = CreateWindowEx(WS_EX_CLIENTEDGE,
167:                                 TEXT("EDIT"), NULL,
168:                                 WS_CHILD | WS_VISIBLE | WS_TABSTOP | 
169:                                 ES_AUTOHSCROLL,
170:                                 65, 5,
171:                                 200, 25,
172:                                 hWnd,
173:                                 (HMENU)ID_EDIT_SERVER,
174:                                 g_hInst,
175:                                 NULL);
176:    hSendButton = CreateWindow(TEXT("BUTTON"), TEXT("SEND"),
177:                               WS_CHILD | WS_VISIBLE | WS_TABSTOP |
178:                               BS_PUSHBUTTON,
179:                               265, 30,
180:                               50, 25,
181:                               hWnd,
182:                               (HMENU)ID_SEND,
183:                               g_hInst,
184:                               NULL);
185:    hStringLabel = CreateWindow(TEXT("STATIC"), TEXT("String"),
186:                                WS_CHILD | WS_VISIBLE | SS_CENTER,
187:                                5, 30,
188:                                60, 25,
189:                                hWnd,
190:                                (HMENU)ID_STATIC,
191:                                g_hInst,
192:                                NULL);
193:    hStringEdit = CreateWindowEx(WS_EX_CLIENTEDGE,
194:                                 TEXT("EDIT"), NULL,
195:                                 WS_CHILD | WS_VISIBLE | WS_TABSTOP | 
196:                                 ES_AUTOHSCROLL,
197:                                 65, 30,
198:                                 200, 25,
199:                                 hWnd,
200:                                 (HMENU)ID_EDIT_SERVER,
201:                                 g_hInst,
202:                                 NULL);
203:    hRecvEdit = CreateWindowEx(WS_EX_CLIENTEDGE,
204:                               TEXT("EDIT"), NULL,
205:                               WS_CHILD | WS_VISIBLE | WS_VSCROLL |
206:                               WS_TABSTOP | ES_NOHIDESEL |
207:                               ES_MULTILINE | ES_LEFT | ES_READONLY,
208:                               0, 55,
209:                               315, 50,
210:                               hWnd,
211:                               (HMENU)ID_EDIT_VIEW,
212:                               g_hInst,
213:                               NULL);
214:
215:    /* ステータスバーの作成(要コモンコントロール) */
216:    InitCommonControls(); 
217:    hStatusLabel = CreateWindowEx(0,
218:                                  STATUSCLASSNAME, NULL,
219:                                  WS_CHILD | SBARS_SIZEGRIP | CCS_BOTTOM |
220:                                  WS_VISIBLE,   
221:                                  0, 105,
222:                                  60, 25,
223:                                  hWnd,
224:                                  (HMENU)ID_STATUS,
225:                                  g_hInst,
226:                                  NULL);
227:
228:    SetFocus(hServerEdit);
229:
230:    ShowWindow(hWnd, nCmdShow);  /* ウインドウの表示 */
231:    UpdateWindow(hWnd);          /* ウインドウの最初の更新 */
232:
233:    return TRUE;
234:}
235:
236:
237:/** ウインドウプロシージャの本体 */
238:/*
239: * ボタン押下時のイベント、ソケットI/Oの処理が発生した時に、
240: * ウインドウメッセージがuMsg変数によって通知される。
241: */
242:LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
243:{
244:    char buf[BUFSIZ];
245:    ADDRINFO hints;
246:    SOCKET s;
247:    int len;
248:    int n;
249:
250:    /* 受信したウインドウメッセージの種類(uMsg)に応じて処理を振分け */
251:    switch (uMsg) {
252:    case WM_COMMAND:
253:        switch (LOWORD(wParam)) {
254:        case ID_SEND:
255:            /* SENDボタンを無効化する */
256:            EnableWindow(hSendButton, FALSE);
257:
258:            /* ステータスバーに「名前解決中」の状態を表示する */
259:            SendMessage(hStatusLabel, SB_SETTEXT,
260:                        (WPARAM)(255 | 0),
261:                        (LPARAM)TEXT("Status: resolving servername"));
262:
263:            /* Serverエデットボックスからサーバ名を取り出す */
264:            SendMessage(hServerEdit, WM_GETTEXT,
265:                        (WPARAM)sizeof(buf), (LPARAM)buf);
266:            if (strlen(buf) == 0) {
267:                NotifyError(hWnd, TEXT("invalid server name or address"));
268:                break;
269:            }
270:
271:            /* サーバ名の名前解決を行う */
272:            memset(&hints, 0, sizeof(hints));
273:            hints.ai_family = AF_UNSPEC;
274:            hints.ai_socktype = SOCK_STREAM;
275:            if (getaddrinfo(buf, "echo", &hints, &g_ai0)) {
276:                NotifyError(hWnd, TEXT("server not found"));
277:                break;
278:            }
279:
280:            /* ステータスバーに「接続中」の状態を表示する */
281:            SendMessage(hStatusLabel, SB_SETTEXT,
282:                        (WPARAM)(255 | 0),
283:                        (LPARAM)TEXT("Status: connecting server"));
284:
285:            /* 接続処理を開始する */
286:            g_ai = g_ai0;
287:            if (ConnectServer(hWnd) == FALSE) {
288:                NotifyError(hWnd, TEXT("connect error"));
289:                freeaddrinfo(g_ai0);
290:                break;
291:            }
292:
293:            break;
294:
295:        default:
296:            break;
297:        }
298:        break;
299:
300:    case WM_SOCKET:
301:        /** ソケットI/Oに関するイベントが発生した */
302:
303:        /* ソケットの記述子を取り出し */
304:        s = (SOCKET)wParam;
305:
306:        /* connect()関数以外のソケットI/Oの処理結果を調べる */
307:        /* WSAGETSELECTERROR(lParam)が非0(失敗時)は初期状態 */
308:        /* に戻る */
309:        if (   WSAGETSELECTERROR(lParam) != 0
310:            && WSAGETSELECTEVENT(lParam) != FD_CONNECT) {
311:            closesocket(s);
312:            NotifyError(hWnd, TEXT("socket error"));
313:            break;                       
314:        }
315:
316:        switch (WSAGETSELECTEVENT(lParam)) {
317:        case FD_CONNECT:
318:            /** 接続処理が完了したことが通知された */
319:
320:            if (WSAGETSELECTERROR(lParam) != 0) {
321:                /** connect()が失敗に終わったため、ソケットを閉じ再試行する */
322:
323:                /* 接続できなかったソケットを閉じる */
324:                closesocket(s);
325:
326:                /* 接続を再試行し、再度ウインドメッセージ(FD_CONNECT)を待つ */
327:                g_ai = g_ai-›ai_next;
328:                if (ConnectServer(hWnd) == FALSE) {
329:                    closesocket(s);
330:                    NotifyError(hWnd, TEXT("connect error"));
331:                    freeaddrinfo(g_ai0);
332:                    break;
333:                }
334:
335:                break;
336:            }
337:
338:            /** この地点に到達していれば接続処理 */
339:            /** (connect()関数)が成功している */
340:
341:            /* ADDRINFOリストを開放する */
342:            freeaddrinfo(g_ai0);
343:
344:            /* ステータスバーに「送信中」の状態を表示する */
345:            SendMessage(hStatusLabel, SB_SETTEXT,
346:                        (WPARAM)(255 | 0),
347:                        (LPARAM)TEXT("Status: sending string"));
348:
349:            /* エデットボックスからサーバに送信する文字列を取得 */
350:            g_StringLen = SendMessage(hStringEdit, WM_GETTEXT,
351:                                      (WPARAM)sizeof(buf), (LPARAM)buf);
352:            if (g_StringLen == 0) {
353:                closesocket(s);
354:                NotifyError(hWnd, TEXT("empty string"));
355:                break;
356:            }
357:
358:            /* エデットボックスから取得した文字列をサーバへ送信 */
359:            for (len = 0; len ‹ g_StringLen; ) {
360:                n = send(s, buf + len, g_StringLen - len, 0);
361:                if (n == SOCKET_ERROR) {
362:                    closesocket(s);
363:                    NotifyError(hWnd, TEXT("send Error"));
364:                    break;
365:                }
366:                len += n;
367:            }
368:            
369:            break;
370:
371:        case FD_READ:
372:            /** 受信バッファにデータがあることが通知された */
373:            
374:            /* ステータスバーに「受信中」の状態を表示する */
375:            SendMessage(hStatusLabel, SB_SETTEXT,
376:                        (WPARAM)(255 | 0),
377:                        (LPARAM)TEXT("Status: reciving string"));
378:
379:            /* 受信バッファからデータを受け取る */
380:            n = recv(s, buf + g_RecvLen, g_StringLen - g_RecvLen, 0);
381:            if (n == 0 || n == SOCKET_ERROR) {
382:                closesocket(s);
383:                NotifyError(hWnd, TEXT("recv error"));
384:            }
385:
386:            /* 到着を待つデータがあるか調べる */
387:            g_RecvLen += n;
388:            if (g_RecvLen ‹ g_StringLen)
389:                return 0;
390:
391:            /* 受信した文字列を画面に表示 */
392:            buf[g_RecvLen] = '¥0';
393:            SendMessage(hRecvEdit, WM_SETTEXT, (WPARAM)0, (LPARAM)buf);
394:
395:            /* ソケットを閉じて通信終了 */
396:            WSAAsyncSelect(s, hWnd, WM_SOCKET, 0);
397:            closesocket(s);
398:            EnableWindow(hSendButton, TRUE);
399:            SendMessage(hStatusLabel, SB_SETTEXT,
400:                        (WPARAM)(255 | 0), (LPARAM)TEXT(""));
401:            g_RecvLen = 0;
402:
403:            break;
404:            
405:        default:
406:            break;
407:        }
408:
409:        break;
410:
411:    case WM_DESTROY:
412:        PostQuitMessage(0);
413:        break;
414:
415:    default:
416:        return DefWindowProc(hWnd, uMsg, wParam, lParam);
417:    }
418:
419:    return 0;
420:}
421:
422:
423:/** サーバへの接続を行う処理 */
424:/* 戻り値
425: *   TRUE:  接続が成功した場合、もしくは接続処理が行われており結果が
426:            確定していない場合。後者の場合はウインドウメッセージ
427:            (FD_CONNECT)の結果をウインドウプロシージャWndProc内で検査
428:            することで接続処理の成否を判定する
429:     FALSE: ADDRINFOリストg_ai0のすべての要素に対して接続を試行したが、
430:            失敗した。FALSEが返された場合はIPv6→IPv4のフォールバック
431:            にも完全に失敗している
432: */
433:BOOL ConnectServer(HWND hWnd)
434:{
435:    SOCKET s;
436:    
437:    if (g_ai == NULL)
438:        return FALSE;
439:
440:    for ( ; g_ai; g_ai = g_ai-›ai_next) {
441:
442:        /* ソケットの生成 */
443:        s = socket(g_ai-›ai_family, g_ai-›ai_socktype, g_ai-›ai_protocol);
444:        if (s == INVALID_SOCKET)
445:            continue;
446:
447:        /* ソケットを非同期モードにする */
448:        if (WSAAsyncSelect(s, hWnd, WM_SOCKET,
449:                           FD_CONNECT | FD_READ) == SOCKET_ERROR) {
450:            closesocket(s);
451:            s = INVALID_SOCKET;
452:            continue;
453:        }
454:
455:        /* 接続処理 */
456:        if (connect(s, g_ai-›ai_addr, g_ai-›ai_addrlen) == SOCKET_ERROR)
457:            /* エラーが返ってきても、エラーコードがWSAEWOULDBLOCK */
458:            /* ならば接続処理がバックグラウンドで行われている状態 */
459:            /* である。ウインドウメッセージ(FD_CONNECT)の到着を待 */
460:            /* つ。その他のエラーコードならば、このg_aiについての */
461:            /* 処理を打ち切る */
462:            if (WSAGetLastError() != WSAEWOULDBLOCK) {
463:                closesocket(s);
464:                s = INVALID_SOCKET;
465:                continue;
466:            }
467:
468:        break;
469:    }
470:
471:    if (s == INVALID_SOCKET) {
472:        /* ADDRINFOリスト g_ai0 のすべての要素について接続 */
473:        /* に失敗した。これ以上接続を試行する候補はない    */
474:        return FALSE;
475:    }
476:
477:    return TRUE;
478:}
479:
480:
481:/** エラー発生時にメッセージボックスを表示してGUIを初期状態に戻す処理 */
482:void NotifyError(HWND hWnd, TCHAR *errmsg)
483:{
484:    /* ステータスバーにエラーを表示する */
485:    SendMessage(hStatusLabel, SB_SETTEXT,
486:                        (WPARAM)(255 | 0), (LPARAM)TEXT("Status: error!"));
487:
488:    /* エラーを通知するメッセージボックスを表示 */
489:    MessageBox(hWnd, errmsg, TEXT("Error!"), MB_OK | MB_ICONERROR);
490:
491:    /* SENDボタンを初期状態に戻す */
492:    EnableWindow(hSendButton, TRUE);
493:
494:    /* Recvエデットボックスを初期状態に戻す */
495:    SendMessage(hRecvEdit, WM_SETTEXT, (WPARAM)0, (LPARAM)TEXT(""));
496:
497:    /* ステータスバーを初期状態に戻す */
498:    SendMessage(hStatusLabel, SB_SETTEXT, (WPARAM)(255 | 0), (LPARAM)TEXT(""));
499:
500:    /* 受信済み文字数をリセットする */
501:    g_RecvLen = 0;
502:}
図8:tcp-echo-client-asyncio.cのコンパイル方法
C:\>cl tcp-echo-client-asyncio.c user32 comctl32.lib 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-asyncio.c
Microsoft (R) Incremental Linker Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:tcp-echo-client-asyncio.exe
tcp-echo-client-asyncio.obj
user32.lib
comctl32.lib
ws2_32.lib

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

多くのUNIXではシステムコールが失敗したとき、エラーの原因を示す数値が大域変数errnoに格納されます。errno変数は、ファイルI/Oだけでなくソケットに対する操作についてもエラー原因を参照するために利用されています。

    ------------------------------------------------------------
    /* バークレーソケット(UNIX)でのエラーの調べ方 */
    if ((s = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
        if (errno == EPROTONOSUPPORT)
            fprintf(stderr, "IPv6 is unsupported protocol\n");
        exit(1);
    }
    ------------------------------------------------------------

一方、WinSock APIではソケットの操作結果についてerrno変数を参照することはできません。errno変数の参照の代わりにWSAGetLastError()関数を使用します。

    ------------------------------------------------------------
    /* WinSock APIでのエラーの調べ方 */
    if ((s = socket(AF_INET6, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        if (WSAGetLastError() == WSAEPROTONOSUPPORT)
            fprintf(stderr, "IPv6 is unsupported protocol\n");
        exit(1);
    }
    ------------------------------------------------------------

なお、errno変数へ値をセットする操作に対応する関数はWSASetLastError()関数を使います。

    ------------------------------------------------------------
    errno = EPROTONOSUPPORT;              /* for バークレーソケット */
    WSASetLastError(WSAEPROTONOSUPPORT);  /* for WinSock API */
    ------------------------------------------------------------

※注1 送信処理そのものはアプリケーションではなくOSの持つプロトコルスタックが行います。

※注2 ソケットI/O桙ノ発生するメッセージの名前はユーザが定義を与えます。本サンプルプログラムではWM_SOCKETと定義します。

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

http://www.ipv6style.jp/trackback/199

Source URL:
http://www.ipv6style.jp/jp/apps/20051212/index.shtml