WindowsAPI Programming  第3章 〜ソケット通信〜


Windows Sockets (ウィンドウズ・ソケット)

<ソケット通信とWindows版ソケット>
 現在インターネットの標準プロトコルとしてTCP/IPが急速に普及しています。TCP/IPは1980年代初め、BSD(Berkeley Software Distribution)版UNIXにおいてローカルのプロトコル通信を行うメカニズムとしてソケットが実装され、その後ネットワーク通信インタフェースとして発達したものです。

このソケットインタフェースは以後様々なUNIXに移植され、UNIXにおける標準のネットワーク通信インタフェースとなっています。 Windowsにおいては、TCP/IPプロトコルの通信インタフェースを実現するため、1992年にTCP/IPネットワークコミュニティに属する20社以上のベンダが協力して、API仕様が策定されました。これがWindows Socketsです。Windows Socketsの仕様は基本的にUNIXと同じであるため、UNIXで動作していたソケットアプリケーションの移植は比較的簡単で、UNIX〜Windows間のネットワーク通信も容易に実現することができます。ソケットにはストリームソケットと、データグラムソケットの2種類があり、どちらのソケットも、全二重の同時双方向通信を行うことができます。

ストリームソケットは連続的なデータを境界のない形式で送ることのできる方式(データフロー方式)で、実際のデータ送受信に先立ってコネクションを確立する必要がありますが(コネクション型通信方式)、このため信頼性のあるデータ送受信が保証されています。確実なデータの送受信やサイズの大きいデータ転送に適しています。トランスポート層プロトコルとしてTCP (Transmission Control Protocol)を使用します。ストリームソケットで”下請け”にIPプロトコルを用いる場合,TCP/IP通信と呼びます.信頼性がある通信方式である反面,速度を犠牲にしています。

一方、データグラムソケットは、ある大きさのレコードを単位としたコネクションレス型通信方式です。データの通信における送信データの順序は保証されず、データ損失や重複が起こる可能性もあるため、このままではデータの信頼性が確保できません。そこで信頼性を確保するためにはアプリケーション側で何らかの機能を付け加える必要があります。トランスポート層プロトコルはUDP(User Datagram Protocol)を使用して通信を行うため、LANのように下位層での信頼性がある程度確保できるネットワーク環境下ではTCPよりも高速な通信が可能となります。この場合も”下請け”にIPプロトコルを用いるのでUDP/IP通信と呼びます。

TELNETやFTP、HTTP、SMTP、POP3といったプロトコルには信頼性の高いストリームソケット,一方マルチキャストやブロードキャストを使った画像配信など,実時間性の高いアプリケーションにはデータグラムソケットが適しています。すなわち信頼性の確保にはTCP/IP通信,実時間性,高速性の確保にはUDP/IPが使われることを覚えておいてください。今回のWindows Socketの解説では、より実用性が高いと思われるストリームソケット(TCP/IP)について重点的に解説していきます。

ストリームソケットの通信手順(TCPプロトコル)

<TCP/IPの概要>
 ストリームソケットの通信手順は以下の図の通りです。この手順はUNIXでもWindowsでも一緒です。また、ここで用いられる各関数はUNIXの場合socket.h、Windowsの場合winsock.hで型宣言されており、引数や戻り値も共通となっています。
 ではまず,ストリームソケット(TCP/IP)のイメージを述べておきます。ストリームソケットは電話式コミュニケーションと覚えておくと便利です。つまり電話になぞらえて設計した通信方式です。通常,電話でのコミュニケーションに必要なのは電話回線に繋がれている受話器というハードウェアが2つ(自分の分と相手の分)と,電話局に登録されている受話器の識別子つまり電話番号というソフトウェア,ならびに電話をかける側がその電話番号を 知っているということが必要です。
  ストリームソケットではこの手順を真似た形で事が運びます(下図)。
 つまり,クライアントを電話をかける側,サーバを電話をとる側とみなし,
@受話器の取得(サーバ側・クライアント側共にsocket関数の呼び出し)
A受話器の登録(サーバ側のbind関数呼び出し)
B回線数の指定(サーバ側のlisten関数呼び出し)
C受話器が鳴るのを待ちうけ(サーバ側のaccept関数呼び出し)
D電話をかける(クライアント側のconnect関数呼び出し)
という手順で回線が接続されます。また回線が接続された状態では
E話す(send関数の呼び出し)

F聞く(recv関数の呼び出し)を交互に行って「会話」を行います。そして「会話」の終了時にはどちらからでも
G受話器を切る(close関数の呼び出し)
ことによって回線を解放します。
 ただし,実際の電話でのやりとりを考えてみれば分かるように,通常クライアント側で「聞く」ときにはサーバ側で「話し」,クライアント側で「話す」ときにはサーバ側では「聞く」状態になっている必要があります。
  1)サーバ(聞き手) ← クライアント(話し手)
  2)サーバ(話し手) → クライアント(聞き手)
  3)サーバ(聞き手) ← クライアント(話し手)
  4) ・・・・
このため,どちらが先に「聞き手」になり,もう片方が「話し手」になるかを予め決めておきます。このような設計の方法を同期通信と呼びます。これに対し,好きなときに「話し手」になったり「聞き手」にまわったりができるような設計の方法を非同期通信と呼びます。非同期通信については後で述べます。


