Internet Programming

TCPクライアントの作成手順(非同期型)

[1997/11/29]

■非同期型APIを使ったTCPクライアント作成の流れ

非同期型APIを使ったTCPクライアントの簡単な流れは、
1.ソケットの作成を行う。

2.サーバに接続する。

3.サーバと送受信を行う。

4.ソケットを破棄する。
の4段階から成り立ちます。
同期型のAPIを使った場合との違いは、サーバに接続した時や、サーバからデータを受信したときなのどソケットイベントウィンドウメッセージで通知されることです。

実際にプログラムで使うWinSockのAPI(Socket Interface)を使用してもう少し詳しくあらわすと、

  1. WSAStartup関数でWinSockの初期化を行います。
    (プログラムの開始時のみ)

  2. socket関数でソケットの作成を行います。

  3. サーバのIPアドレスを取得します。もしinet_addr関数でIPアドレスの取得ができなかった場合は、WSAAsyncGetHostByName関数でサーバのIPアドレスの取得を行います。このときサーバの情報が取得できたらウィンドウメッセージとして通知されます。

  4. WSAAsyncSelect関数でソケットイベントをウィンドウメッセージで通知されるようにします。

  5. 作成されたソケットを使用して、サーバのIPアドレスとポート番号を指定してconnect関数でサーバに接続(コネクト)します。サーバと接続するとウィンドウメッセージで通知されます。

  6. 送信バッファに空きができたことを通知するウィンドウメッセージが来たら、ソケットに対して、send関数で接続しているサーバにデータを送信します。

  7. サーバからのデータを受信したことを通知するウィンドウメッセージが来たら、ソケットに対してrecv関数でサーバからデータを受信します。

  8. サーバから切断したことを通知するウィンドウメッセージが来たら、closesocket関数でソケットを破棄します。
    または、送受信するべきデータが無くなったら、shutdown関数で送受信を無効にして、closesocket関数でソケットを破棄します。

  9. WSACleanup関数でWinSockのリソースを解放します。
    (プログラムの終了時のみ)
となります。

■APIの使用方法

WinSockのAPIを使用した例を示します。

WinSockを使用する際は、
wsock32.libをリンク
winsock.hをインクルード
する必要があります。

ソース中は、
: ソケット
: コメント
: ソケットイベント
太字 : メインのAPI
斜体 : メインのメッセージ
となっています。

●WinSockの初期化の例

	WORD wVersionRequested;
	int  nErrorStatus;
	WSADATA wsaData;

	/* WinSockの初期化を行う */
	wVersionRequested = MAKEWORD(1, 1);			/* バージョン 1.1 を要求する */
	nErrorStatus = WSAStartup(wVersionRequested, &wsaData);
	if (atexit((void (*)(void))(WSACleanup))) {		/* 終了時にWinSockのリソースを解放するようにしておく */
		MessageBox(NULL, "atexit(WSACleanup)に失敗しました。", "Error", MB_OK | MB_ICONERROR);
		return;
	}
	if ( nErrorStatus != 0 ) {
		MessageBox(NULL, "WinSockの初期化に失敗しました。", "Error", MB_OK | MB_ICONERROR);
		return;
	}

WSAStartup関数の第一引数は、要求するWinSockのバージョンを入れます。
もし、バージョン 2.0 を要求する場合は、
MAKEWORD(2, 0)
と指定します。

atexit((void (*)(void))(WSACleanup))は、終了時にWSACleanup関数を実行するようにしています。

●ソケットの作成例

	int soc;		/* ソケット(Soket Descriptor) */

	/* socにソケットを作成します */
	soc = socket(PF_INET, SOCK_STREAM, 0);
	if(soc == INVALID_SOCKET){
		MessageBox(NULL, "ソケットの作成に失敗しました。", "Error", MB_OK | MB_ICONERROR);
	}

socket関数の第1引数には、インターネットを表すPF_INETを指定して、第2引数にはTCPを表すSOCK_STREAMを指定しています。
第3引数は、プロトコルを指定します。