<TCP/IPの接続手順>
 ではTCP/IPを使用する場合,具体的に何が「受話器」で何が「受話器の登録」なのか?そしてどのような手順で通信路の接続がなされるのかを説明していきます。

 @まず,「受話器」に例えたソケットとは,OSが持っている資源の一部で,OSによって識別子(名前)が付けられています。具体的にはSOCKET型(int型と等価)の整数で呼ばれています。socket関数の戻り値がこの識別子となります。socket関数の呼び出しが失敗に終わるとソケットが取得できないことを表すために,戻り値はINVALID_SOCKETとなります。

 A次に「受話器の登録」とは,このソケットとローカルアドレスを結合(bind)することを意味します。つまり単なる受話器に「電話番号」が付くわけです。このため,「受話器の登録」にはbind関数が使われるのです。従ってbind関数に必要なのは「受話器」,つまりソケットの識別子と,サーバ側のインターネットアドレス(俗に言うIPアドレス)それからポート番号です。ポート番号とは個別のサーバ・クライアントペアの間で共通にしておくチャンネル番号と考えて下さい。皆さんがよく知っているhttpやftp, rlogin なんかも独自のポート番号を持っています(よく使われるアプリケーションプロトコルとポート番号の対応表についてはここを参照,またそれぞれのプロトコルの詳細な説明はここを参照)。IPアドレスで決まる一つのホストの中には複数のプロトコルにおけるサーバが待ち構えているのでチャンネル番号で交通整理しないと混線しますよね。

 B「回線数の指定」とは,このサーバで最大何個のクライアントから同時に電話を受信できるか,を指定することを表します。ソフトウェアの設計から言えば,待ち受けキューの数を決めています。授業の範囲を超えるのでここでは説明しませんが,telnetdやhttpd,smpdやpopdなどの,いわゆる「XXデーモンプログラム」は複数のクライアントからの同時接続を許しています。これを可能にしているのが複数キューの指定です。これはlisten関数で行います。

 C接続待ちが可能になったサーバでは,早速相手からのcallを待ち受けます。それがaccept関数です。accept(受理)というといかにも電話を取った感じに聞こえますが,実際にはこの関数の中で相手からのcallを待ち受けます。つまり,callがあるまでブロックされるわけです。相手からのcallがあって初めてこのブロックから解放され,制御がaccept関数から戻ってくるのです。

 Dクライアント側では,一旦「受話器」を取得してしまえばあとは「電話をかける」だけです。「電話をかける」,つまり相手をcallするには,connect関数を使います。connect関数の引数として必要な情報は相手の「電話番号」つまり,サーバのIPアドレスと,待ち受けポート番号です。ただし,ここでもサーバ側と同様な注意が必要です。callしても相手が受話器をとってくれない限り先には進めないので,connect関数は相手サーバがacceptしてくれない限りブロックされてしまうということです。

 E電話で「話す」ということは,ソケットに書込み(send関数またはwrite関数を使う)をすることです。
 F電話からの声を「聞く」ということは,ソケットからの読み込み(recv関数またはread関数を使う)をすることです。そして
 G会話が終わったら速やかに受話器を返納する必要があります。これがclose関数(Wondowsではclosesocket関数)です。

 ここで,connect関数(電話をかける)とaccept関数(電話が鳴るのを待つ)は上記のCとDのように,クライアント(かける側)とサーバ(うける側)で独立に発生します。しかしながら回線が正常に接続される(電話が繋がる)と同時に,両者は同時にブロッキング状態から解放されるわけです。このあたりは「電話が通じる」プロセスとそっくりですよね。

 



1. ソケット作成

ネットワーク通信を行うためにまず、サーバ側及びクライアント側でソケットを作成する必要があります。ソケットの作成には以下のsocket( )関数を使用します。関数が正常に終了すると戻り値にソケットディスクリプタ(SOCKET型)が返ってきます。このソケットディスクリプタはデータ送受信の際に必要となりますので保持しておかなければなりません。


SOCKET socket(af, type, protocol);

    SOCKET ret;    // 戻り値
                         成功        ソケットディスクリプタ
                         失敗        INVALID_SOCKET
    int af;        // アドレスフォーマット。AF_INETを指定。
    int type;      // ソケットタイプ。
                        SOCK_STREAM  ストリームソケット
                        SOCK_DGRAM   データグラムソケット
    int protocol;  // ソケットプロトコル
                        IPPROTO_TCP  SOCK_STREAMのとき
                        IPPROTO_UDP  SOCK_DGRAMのとき
                        0            デフォルト

2. ソケット閉じる

ソケット通信を終了するときにはclosesocket()関数を使用します。使い終わったソケットは必ず閉じるよう心がけましょう。

int closesocket(sock);

    int ret;      // 戻り値
                        成功  0
                        失敗  SOCKET_ERROR
    SOCKET sock;  // ソケットディスクリプタ

3. ソケットのバインド

サーバ側ではソケットを生成後、そのソケットにbind()関数を使用してローカルアドレスとポートを割り当てておく必要があります。クライアント側では特にアドレスやポートを結び付ける必要がありませんが、bind()関数を呼んでも特に問題はありません。

int bind(sock, addr, len);

    int ret;                // 戻り値
                                 成功  0
                                 失敗  SOCKET_ERROR
    SOCKET sock;            // 結合されるソケット
    struct sockaddr *addr;  // アドレス、ポート情報を含む構造体へのポインタ
    int    len;             // 構造体のサイズ

4. ソケットの接続待ち

サーバ側は相手から接続されるのを待っておく必要があります。ソケットをバインドした後、listen()関数でソケットの接続待ちキュー数(回線数)を指定します。これにより接続待ち状態になります。

int listen(sock, nBacklog);

    int ret;         // 戻り値
                           成功  0
                           失敗  SOCKET_ERROR
    SOCKET sock;     // 接続待ちにするソケット
    int nBacklog;    // 接続待ちのキューを拡張できる最大長
                     //    有効値は 0〜5

5. 接続の受け入れ

接続を確立してデータ通信を行うために、サーバ側では相手から接続要求がきた場合にaccept()関数で接続を受け入れなければなりません。

SOCKET accept(sock, addr, len);

    SOCKET ret;              // 戻り値
                                   成功  接続できる新しいソケットディスクリプタ
                                   失敗  INVALID_SOCKET
    SOCKET sock;             // 受け入れるソケット
    struct sockaddr *addr;   // 接続を識別するためのsockaddr構造体へのポインタ
    int *len;                // 返されたsockaddr構造体のサイズ

6. サーバへの接続

クライアント側はソケットを生成後、サーバと通信を行うためにconnect()関数でサーバに接続し、コネクションを確立する必要があります。

int connect(sock, addr, len);

    int ret;                 // 戻り値
                                   成功  0
                                   失敗  SOCKET_ERROR
    SOCKET sock;             // 受け入れるソケット
    struct sockaddr *addr;   // 接続するアドレスを格納したsockaddr構造体へのポインタ
    int len;                 // sockaddr構造体のサイズ

7. データの送信

コネクションが確立した相手に対しデータを送信する場合、send()関数を使用します。