●サーバのIPアドレス取得の例

	#define WSOCK_GETHOST	WM_USER + 1	/* ホスト情報を取得したときに通知するメッセージ */
	#define BUFSIZE		256		/* バッファサイズ */

	char gHostEnt[MAXGETHOSTSTRUCT];	/* WSAAsyncGetHostByNameで使用するホスト情報 */
	HANDLE hGetHost;		/* WSAAsyncGetHostByNameのハンドル */

	LONG APIENTRY	MainProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		struct hostent FAR *HostEntry;	/* サーバのホスト情報 */
		unsigned long serveraddr;		/* サーバのIPアドレス(32bit) */
		char SvName[BUFSIZE];		/* サーバ名 */

		char msgbuf[BUFSIZE];		/* メッセージ用バッファ */

		switch (uMsg)
		{
		case WM_CREATE:		/* ウィンドウ作成時 */
			/* SvNameにドットで区切った10進数のIPアドレスが入っている場合、serveraddrに32bit整数のIPアドレスが返ります */
			serveraddr = inet_addr((char*)SvName);
			if(serveraddr == -1){
				/* サーバ名からサーバのホスト情報を取得する */
				hGetHost = WSAAsyncGetHostByName(hWnd,WSOCK_GETHOST,SvName,gHostEnt, MAXGETHOSTSTRUCT);
				if(hGetHost == 0){
					MessageBox(hWnd,"サーバのIPアドレスの取得に失敗しました。","Error",MB_OK | MB_ICONERROR);
				}
				break;
			}
			/* IPアドレスをメッセージボックスで表示する */
			wsprintf(msgbuf,"サーバのIPアドレス = %ld",serveraddr);
			MessageBox(hWnd,msgbuf,"info",MB_OK | MB_ICONINFORMATION);
			break;

		case WSOCK_GETHOST:	/* ホスト情報を取得した時 */
			/* 結果がエラーか判定する */
			if(WSAGETASYNCERROR(lParam) != 0){
				MessageBox(hWnd,"サーバのIPアドレスの取得に失敗しました。","Error",MB_OK | MB_ICONERROR);
				break;
			}
			/* WSAAsyncGetHostByNameの戻り値である非同期なタスクのハンドルの場合 */
			if(hGetHost == (HANDLE)wParam){
				/* サーバのホスト情報からIPアドレスを抽出する */
				HostEntry = (struct hostent FAR *)gHostEnt;
				serveraddr =  *((unsigned long *)((HostEntry->h_addr_list)[0]));

				/* IPアドレスをメッセージボックスで表示する */
				wsprintf(msgbuf,"サーバのIPアドレス = %ld",serveraddr);
				MessageBox(hWnd,msgbuf,"info",MB_OK | MB_ICONINFORMATION);
			}
			break;
			:
			:

SvNameにドットで区切った10進数のIPアドレスが入っている場合は、inet_addr関数で32bitの整数値に変換されます。
例)
210.146.62.2 -(inet_addr)→ 23E92D2


SvNameにサーバのホスト名が入っている場合は、WSAAsyncGetHostByName関数でサーバのホスト情報を取得します。
WSAAsyncGetHostByName関数は、gethostbyname関数と違って処理がすぐもどります。(すぐにホスト情報が取得できるわけではない)

WSAAsyncGetHostByName関数の第1引数は、メッセージを通知するウィンドウのハンドルです。
第2引数は、通知するメッセージです。
第3引数は、ホスト情報を取得するサーバの名前へのポインタです。
第4引数は、ホスト情報を格納するバッファへのポインタです。 このバッファのサイズは、hostent構造体より大きい必要があり、MAXGETHOSTSTRUCTはそれを保証します。
第5引数は、第4引数で指定するバッファのサイズです。
戻り値は、非同期なタスクのハンドルです。