int send(sock, buf, len, flag);

    int ret;      // 戻り値
                        成功  送信したバイト数
                        失敗  SOCKET_ERROR
    SOCKET sock;  // 送信のためのソケット
    char *buf;    // 送信データバッファ
    int len;      // 送信データサイズ
    int flag;     // 呼び出し方法
                        MSG_DONTROUTE  データがルーティングに従わないことを指定
                        SO_DONTROUTE   ルーティングを無効化する
                        MSG_OOB        帯域外データを送信する

8. データの受信

コネクションが確立した相手からデータを受け取る場合、recv()関数を使用します。

int recv(sock, buf, len, flag);

    int ret;      // 戻り値
                        成功  受信したバイト数
                        失敗  0             すでに接続が閉じられている
                              SOCKET_ERROR  それ以外
    SOCKET sock;  // 受信のためのソケット
    char *buf;    // 受信バッファ
    int len;      // 受信バッファサイズ
    int flag;     // 呼び出し方法
                        MSG_PEEK  受信データはバッファに格納されるが、
                                  入力キューからは削除されない
                        MSG_OOB   帯域外データを受信する



非同期ソケット通信

ソケット関数の多く,具体的にはaccept, connect, recv, send, closesocketの各関数は基本的にアクションが完了するまで戻ってきません。たとえば、接続先が遠いところにある場合など相手からなかなか応答が来ないとき、関数を呼び出したルーチンはずっとブロックされ、相手側から応答があるか何らかのエラーが検出されない限り何もできなくなる状態になり、ウィンドウの移動や再描画などのウィンドウメッセージをまったく処理できなくなります(アプリケーションの終了すらできない)。これでは使い易いプログラムとは言えません。

このような状態を回避させるため、Windows Socketではノンブロッキング処理(非同期処理)ができるような関数が用意されています。非同期処理を行うためには,send(), recv(), connect(), accept()など,通常は相手からのレスポンスがあるまでブロッキングしてしまうようなソケット関連の関数をコールする前にWSAAsyncSelect()を呼びます。こうすると 非同期処理下でのソケット関数の呼び出しは即座に戻り、実際にアクションが完了したときには任意のウィンドウに通知メッセージが発行されます。

int WSAAsyncSelect(sock, hWnd, uMsg, lEvent);

    int  ret;        // 戻り値
                           成功 0
                           失敗 SOCKET_ERROR
    SOCKET sock;     // ソケットディスクリプタ
    HWND   hWnd;     // 通知を行うウィンドウのウィンドウハンドル
    UINT   wMsg;     // 通知メッセージID (WM_USER 以降なら自由に設定可能)
    long   lEvent;   // 通知内容 (OR演算子を用いて複数指定可能)
                           FD_ACCEPT   接続受け入れ
                           FD_CONNECT  接続
                           FD_WRITE    データ送信
                           FD_READ     データ受信
                           FD_CLOSE    接続終了

この関数の使い方を,それぞれの場合について説明します。イベント内容ごとに下記の使い方ができます。

1) FD_ACCEPT :サーバが,クライアント側のconnectを待つときに呼び出すaccept関数のブロッキングを防ぐのに用いる。この関数を呼び出した後,第3引数で指定したWM_XXXXメッセージが相手からのconnect受信のときに発行される。
従って,WM_XXXXメッセージでイベント内容がFD_ACCEPTのときにaccept関数を呼び出すようにすれば結果的にノンブロッキング呼び出しとして使える。

2)FD_CONNECT:クライアントがサーバに対して接続するときに呼び出すconnect関数のブロッキングを防ぐのに用いる。この関数を呼び出した直後にconnect関数を呼び出せば接続要求を発行するだけでブロッキングされることは無い。接続が完了した時点で,第3引数で指定したWM_XXXXメッセージがイベント内容=FD_CONNECTとして発行される。

3)FD_WRITE:ソケットを通してデータを送信するとき,同期通信方式では相手が受信状態になるまでブロックされてしまう。そこでこの関数WSAAsync...を呼び出した直後に送信命令を発行しておけば(相手側の受信を待たずに)ノンブロッキング呼び出しとなってリターンされる。その後相手の受信時に,関数の第3引数で指定したWM_XXXXメッセージがイベント内容=FD_WRITEとして発行されるので,送信の完了が確認できる。

4)FD_READ:ソケットを通してデータを受信するとき,(データ送信のときと同様)同期通信方式では相手が送信状態になるまでブロックされる。そこでこの関数WSAAsync...を呼び出した直後に受信命令を発行しておけば(相手の送信を待たずに)ノンブロッキング呼び出しとなってリターンされる。その後相手の送信時に,関数の第3引数で指定したWM_XXXXメッセージがイベント内容=FD_READとして発行されるので,受信の完了が確認できる。

5)FD_CLOSE:相手端末から切断要求(closesocket関数による)が来た場合にこの関数の第3引数で指定したWM_XXXXメッセージがイベント内容=FD_CLOSEとして発行される。これにより相手の切断要求の受信ならびに回線の切断が確認される。

つまり,ソケットに状態の変化が起きたときにはイベント「WM_XXXX」で通知してもらうということを予約するのがこの関数です。その使用方法は以下のようになります。
例)
switch(uMsg) {
 case WM_XXXX :

switch(WSAGETSELECTEVENT(lP)){
    case FD_ACCEPT:  {  /* (listen関数を呼び出し,接続待ちの状態で)相手からの接続要求(connect)が届いているぞ! それなら 接続待ち&受付だ!*/
      sock = accept(); /* 接続要求受付け */
      ....
      return 0L;
       }
    case FD_CONNECT: {  /* (既に接続要求を発行してある状態で)相手サーバがacceptしてくれた!これで接続完了だ! */
      ...
      return 0L;
    case FD_READ : { /* 相手から何らかのメッセージが届いているぞ!それじゃ読みに行こう!*/
       recv(); /* 実際の読み込み */
      ...
      return 0L;


変数型名

Winsockではソケット通信で使用される各変数型を、Windowsの命名規約に合わせるように定義し直してます。個人的には、従来の冗長な変数型よりもWindowsの命名規約の方が好きですが、どちらを利用するかは自由です。UNIXへの移植を考慮するのなら、UNIXのsocket.hで定義されている従来の変数型を用いる方がいいかもしれません。

標準型再定義型説明
struct sockaddrSOCKADDRソケットアドレス
struct sockaddr_inSOCKADDR_INインターネット用ソケットアドレス
struct hostentHOSTENTホスト情報



簡易チャットプログラム

それでは、TCP/IPネットワーク通信を使った簡易チャットプログラムを例にとって見ていきましょう。これはサンプルプログラムなのであまり凝ったものにせず、サーバ〜クライアント間でメッセージを送受信し、メッセージを受け取ったら受信ウィンドウに表示するという機能だけしかありません。なお、チャットプログラムの場合はサーバもクライアントも機能的には同等ですが、接続する側をクライアント、接続を待つ側をサーバと呼ぶことにします。

実行画面は右図の通りです。サーバとして使用する場合は[接続待ち]ボタンを、クライアントとして使用する場合はホスト名を入力し[接続]ボタンを押します。コネクションが確立すると送信ウィンドウが編集可能となり、[送信]ボタンで送信ウィンドウの内容を相手に送ることができるようになります。

プログラムは以下の通りです(chat.cpp)。あまり綺麗なプログラムでなくて大っぴらに見せるにはちょっと恥ずかしいのですが(^^;、プログラミングに慣れていない方は、今回のチャットプログラムに限らず、なるべく先人の書いたソースをよく見て、いいところはどんどん真似して自分の技術として取り込んでしまいましょう。もちろん、悪いところは真似しないように気をつけてね。

プログラムリスト

/*                                     *
 *  TCP/IP Chat Program Ver0.1         *
 *                                     *
 *                                     *
 *   Copyright(c)1999 FAKE  T.Morioka  *
 *                                     */

/////////////////////////////////////////////////////////////////////////////
//
//  ヘッダファイル
//
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <memory.h>
#include <process.h>


/////////////////////////////////////////////////////////////////////////////
//
//  定数定義
//
#define WM_SOCKET     (WM_USER+1)                       //ソケット用メッセージ
#define PORT          10000                             //通信ポート番号
#define MAX_MESSAGE   2048                              //メッセージサイズ

#define IDE_HOSTNAME  1000                              //ホスト名入力用
#define IDB_CONNECT   1001                              //[接続]ボタン
#define IDB_ACCEPT    1002                              //[接続待ち]ボタン
#define IDB_REJECT    1003                              //[切断]
#define IDE_SENDMSG   1004                              //送信文入力用
#define IDE_RECVMSG   1005                              //受信文表示用
#define IDB_SEND      1006                              //[送信]ボタン
//#define NO_DNS                                        //DNSを参照できない時


/////////////////////////////////////////////////////////////////////////////
//
//  グローバル変数
//
HINSTANCE hInstance;                                    //インスタンスハンドル
LPSTR lpszClassName="Chat";                             //ウィンドウクラス名
SOCKET sock    = INVALID_SOCKET;                        //ソケット
SOCKET sv_sock = INVALID_SOCKET;                        //サーバ用ソケット
HOSTENT *phe;                                           //HOSTENT構造体


/////////////////////////////////////////////////////////////////////////////
//
//  プロトタイプ宣言
//
LRESULT CALLBACK WindowProc(HWND,UINT,WPARAM,LPARAM);   //ウィンドウ関数
BOOL SockInit(HWND hWnd);                               //ソケット初期化
BOOL SockAccept(HWND hWnd);                             //ソケット接続待ち
BOOL SockConnect(HWND hWnd, LPCSTR host);               //ソケット接続


/////////////////////////////////////////////////////////////////////////////
//
//  WinMain関数 (Windowsプログラム起動時に呼ばれる関数)
//
int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst,
    LPSTR lpszArgs, int nWinMode)
{
    HWND hWnd;                                          //ウィンドウハンドル
    MSG  msg;                                           //メッセージ
    WNDCLASSEX wc;                                      //ウィンドウクラス

    hInstance = hThisInst;                              //グローバル化

    //ウィンドウクラス定義
    wc.hInstance     = hInstance;                       //インスタンス
    wc.lpszClassName = lpszClassName;                   //クラス名
    wc.lpfnWndProc   = WindowProc;                      //ウィンドウ関数名
    wc.style         = 0;                               //クラススタイル
    wc.cbSize        = sizeof(WNDCLASSEX);              //構造体サイズ
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION); //アイコンハンドル
    wc.hIconSm       = LoadIcon(NULL, IDI_WINLOGO);     //スモールアイコン
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);     //マウスポインタ
    wc.lpszMenuName  = NULL;                            //メニュー(なし)
    wc.cbClsExtra    = 0;                               //クラス拡張情報
    wc.cbWndExtra    = 0;                               //ウィンドウ拡張情報
    wc.hbrBackground = (HBRUSH) COLOR_WINDOW;           //ウィンドウの背景色
    if(!RegisterClassEx(&wc)) return 0;                 //ウィンドウクラス登録

    //ウィンドウ生成
    hWnd = CreateWindow(
        lpszClassName,                                  //ウィンドウクラス名
        "Chat Program Ver0.1",                          //ウィンドウ名
        WS_DLGFRAME|WS_VISIBLE|WS_SYSMENU,              //ウィンドウ属性
        CW_USEDEFAULT,                                  //ウィンドウ表示位置(X)
        CW_USEDEFAULT,                                  //ウィンドウ表示位置(Y)
        382,                                            //ウィンドウサイズ(X)
        374,                                            //ウィンドウサイズ(Y)
        HWND_DESKTOP,                                   //親ウィンドウハンドル
        NULL,                                           //
        hInstance,                                      //インスタンスハンドル
        NULL                                            //
    );

    //ウィンドウ表示
    ShowWindow(hWnd, nWinMode);                         //ウィンドウ表示モード
    UpdateWindow(hWnd);                                 //ウインドウ更新

    //メッセージループ
    while(GetMessage(&msg, NULL, 0, 0)){                //メッセージを取得
        TranslateMessage(&msg);                         //
        DispatchMessage(&msg);                          //メッセージ送る
    }
    return msg.wParam;                                  //プログラム終了
}


/////////////////////////////////////////////////////////////////////////////
//
//  ウィンドウ関数(イベント処理を記述)
//
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wP, LPARAM lP)
{
    static HWND hWndHost, hWndConnect, hWndAccept;
    static HWND hWndReject, hWndSendMsg, hWndRecvMsg, hWndSend;
    switch(uMsg){
    case WM_CREATE:
        //文字列表示
        CreateWindow("static", "Host Name", WS_CHILD|WS_VISIBLE,
            10,10, 100, 18, hWnd, NULL, hInstance, NULL);
        CreateWindow("static", "Send Message", WS_CHILD|WS_VISIBLE,
            10,  65, 200, 18, hWnd, NULL, hInstance, NULL);
        CreateWindow("static", "Receive Message", WS_CHILD|WS_VISIBLE,
            10, 210, 200, 18, hWnd, NULL, hInstance, NULL);
        //ホスト名入力用エディットボックス
        hWndHost    = CreateWindowEx(WS_EX_CLIENTEDGE, "edit","",
            WS_CHILD|WS_VISIBLE, 10,30, 200, 25,
            hWnd, (HMENU)IDE_HOSTNAME, hInstance, NULL);
        //[接続]ボタン
        hWndConnect = CreateWindow("button", "接続",
            WS_CHILD|WS_VISIBLE, 220,30,50,25,
            hWnd, (HMENU)IDB_CONNECT, hInstance, NULL);
        //[接続待ち]ボタン
        hWndAccept  = CreateWindow("button", "接続待ち",
            WS_CHILD|WS_VISIBLE, 275,30,90,25,
            hWnd, (HMENU)IDB_ACCEPT, hInstance, NULL);

        //送信メッセージ入力用エディットボックス
        hWndSendMsg = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", "",
            WS_CHILD|WS_VISIBLE|ES_MULTILINE|WS_DISABLED, 10, 85, 355, 100,
            hWnd, (HMENU)IDE_SENDMSG, hInstance, NULL);
        //[送信]ボタン
        hWndSend    = CreateWindow("button", "送信",
            WS_CHILD|WS_VISIBLE|WS_DISABLED, 255,190,50,25,
            hWnd, (HMENU)IDB_SEND, hInstance, NULL);
        //[切断]ボタン
        hWndReject  = CreateWindow("button", "切断",
            WS_CHILD|WS_VISIBLE|WS_DISABLED, 310,190,50,25,
            hWnd, (HMENU)IDB_REJECT, hInstance, NULL);
        //受信メッセージ表示用エディットボックス
        hWndRecvMsg = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", "",
            WS_CHILD|WS_VISIBLE|ES_MULTILINE|ES_READONLY, 10, 230, 355, 100,
            hWnd, (HMENU)IDE_RECVMSG, hInstance, NULL);
        SetFocus(hWndHost);                             //フォーカス指定
        SockInit(hWnd);                                 //ソケット初期化
        return 0L;

    case WM_COMMAND:
        switch(LOWORD(wP)){
        case IDB_ACCEPT:                                //[接続待ち]ボタン押下
            if(SockAccept(hWnd)) return 0L;             //接続待ち失敗
            EnableWindow(hWndHost, FALSE);              //[HostName]無効
            EnableWindow(hWndConnect, FALSE);           //[接続]    無効
            EnableWindow(hWndAccept, FALSE);            //[接続待ち]無効
            EnableWindow(hWndReject, TRUE);             //[切断]    有効
            return 0L;
        case IDB_CONNECT:                               //[接続]ボタン押下
            {
                char host[100];                         //ホスト名格納域
                GetWindowText(hWndHost, host, sizeof(host)-1);
                if(SockConnect(hWnd, host)){            //接続失敗
                    SetFocus(hWndHost);                 //フォーカス指定
                    return 0L;
                }
                EnableWindow(hWndHost, FALSE);          //[HostName]無効
                EnableWindow(hWndConnect, FALSE);       //[接続]    無効
                EnableWindow(hWndAccept, FALSE);        //[接続待ち]無効
                EnableWindow(hWndReject, TRUE);         //[切断]    有効
            }
            return 0L;
        case IDB_SEND:                                  //メッセージ送信
            {
                char buf[MAX_MESSAGE];                  //メッセージ格納域
                GetWindowText(hWndSendMsg, buf, sizeof(buf)-1);
                if(send(sock, buf, strlen(buf)+1, 0)==SOCKET_ERROR){
                    MessageBox(hWnd, "sending failed", "Error",
                        MB_OK|MB_ICONEXCLAMATION);
                }
                SetWindowText(hWndSendMsg, "");         //内容を空にする
                SetFocus(hWndSendMsg);                  //フォーカス指定
            }
            return 0L;
        case IDB_REJECT:                                //[切断]ボタン押下
            {
                if(sock!=INVALID_SOCKET){               //ソケットを閉じる
                    closesocket(sock);
                    sock=INVALID_SOCKET;
                }
                if(sv_sock!=INVALID_SOCKET){            //サーバ用ソケット
                    closesocket(sv_sock);               //    を閉じる
                    sv_sock=INVALID_SOCKET;
                }
                EnableWindow(hWndHost, TRUE);           //[HostName]有効
                EnableWindow(hWndConnect, TRUE);        //[接続]    有効
                EnableWindow(hWndAccept, TRUE);         //[接続待ち]有効
                EnableWindow(hWndSendMsg, FALSE);       //送信文入力不可
                EnableWindow(hWndSend, FALSE);          //[送信]    無効
                EnableWindow(hWndReject, FALSE);        //[切断]    無効
                SetFocus(hWndHost);                     //フォーカス指定
            }
            return 0L;
        }
        return 0L;
    case WM_SOCKET:                                     //非同期処理メッセージ
        if(WSAGETSELECTERROR(lP)!=0) return 0L;
        switch(WSAGETSELECTEVENT(lP)){
        case FD_ACCEPT:                                 //接続待ち完了通知
            {
                SOCKADDR_IN cl_sin;
                int len = sizeof(cl_sin);
                sock = accept(sv_sock, (LPSOCKADDR)&cl_sin, &len);
                if(sock==INVALID_SOCKET){
                    MessageBox(hWnd, "Accepting connection failed", "Error",
                        MB_OK|MB_ICONEXCLAMATION);
                    closesocket(sv_sock);
                    sv_sock=INVALID_SOCKET;
                    EnableWindow(hWndHost, TRUE);       //[HostName]有効
                    EnableWindow(hWndConnect, TRUE);    //[接続]    有効
                    EnableWindow(hWndAccept, TRUE);     //[接続待ち]有効
                    EnableWindow(hWndReject, FALSE);    //[切断]    無効
                    SetFocus(hWndHost);                 //フォーカス指定
                    return 0L;
                }
#ifndef NO_DNS
                //ホスト名取得
                phe = gethostbyaddr((char *)&cl_sin.sin_addr, 4, AF_INET);
                if(phe){
                    SetWindowText(hWndHost, phe->h_name);
                }
#endif  NO_DNS
                //非同期モード (受信&切断)
                if(WSAAsyncSelect(sock, hWnd, WM_SOCKET, FD_READ|FD_CLOSE)
                   ==SOCKET_ERROR){
                    MessageBox(hWnd, "WSAAsyncSelect() failed", "Error",
                        MB_OK|MB_ICONEXCLAMATION);
                    EnableWindow(hWndHost, TRUE);       //[HostName]有効
                    EnableWindow(hWndConnect, TRUE);    //[接続]    有効
                    EnableWindow(hWndAccept, TRUE);     //[接続待ち]有効
                    EnableWindow(hWndReject, FALSE);    //[切断]    無効
                    SetFocus(hWndHost);                 //フォーカス指定
                    return 0L;
                }
                EnableWindow(hWndSendMsg, TRUE);        //送信文入力可
                EnableWindow(hWndSend, TRUE);           //[送信]有効
                SetFocus(hWndSendMsg);                  //フォーカス指定
            }
            return 0L;
        case FD_CONNECT:                                //接続完了通知
            //非同期モード (受信&切断)
            if(WSAAsyncSelect(sock, hWnd, WM_SOCKET, FD_READ|FD_CLOSE)
               ==SOCKET_ERROR){
                MessageBox(hWnd, "WSAAsyncSelect() failed", "Error",
                    MB_OK|MB_ICONEXCLAMATION);
                EnableWindow(hWndHost, TRUE);           //[HostName]有効
                EnableWindow(hWndConnect, TRUE);        //[接続]    有効
                EnableWindow(hWndAccept, TRUE);         //[接続待ち]有効
                EnableWindow(hWndReject, FALSE);        //[切断]    無効
                SetFocus(hWndHost);                     //フォーカス指定
                return 0L;
            }
            EnableWindow(hWndSendMsg, TRUE);            //送信文入力可
            EnableWindow(hWndSend, TRUE);               //[送信]有効
            SetFocus(hWndSendMsg);                      //フォーカス指定
            return 0L;
        case FD_READ:                                   //メッセージ受信
            {
                char buf[MAX_MESSAGE];                  //受信バッファ
                if(recv(sock, buf, MAX_MESSAGE, 0)>0){
                    SetWindowText(hWndRecvMsg, buf);    //テキスト貼り付け
                }
            }
            return 0L;
        case FD_CLOSE:                                  //切断された
            MessageBox(hWnd, "Disconnected", "Information",
                MB_OK|MB_ICONINFORMATION);
            SendMessage(hWnd,WM_COMMAND,IDB_REJECT,0);  //切断処理発行
            return 0L;
        }
        return 0L;
    case WM_SETFOCUS:
        SetFocus(IsWindowEnabled(hWndHost)?hWndHost:hWndSendMsg);
        return 0L;
    case WM_DESTROY:                                    //ウィンドウ終了通知
        closesocket(sock);                              //ソケットを閉じる
        PostQuitMessage(0);                             //プログラム終了
        return 0L;                                      //イベント処理終了
    default:
        return DefWindowProc(hWnd, uMsg, wP, lP);       //標準メッセージ処理
    }
    return 0L;                                          //イベント処理終了
}