ホスト情報が取得できると、ウィンドウにメッセージが通知されます
ウィンドウプロシージャのwPramaには、WSAAsyncGetHostByName関数の戻り値である非同期なタスクのハンドルが入っています。
lParamの上位ワードにエラーコードが入っていてWSAGETASYNCERRORマクロで取得でき、0 の以外の場合はエラーです。
lParamの下位ワードには取得したホスト情報の長さが入っていて、WSAGETASYNCBUFLENマクロで取得できます。
そして、WSAAsyncGetHostByName関数の第4引数で指定したバッファには、サーバのホスト情報が入っているので、その中からIPアドレスを取り出します。
例)
www.kinet.or.jp - (WSAAsyncGetHostByName)

(ウィンドウメッセージを処理する)

(ホスト情報取得のウィンドウメッセージ) - サーバのホスト情報の中からIPアドレスを取り出します

このWSAAsyncGetHostByName関数のタスクをキャンセルするには、WSACancelAsyncRequest関数を使います。
WSACancelAsyncRequest関数の引数には、WSAAsyncGetHostByName関数の戻り値である非同期なタスクのハンドルを指定します。
例)
WSACancelAsyncRequest(hGetHost);

●ソケットイベントをウィンドウメッセージで送られるようにする例

	#define WSOCK_SELECT	WM_USER + 2	/* ソケットイベントを通知するメッセージ */

	/* 接続、送信、受信、切断のイベントをウィンドウメッセージで通知されるようにする */
	if(WSAAsyncSelect(soc, hWnd, WSOCK_SELECT, FD_CONNECT | FD_WRITE | FD_READ | FD_CLOSE) == SOCKET_ERROR){
		MessageBox(hWnd,"ソケットイベントを通知させる設定に失敗しました。","Error",MB_OK | MB_ICONERROR);
	}


WSAAsyncSelect関数は、指定のソケットの接続や送信や受信や切断などのイベントをウィンドウメッセージで通知させるようにする関数です。

WSAAsyncSelect関数の第1引数は、イベントをウィンドウメッセージで通知させるようにするソケットです。
第2引数は、ソケットイベントのウィンドウメッセージを受け取るウィンドウのハンドルです。
第3引数は、通知するメッセージです。
第4引数は、通知させるイベントのタイプです。
         FD_CONNECTは、ソケットへの接続が完了したことを通知します。
         FD_WRITEは、ソケットの送信バッファに空きができたことを通知します。
         FD_READは、ソケットの受信バッファにデータがあることを通知します。
         FD_CLOSEは、ソケットへの接続が終了したことを通知します。
戻り値が 0 の場合は成功で、SOCKET_ERRORの場合はエラーです。

複数のソケットを扱う場合は、ソケット毎にWSAAsyncSelect関数を実行します。
同一ソケットに対して複数回WSAAsyncSelect関数を実行した場合、最後に実行したWSAAsyncSelect関数だけ有効になり、それ以前のは無効になるので注意してください。

ウィンドウにソケットイベントが通知されるときは、第3引数で指定したメッセージで通知されます。
ウィンドウプロシージャのwParamにはイベントが生じたソケットが入っています。
lParamの上位ワードにはエラーコードが入っています。WSAGETSELECTERRORマクロで取得でき 0 以外の場合はエラーです。
lParamの下位ワードにFD_CONNECTやFD_WRITEやFD_READやFD_CLOSEなどのイベントのタイプが入っています。WSAGETSELECTEVENTマクロで取得できます。

ソケットのイベント通知を取り消すには、メッセージとイベントに 0 を設定します。
例)
WSAAsyncSelect(soc, hWnd, 0,0);