/////////////////////////////////////////////////////////////////////////////
//
//  ソケット初期化処理
//
BOOL SockInit(HWND hWnd)
{
    WSADATA wsa;
    int ret;
    if((ret=WSAStartup(MAKEWORD(1,1), &wsa))){
        char buf[80];
        wsprintf(buf, "%d is the err", ret);
        MessageBox(hWnd, buf, "Error", MB_OK|MB_ICONSTOP);
        exit(-1);
    }
    return FALSE;
}


/////////////////////////////////////////////////////////////////////////////
//
//  ソケット接続 (クライアント側)
//
BOOL SockConnect(HWND hWnd, LPCSTR host)
{
    SOCKADDR_IN cl_sin;                                     //SOCKADDR_IN構造体

    //ソケットを開く
    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);       //ソケット作成失敗
    if(sock==INVALID_SOCKET){
        MessageBox(hWnd, "Socket() failed", "Error",
            MB_OK|MB_ICONEXCLAMATION);
        return TRUE;
    }

    memset(&cl_sin, 0x00, sizeof(cl_sin));                  //構造体初期化
    cl_sin.sin_family = AF_INET;                            //インターネット
    cl_sin.sin_port   = htons(PORT);                        //ポート番号指定

    if(!(phe=gethostbyname(host))){                         //アドレス取得
        MessageBox(hWnd, "gethostbyname() failed.", "Error",
            MB_OK|MB_ICONEXCLAMATION);
        return TRUE;
    }
    memcpy(&cl_sin.sin_addr, phe->h_addr, phe->h_length);   //アドレス値格納

    //非同期モード (接続)
    if(WSAAsyncSelect(sock, hWnd, WM_SOCKET, FD_CONNECT)==SOCKET_ERROR){
        closesocket(sock);
        sock=INVALID_SOCKET;
        MessageBox(hWnd, "WSAAsyncSelect() failed", "Error",
            MB_OK|MB_ICONEXCLAMATION);
        return TRUE;
    }

    //接続処理
    if(connect(sock, (LPSOCKADDR)&cl_sin, sizeof(cl_sin))==SOCKET_ERROR){
        if(WSAGetLastError()!=WSAEWOULDBLOCK){
            closesocket(sock);
            sock=INVALID_SOCKET;
            MessageBox(hWnd, "connect() failed", "Error",
                MB_OK|MB_ICONEXCLAMATION);
            return TRUE;
        }
    }
    return FALSE;
}


/////////////////////////////////////////////////////////////////////////////
//
//  接続待ち (サーバ側)
//
BOOL SockAccept(HWND hWnd)
{
    SOCKADDR_IN sv_sin;                                 //SOCKADDR_IN構造体

    //サーバ用ソケット
    sv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sv_sock == INVALID_SOCKET){                      //ソケット作成失敗
        MessageBox(hWnd, "Socket() failed", "Error",
            MB_OK|MB_ICONEXCLAMATION);
        return TRUE;
    }

    memset(&sv_sin, 0x00, sizeof(sv_sin));              //構造体初期化
    sv_sin.sin_family      = AF_INET;                   //インターネット
    sv_sin.sin_port        = htons(PORT);               //ポート番号指定
    sv_sin.sin_addr.s_addr = htonl(INADDR_ANY);         //アドレス指定

    if(bind(sv_sock, (LPSOCKADDR)&sv_sin, sizeof(sv_sin))==SOCKET_ERROR){
        closesocket(sv_sock);
        sv_sock = INVALID_SOCKET;
        MessageBox(hWnd, "bind() failed", "Error",
            MB_OK|MB_ICONEXCLAMATION);
        return TRUE;
    }

    if(listen(sv_sock, 1)==SOCKET_ERROR){               //接続待ち失敗
        closesocket(sv_sock);
        sv_sock = INVALID_SOCKET;
        MessageBox(hWnd, "listen() failed", "Error",
            MB_OK|MB_ICONEXCLAMATION);
        return TRUE;
    }

    //非同期処理モード (接続待ち)
    if(WSAAsyncSelect(sv_sock, hWnd, WM_SOCKET, FD_ACCEPT)==SOCKET_ERROR){
        closesocket(sv_sock);
        sv_sock = INVALID_SOCKET;
        MessageBox(hWnd, "WSAAsyncSelect() failed", "Error",
            MB_OK|MB_ICONEXCLAMATION);
        return TRUE;
    }
    return FALSE;
}