●サーバにコネクトする例

	/*--------------------------------
	     コネクト処理
	---------------------------------*/
	struct  sockaddr_in	 serversockaddr;		/* サーバのアドレス */

	/* サーバのアドレスの構造体にサーバのIPアドレスとポート番号を設定します */
	serversockaddr.sin_family	 = AF_INET;				/* インターネットの場合 */
	serversockaddr.sin_addr.s_addr  = serveraddr;				/* サーバのIPアドレス */
	serversockaddr.sin_port	 = htons((unsigned short)port);		/* ポート番号 */
	memset(serversockaddr.sin_zero,(int)0,sizeof(serversockaddr.sin_zero));

	/* 指定のソケットでサーバへコネクトします */
	if(connect(soc,(struct sockaddr *)&serversockaddr,sizeof(serversockaddr)) == SOCKET_ERROR){
		if(WSAGetLastError() != WSAEWOULDBLOCK){
			MessageBox(hWnd,"サーバへの接続に失敗しました。","Error",MB_OK | MB_ICONERROR);
		}
	}

	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	/*--------------------------------
	     コネクトの結果
	---------------------------------*/
	LONG APIENTRY	MainProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		switch (uMsg)
		{
		case WSOCK_SELECT:				/* ソケットイベントのメッセージの場合(WSAAsyncSelect関数にて登録) */
			if(WSAGETSELECTERROR(lParam) != 0){		/* エラーの場合 */
				MessageBox(hWnd,"ソケットイベントの通知でエラーが発生しました。","Error",MB_OK | MB_ICONERROR);
				/* ソケットを破棄する */
				closesocket(soc);
				break;
			}
			if(soc != (int)wParam){	/* 処理すべきソケットか判定 */
				break;
			}
			switch(WSAGETSELECTEVENT(lParam))
			{
			case FD_CONNECT:		/* サーバ接続したイベントの場合 */
				MessageBox(hWnd,"サーバに接続しました。","info",MB_OK | MB_ICONINFORMATION);
				break;
				:
				:

サーバのアドレスの構造体にサーバのIPアドレスとポート番号を設定して、connect関数で接続を行っています。

connect関数の第1引数は、socket関数で作成されたソケットです。
第2引数は、サーバのアドレスの構造体へのポインタで、第3引数はその構造体のサイズです。

サーバとの接続が完了すると、ウィンドウにWSAAsyncSelect関数で指定したメッセージが通知されます。
そのときの、イベントのタイプは、FD_CONNECTです。

●サーバに文字列を送信する例

	LONG APIENTRY	MainProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		switch (uMsg)
		{
		case WSOCK_SELECT:				/* ソケットイベントのメッセージの場合(WSAAsyncSelect関数にて登録) */
			if(WSAGETSELECTERROR(lParam) != 0){		/* エラーの場合 */
				MessageBox(hWnd,"ソケットイベントの通知でエラーが発生しました。","Error",MB_OK | MB_ICONERROR);
				/* ソケットを破棄する */
				closesocket(soc);
				break;
			}
			if(soc != (int)wParam){	/* 処理すべきソケットか判定 */
				break;
			}
			switch(WSAGETSELECTEVENT(lParam))
			{
			case FD_WRITE:		/* 送信バッファに空きがあることを示すイベントの場合 */
				/* 指定のソケットに文字列(buf)を送信します */
				/* 送信した文字列はサーバに届きます */
				if(send(soc, buf, lstrlen(buf), 0) == SOCKET_ERROR){
					if(WSAGetLastError() != WSAEWOULDBLOCK){
						MessageBox(hWnd,"サーバへの送信に失敗しました。","Error",MB_OK | MB_ICONERROR);
						closesocket(soc);
					}
					break;
				}
				break;
				:
				:

send関数の第1引数は、socket関数で作成されたソケットです。
第2引数は送信する文字列のバッファで、第3引数は文字列の長さです。

送信バッファに空きがあると、ウィンドウにWSAAsyncSelect関数で指定したメッセージが通知されます。
そのときの、イベントのタイプは、FD_WRITEです。
接続したあと一度このイベントが発生します。
その後送信バッファに空きがある間このイベントが発生します。

●サーバから文字列を受信する例

	#define RECVSIZE 256	/* 受信サイズ */


	LONG APIENTRY	MainProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		char buf[RECVSIZE];	/* 受信するバッファ */
		int buf_len;		/* 受信したバイト数 */

		switch (uMsg)
		{
		case WSOCK_SELECT:				/* ソケットイベントのメッセージの場合(WSAAsyncSelect関数にて登録) */
			if(WSAGETSELECTERROR(lParam) != 0){		/* エラーの場合 */
				MessageBox(hWnd,"ソケットイベントの通知でエラーが発生しました。","Error",MB_OK | MB_ICONERROR);
				/* ソケットを破棄する */
				closesocket(soc);
				break;
			}
			if(soc != (int)wParam){	/* 処理すべきソケットか判定 */
				break;
			}
			switch(WSAGETSELECTEVENT(lParam))
			{
			case FD_READ:		/* 受信バッファにデータがあることを示すイベントの場合 */
				/* ソケットから文字列を受信します */
				/* 受信した文字列は buf に入ります */
				/* 受信する文字列はサーバが送信したものです */
				buf_len = recv(soc, buf, RECVSIZE - 1, 0);
				if (buf_len == SOCKET_ERROR ){
					MessageBox(hWnd,"サーバからの受信に失敗しました。","Error",MB_OK | MB_ICONERROR);
					closesocket(soc);
					break;
				}
				buf[buf_len] = '\0';	/* 受信したバッファの後ろにヌル文字を付加する */
				break;
				:
				:

recv関数の第1引数は、socket関数で作成されたソケットです。
第2引数は受信するバッファで、第3引数は受信バッファのサイズです。

文字列を受信する場合は、受信した文字列の最後にヌル文字は付加されていないので、recv関数の戻り値である受信サイズを使ってヌル文字を付加します。

受信バッファにデータがあると、ウィンドウにWSAAsyncSelect関数で指定したメッセージが通知されます。
そのときの、イベントのタイプは、FD_READです。
例えば、100Kのデータがあって50Kしか読まなかった場合、受信バッファには50Kのデータが残っているので、またこのイベントが発生します。

●サーバへの接続が終了した例

	LONG APIENTRY	MainProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		switch (uMsg)
		{
		case WSOCK_SELECT:				/* ソケットイベントのメッセージの場合(WSAAsyncSelect関数にて登録) */
			if(WSAGETSELECTERROR(lParam) != 0){		/* エラーの場合 */
				MessageBox(hWnd,"ソケットイベントの通知でエラーが発生しました。","Error",MB_OK | MB_ICONERROR);
				/* ソケットを破棄する */
				closesocket(soc);
				break;
			}
			if(soc != (int)wParam){	/* 処理すべきソケットか判定 */
				break;
			}
			switch(WSAGETSELECTEVENT(lParam))
			{
			case FD_CLOSE:		/* サーバへの接続が終了したことを示すイベントの場合 */
				MessageBox(hWnd,"サーバから切断しました。","info",MB_OK | MB_ICONINFORMATION);
				closesocket(soc);
				break;
				:
				:

サーバへの接続が終了すると、ウィンドウにWSAAsyncSelect関数で指定したメッセージが通知されます。
そのときの、イベントのタイプは、FD_CLOSEです。
しかし、受信バッファにはデータが残っている場合があるので、closesocket関数を実行する前に受信バッファからデータを受信してしまう方がいいです。

●ソケットを破棄する例

	/* 送受信を無効にする */
	shutdown(soc, 2);
	/* ソケットを破棄する */
	closesocket(soc);

shutdown関数の第1引数は、socket関数で作成されたソケットです。
第2引数は、送信と受信の両方を無効にしています。

closesocket関数の第1引数は、socket関数で作成されたソケットです。

●WinSockのリソースを解放する例

	/* WinSockのリソースを解放します */
	WSACleanup();

WinSockのリソースを解放する関数ですが、プログラムの初めにatexit()を使って終了時に呼び出されるようにしておけばいいと思います。

●エラーコードを取得する場合

WinSockのAPIを使用してエラーになった場合、WSAGetLastError関数でエラーコードを取得することができます。


mail
メールアドレス
<nakka@nakka.com>


Internet Programming
Internet Programming