このプログラムをVisual C++環境下でビルドすると、きっと以下のようなエラーが出てビルドに失敗するでしょう。これは標準的なビルド環境ではWindows Socketsライブラリws2_32.lib (またはwsock32.lib)がリンクされないためです。このリンクオプションを変えるには、[プロジェクト(P)]−[設定(S)...]を選んで現れる[プロジェクト設定]ウィンドウの[リンク]タブにある[オブジェクト/ライブラリ モジュール(L):]内にws2_32.lib(またはwsock32.lib)を追加してみましょう。


--------------------構成: test - Win32 Debug--------------------
コンパイル中...
chat.cpp
リンク中...
chat.obj : error LNK2001: 外部シンボル "_recv@16" は未解決です
chat.obj : error LNK2001: 外部シンボル "_WSAAsyncSelect@16" は未解決です
chat.obj : error LNK2001: 外部シンボル "_accept@12" は未解決です
chat.obj : error LNK2001: 外部シンボル "_closesocket@4" は未解決です
chat.obj : error LNK2001: 外部シンボル "_send@16" は未解決です
chat.obj : error LNK2001: 外部シンボル "_WSAStartup@8" は未解決です
chat.obj : error LNK2001: 外部シンボル "_WSAGetLastError@0" は未解決です
chat.obj : error LNK2001: 外部シンボル "_connect@12" は未解決です
chat.obj : error LNK2001: 外部シンボル "_gethostbyname@4" は未解決です
chat.obj : error LNK2001: 外部シンボル "_htons@4" は未解決です
chat.obj : error LNK2001: 外部シンボル "_socket@12" は未解決です
chat.obj : error LNK2001: 外部シンボル "_listen@8" は未解決です
chat.obj : error LNK2001: 外部シンボル "_bind@12" は未解決です
chat.obj : error LNK2001: 外部シンボル "_htonl@4" は未解決です
Debug/chat.exe : fatal error LNK1120: 外部参照 14 が未解決です。
link.exe の実行エラー

chat.exe - エラー 15、警告 0

それでは、簡易チャットプログラムのポイントを順に説明していきましょう。

1. ソケット通信


ソースをよく見ると分かりますが、この簡易プログラムではソケットに関する処理が以下の3つの部分にまとめられています。これらは今後ソケット通信を使うプログラムを組むときにも多少の修正で使えるルーチンだと思うので、覚えておくなりファイルとして保存しておくと後々役に立つかもしれません。実際、今回のレポート課題である「ネットワーク対戦型五目並べ」のサンプルプログラムは、このソケットルーチンを切り貼りしてつくりました。Windowsで効率よくアプリケーションを作成するためには、一からキーボードを叩いてプログラムを組むのではなく、既存のソースや以前つくったソースからおいしい部分を切り貼りして組んでいくことがコツです。もちろん、この先にあるのがリサイクル化、すなわちライブラリ化ないしクラス化という考え方であるということは言うまでもありません。

ソケット初期化SockInit()
接続SockConnect()
接続待ちSockAccept()
非同期処理WM_SOCKETメッセージ

ちなみに、上の表に出てくるWM_SOCKETとは独自で定義したウィンドウメッセージIDで、


#define (WM_USER+1)
として定義されています。基本的に、WM_USER以降のウィンドウメッセージIDはユーザが自由に使っても構わないことになっています。ウィンドウ間で何らかの連絡を取りたいときなど、独自のウィンドウメッセージをつくってしまうのもテクニックの一つです。

プログラム中で、非同期処理を行うためのWSAAsyncSelect()関数の引数にWM_SOCKETを渡すことで、処理完了通知としてWM_SOCKETメッセージを発行してくれます。

2. エディットボックス


Windowsでは文字列を入力するためのインタフェースとしてエディットボックスと呼ばれるコントロールを標準で持っており、アプリケーションに利用することができます。 コントロールはウィンドウオブジェクトの一つであり、一般的なウィンドウと同様にCreateWindow()やCreateWindowEx()で生成することができます。今回のチャットプログラムでは、ホスト名入力、送信メッセージ入力、受信メッセージ表示の計3ヶ所にエディットボックスを利用しています。たとえば、ホスト名入力用のエディットボックスは以下のような形式で作成しています。なお、CreateWindow()とCreateWindowEx()の違いは拡張ウィンドウスタイルが使えるかどうかの違いだけです。拡張ウィンドウスタイルはWindows95から取り入れられたスタイルで、ここではエディットボックスの見た目が窪んだ感じなるようにするためにCreateWindowEx()を使用しています。


//ホスト名入力用エディットボックス
hWndHost = CreateWindowEx(
    WS_EX_CLIENTEDGE,                              //拡張ウィンドウスタイル
    "edit",                                        //ウィンドウクラス名
    "",                                            //テキスト内容
    WS_CHILD|WS_VISIBLE,                           //ウィンドウスタイル
    10,30, 200, 25,                                //表示位置とサイズ
    hWnd,                                          //親ウィンドウハンドル
    (HMENU)IDE_HOSTNAME,                           //識別子
    hInstance,                                     //インスタンスハンドル
    NULL);


エディットボックス内で編集された内容を取得するにはGetWindowText()を使います。上のホスト名入力用エディットボックスの場合、[接続]ボタンが押された瞬間(uMsg=WM_COMMAND, wP=IDB_CONNECT)に、入力されたホスト名が必要となりますので、この関数を利用しています。GetWindowText()はエディットボックスに限らず、スタティックコントロール(文字列表示用のウィンドウ)やボタンコントロールなど様々なウィンドウに対して行えます。


char host[100];                                    //文字列格納バッファ
GetWindowText(hWndHost, host, sizeof(host)-1);     //バッファに内容をコピー


それでは逆に文字列をこれらのコントロールに書き込むにはどうすればいいでしょうか? みなさんお察しの通りSetWindowText()というのがあるんですね。Windows API関数にGet〜()という関数があったら、それに対応するSet〜()関数がある場合がけっこう多いんですね。機能からAPI関数名を探す場合は、この辺りのことを知っておくと役に立つでしょう。

この簡易チャットプログラムでは、コネクションが確立したサーバ側で、相手のホスト名を取得してホスト名入力用エディットボックスに表示するために、以下のような方法を使ってます。ここで用いられているgethostbyaddr()はIPアドレスからホスト名を取得するためによく使われる関数で、第1引数は4bytes(long型)のIPアドレス値です。それなのに、なぜ(char *)でキャストするのかな、と疑問に思いません? おそらくこれは将来的にIPアドレスが拡張することも考えてのことでしょう。現在のIPアドレスは、IPv4規格といって4bytes(32bits)で世界中のコンピュータに一意の識別番号をつけていますが、将来的には情報家電と呼ばれるようにビデオ等のオーディオ機器から冷蔵庫に至るまで、あらゆる家電をネットワークに接続してしまおうということも考えられていて、そうなるとIPアドレスが足らなくなりますよね? これを見越して現在IPv6と呼ばれる規格も考えられているのですが、そのときになってもこの関数が使えるようにという配慮もあってのことでしょうか。第2引数の4は4bytesということなのかな??? もしそうだとすると、この簡易チャットプログラムは2000年問題ならぬIPv6問題にまったく対応できませんね。(^^; こんなプログラムを市販ソフトとして売ったりしたら賠償問題になり兼ねませんので、将来、SEになる(かもしれない)みなさんはこの辺りのことも気をつけて設計しましょう。


HOSTENT *phe;                                               //HOSTENT構造体
phe = gethostbyaddr((char *)&cl_sin.sin_addr, 4, AF_INET);  //ホスト名取得
if(phe){
    SetWindowText(hWndHost, phe->h_name);
}


3. インタフェース


アプリケーションを作成する場合に意外と重要なのはユーザインタフェースです。アプリケーションを個人で使うだけなら、それほど凝る必要はないですが、他人が使うとなれば直感的に扱えるというのはとても重要なことです。また、アプリケーションを使う対象も考慮する必要があります。UNIX使いのようなコマンド入力に馴れたエキスパートならば凝ったGUIは要らないでしょうが(むしろゴミ扱いされるかも?)、初心者など一般的なパソコンユーザやGUIに馴れたユーザを対象とする場合はキーボードでコマンドを入力して云々、というのは極力避けるべきです。

今回作成した簡易チャットプログラムでも、状況に応じてボタンが押せる状態になったり押せない状態になったり、エディットボックスが編集可能な状態になったり編集できない状態になったりと、細々とした処理を行っています。これを実現するためには、各コントロール(ウィンドウ)に対して、EnableWindow()関数を使って、有効/無効の設定をしてやります。今回のプログラムでたとえば、相手との接続が完了したとき[接続]ボタンは不要となります。むしろ無効にしなければ、接続中にさらに[接続]ボタンが押されたときの処理に困ってしまいます。一方、接続中は[切断]ボタンが有効にならなければ、意図的に接続を終了させることができません。以上のようなことを踏まえて、接続が完了した時点で、以下のような処理を行っています。

EnableWindow(hWndConnect, FALSE);                //[接続]ボタン無効
EnableWindow(hWndReject, TRUE);                  //[切断]ボタン有効

アプリケーションの使い勝手を向上させる秘訣として、ある作業の後に、ユーザが今度は何をするかを考慮することが大切です。たとえば簡易チャットプログラムで相手との接続が確立した後、ユーザは次に何をするか考えてみましょう。おそらくは送るメッセージを書きはじめることでしょう。ところが、相手に接続するためにマウスで[接続]ボタンを押したとき、フォーカスはこのボタンに移ってしまい、その状態でキーボードを打っても、送信文入力用のエディットボックスにはタイプが反映されません。そのため、[TAB]キーを押してエディットボックスにフォーカスを移すか、エディットボックス内をマウスでクリックしてフォーカスを移してやらなければなりません。これはとても使い勝手の悪いものですね。このような場合、接続が完了した時点でプログラム側でフォーカスを移してやればいいのです。

SetFocus(hWndSendMsg);                           //フォーカス移動

このようにインタフェースの開発はとてもに骨の折れる作業で、もっとも開発に時間を要する部分です。しかし、このインタフェースの善し悪しが作業効率に大きく影響を及ぼすことも事実ですので、ちゃんとした設計と、試行錯誤を繰り返しましょう。


Copyright(c)1999 T.Morioka & T.Yonekura