EternalWindows
Winsock / データの送受信(XP編)
対応するサーバーはこちら

Windows XPでは、データの送信や受信などの面において、 優れたパフォーマンスを誇る関数が提供されています。 たとえば、次に示すTransmitPacketsは、sendと同じくデータを送信する関数ですが、 この関数はシステムのキャッシュマネージャと統合されています。 これにより、データをネットワーク上に送信するまでの時間がsendよりも短くなることが期待されます。

BOOL PASCAL TransmitPackets(
  SOCKET hSocket,
  LPTRANSMIT_PACKETS_ELEMENT lpPacketArray,
  DWORD nElementCount,
  DWORD nSendSize,
  LPOVERLAPPED lpOverlapped,
  DWORD dwFlags
);

hSocketは、接続済みソケットの記述子を指定します。 lpPacketArrayは、送信データを格納したTRANSMIT_PACKETS_ELEMENT構造体のアドレスを指定します。 nElementCountは、lpPacketArrayの要素数を指定します。 nSendSizeは、送信する全データのサイズだと思われますが、0で問題ありません。 lpOverlappedは、OVERLAPPED構造体のアドレスを指定します。 不要な場合は、NULLを指定して問題ありません。 dwFlagsは、関数の動作について表す定数を指定します。 0指定しても問題ありません。

Winsockの関数は基本的にws2_32.dllに実装されていますが、 TransmitPacketsなどの一部の関数はmswsock.dllに実装されています。 このDLLに実装されている関数を呼び出す場合は、 WSAIoctlを呼び出して関数アドレスを取得し、 それを使用して呼び出すべきとされています。 WSAIoctlは、実に様々な用途で使用される複雑な関数です。

int WSAIoctl(
  SOCKET s,
  DWORD dwIoControlCode,
  LPVOID lpvInBuffer,
  DWORD cbInBuffer,
  LPVOID lpvOutBuffer,
  DWORD cbOutBuffer,
  LPDWORD lpcbBytesReturned,
  LPWSAOVERLAPPED lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

sは、ソケットの記述子を指定します。 dwIoControlCodeは、行いたい動作を表す定数を指定します。 関数のアドレスを取得する場合は、SIO_GET_EXTENSION_FUNCTION_POINTERを指定します。 lpvInBufferは、dwIoControlCodeで指定した動作を行うために必要なデータを指定します。 cbInBufferは、lpvInBufferのサイズを指定します。 lpvOutBufferは、dwIoControlCodeで指定した動作の結果を受け取るバッファを指定します。 cbOutBufferは、lpvOutBufferのサイズを指定します。 lpcbBytesReturnedは、実際にlpvOutBufferに格納されたサイズを受け取る変数のアドレスを指定します。 不要な場合でも、NULLを指定できないことに注意してください。 lpOverlappedは、WSAOVERLAPPED構造体のアドレスを指定します。 不要な場合は、NULLを指定して問題ありません。 lpCompletionRoutineは、関数が制御を返した場合に呼び出される関数のアドレスを指定します。 不要な場合は、NULLを指定して問題ありません。

今回のクライアントプログラムは、対応するサーバープログラムにサーバー上のファイル名を送信します。 これを取得したサーバーは、そのファイルのデータをクライアントに送信し、 クライアントはこれを受信して表示します。 事前に、対応するサーバープログラムを起動しておいてください。

#include <winsock2.h>
#include <ws2tcpip.h>
#include <mswsock.h>

#pragma comment(lib, "ws2_32.lib")

#define ID_SEND 100
#define ID_EDIT 200
#define WM_SOCKET WM_APP

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample-client");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND                 hwndButton = NULL;
	static HWND                 hwndEdit = NULL;
	static SOCKET               soc = INVALID_SOCKET;
	static LPFN_TRANSMITPACKETS lpfnTransmitPackets = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		GUID  guidTransmitPackets = WSAID_TRANSMITPACKETS;
		DWORD dwBytes;

		hwndButton = CreateWindowEx(0, TEXT("BUTTON"), TEXT("送信"), WS_CHILD | WS_VISIBLE, 10, 10, 60, 30, hwnd, (HMENU)ID_SEND, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		hwndEdit = CreateWindowEx(0, TEXT("EDIT"), TEXT("ファイル名を入力してください。"), WS_CHILD | WS_VISIBLE | WS_BORDER, 90, 10, 300, 35, hwnd, (HMENU)ID_EDIT, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		soc = InitializeWinsock("localhost", "6000");
		if (soc == INVALID_SOCKET)
			return -1;

		WSAIoctl(soc, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidTransmitPackets, sizeof(GUID), &lpfnTransmitPackets, sizeof(LPVOID), &dwBytes, NULL, NULL);
		if (lpfnTransmitPackets == NULL)
			return -1;
		
		WSAAsyncSelect(soc, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);

		return 0;
	}

	case WM_COMMAND: {
		int                      nLen;
		TCHAR                    szData[256];
		TRANSMIT_PACKETS_ELEMENT packets;

		if (LOWORD(wParam) != ID_SEND)
			return 0;

		nLen = GetWindowText(hwndEdit, szData, sizeof(szData));
		nLen = (nLen + 1) * sizeof(TCHAR);

		packets.dwElFlags = TP_ELEMENT_MEMORY;
		packets.cLength   = nLen;
		packets.pBuffer   = szData;

		if (!lpfnTransmitPackets(soc, &packets, 1, 0, NULL, 0))
			MessageBox(NULL, TEXT("データの送信に失敗しました。"), NULL, MB_ICONWARNING);

		return 0;
	}

	case WM_SOCKET:
		switch (WSAGETSELECTEVENT(lParam)) {

		case FD_READ: {
			int  nLen;
			int  nResult;
			char szBuf[256];
			char szData[256];
			
			nLen = sizeof(szData);
			nResult = recv(soc, (char *)szData, nLen, 0);
			
			wsprintfA(szBuf, "%dバイト受信しました。\n%s", nResult, szData);
			MessageBoxA(NULL, szBuf, "OK", MB_OK);
			break;
		}

		case FD_CLOSE:
			MessageBox(NULL, TEXT("サーバーとの接続が切断されました。"), TEXT("OK"), MB_OK);
			break;

		}
		return 0;


	case WM_DESTROY:
		if (soc != INVALID_SOCKET) {
			shutdown(soc, SD_BOTH);
			closesocket(soc);
			WSACleanup();
		}

		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort)
{
	WSADATA    wsaData;
	ADDRINFO   addrHints;
	LPADDRINFO lpAddrList;
	SOCKET     soc;
	TCHAR      szHandleName[] = TEXT("handle");
	int        nLen;
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	ZeroMemory(&addrHints, sizeof(addrinfo));
	addrHints.ai_family   = AF_INET;
	addrHints.ai_socktype = SOCK_STREAM;
	addrHints.ai_protocol = IPPROTO_TCP;

	if (getaddrinfo(lpszServerName, lpszPort, &addrHints, &lpAddrList) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return INVALID_SOCKET;
	}

	soc = socket(lpAddrList->ai_family, lpAddrList->ai_socktype, lpAddrList->ai_protocol);

	if (connect(soc, lpAddrList->ai_addr, (int)lpAddrList->ai_addrlen) == SOCKET_ERROR) {
		int nError = WSAGetLastError();
		if (nError == WSAECONNREFUSED)
			MessageBox(NULL, TEXT("サーバーがリッスンしていません。"), NULL, MB_ICONWARNING);
		else if (nError == WSAEHOSTUNREACH)
			MessageBox(NULL, TEXT("サーバーが存在しません。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("サーバーへの接続が失敗しました。"), NULL, MB_ICONWARNING);
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	nLen = sizeof(szHandleName);
	send(soc, (char *)szHandleName, nLen, 0);
	
	freeaddrinfo(lpAddrList);

	return soc;
}

WM_CREATEでは、TransmitPacketsのアドレスを取得するためにWSAIoctlを呼び出しています。 第2引数には、SIO_GET_EXTENSION_FUNCTION_POINTERを指定し、 第3引数にはTransmitPacketsのアドレスを取得することを示すWSAID_TRANSMITPACKETSというGUIDを指定します。 WSAAsyncSelectにFD_READとFD_CLOSEを指定しているため、 これらのイベントが発生した場合は、ウインドウに第3引数のメッセージが送られることになります。

TransmitPacketsの呼び出しは、WM_COMMANDで行われています。

packets.dwElFlags = TP_ELEMENT_MEMORY;
packets.cLength   = nLen;
packets.pBuffer   = szData;

if (!lpfnTransmitPackets(soc, &packets, 1, 0, NULL, 0))
	MessageBox(NULL, TEXT("データの送信に失敗しました。"), NULL, MB_ICONWARNING);

TRANSMIT_PACKETS_ELEMENT構造体のdwElFlagsには、TP_ELEMENT_MEMORYを指定することができます。 これは、メモリ上に存在するデータを送信するという意味であり、 cLengthにデータのサイズを指定し、pBufferに送信したいデータを指定します。

今回のInitializeWinsockでは、対応するサーバープログラムとの関係上、 connectの後にsendを呼び出す処理が追加されています。

nLen = sizeof(szHandleName);
send(soc, (char *)szHandleName, nLen, 0);

サーバープログラムが呼び出すAcceptExという関数は、 クライアントからの接続を受け入れると共に、 最初の送信データを受信できることができます。 この場合の送信データというのは、実際に通信を始める前に意味を持つデータでなければなりませんから、 この例ではクライアントのハンドルネームを送信するようにしています。 こうすればサーバーは、AcceptExで取得したデータから誰が接続されたのかを特定できることになります。

ConnectExについて

TransmitPacketsなどと同様に、Windows XPから追加された関数としてConnectExがあります。 この関数は、接続と同時に最初のデータを送信できることから、 AcceptExで待機しているサーバーに対しては、この関数を使用するのが有効であるように思えます。 ただし、この関数を呼び出す場合は、多くの問題について考慮しなければなりません。 次に、ConnectExを呼び出すようにしたInitializeWinsockを示します。

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort)
{
	WSADATA        wsaData;
	ADDRINFO       addrHints;
	LPADDRINFO     lpAddrList;
	SOCKET         soc;
	TCHAR          szHandleName[] = TEXT("handle");
	SOCKADDR_IN    sockAddrIn;
	DWORD          dwBytes;
	LPFN_CONNECTEX lpfnConnectEx;
	GUID           guidConnectEx = WSAID_CONNECTEX;
	OVERLAPPED     overlapped;
	BOOL           bResult;
	DWORD          dwFlags;
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	ZeroMemory(&addrHints, sizeof(addrinfo));
	addrHints.ai_family   = AF_INET;
	addrHints.ai_socktype = SOCK_STREAM;
	addrHints.ai_protocol = IPPROTO_TCP;

	if (getaddrinfo(lpszServerName, lpszPort, &addrHints, &lpAddrList) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return INVALID_SOCKET;
	}

	soc = socket(lpAddrList->ai_family, lpAddrList->ai_socktype, lpAddrList->ai_protocol);

	WSAIoctl(soc, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidConnectEx, sizeof(guidConnectEx), &lpfnConnectEx, sizeof(lpfnConnectEx), &dwBytes, NULL, NULL);
	if (lpfnConnectEx == NULL) {
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}

	sockAddrIn.sin_family      = (USHORT)lpAddrList->ai_family;
	sockAddrIn.sin_port        = htons(10000);
	sockAddrIn.sin_addr.s_addr = INADDR_ANY;
	if (bind(soc, (SOCKADDR *)&sockAddrIn, sizeof(sockAddrIn)) == SOCKET_ERROR) {
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}

	overlapped.hEvent = NULL;
	bResult = lpfnConnectEx(soc, lpAddrList->ai_addr, (int)lpAddrList->ai_addrlen, szHandleName, sizeof(szHandleName), &dwBytes, &overlapped);
	if (!bResult && WSAGetLastError() != WSA_IO_PENDING) {
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}

	if (!WSAGetOverlappedResult(soc, &overlapped, &dwBytes, TRUE, &dwFlags)) {
		int nError = WSAGetLastError();
		if (nError == WSAEADDRINUSE)
			MessageBox(NULL, TEXT("ソケットのバインドに問題があります。"), NULL, MB_ICONWARNING);
		else if (nError == WSAECONNREFUSED)
			MessageBox(NULL, TEXT("サーバーがリッスンしていません。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("サーバーへの接続が失敗しました。"), NULL, MB_ICONWARNING);
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}

	freeaddrinfo(lpAddrList);

	return soc;
}

ConnectExを呼び出すためには、事前にソケットを明示的にバインドしておく必要があります。 bindを呼び出すために必要なSOCKADDR構造体はgetaddrinfoで初期化してもよいのですが、 サーバーとの接続用に既にADDRINFO構造体を使用しているため、 混同しないようにSOCKADDR構造体を明示的に初期化しています。 ConnectExの第4引数は送信したいデータを指定し、第5引数はデータのサイズを指定します。 第6引数は、実際に送信されたバイト数を受け取る変数を指定し、 第7引数はOVERLAPPED構造体を指定します。 第7引数はNULLを指定することができないので注意してください。 ConnectExは、実際に接続が完了する前に制御を返すことになっています。 この場合、関数の戻り値がFALSEになり、WSAGetLastErrorがWSA_IO_PENDINGを返すため、 この場合は関数が失敗したものと判断してはいけません。 接続の完了を確認したくなった段階でWSAGetOverlappedResultを呼び出し、 接続が完了するまで待機することになります。

上記のInitializeWinsockを利用した場合、1回目は問題なくサーバーに接続できても、 2回目に実行した場合はWSAGetOverlappedResultがWSAEADDRINUSEを返して失敗することがあります。 これは、クライアントがサーバーよりも先に切断したときに発生し、 ソケットがTIME_WAIT状態として残ってしまうことが原因です。 TIME_WAIT状態になったソケットは正常にバインドできなくなるため(bind自体が失敗するとは限らない)、 ある程度時間が経過してTIME_WAITが解除されるまで、サーバーに接続できなくなるという問題があります。 connectを呼び出しによってこの問題が発生しないのは、システムが内部でバインドを行うからであり、 この場合は使用されるポート番号が任意となるので、既にTIME_WAITのソケットに対してバインドが行われることはありません。 このため、ConnectExを呼び出す設計であっても、bindに指定するポート番号を常に任意にすれば、 サーバーへの接続に失敗することはなくなるはずです。

TIME_WAITになったソケットへの対策には、setsockoptによってSO_REUSEADDRを指定する方法があります。 これは、特定のポート番号を持ったソケットが既にバインドされていたとしても、 そのポート番号を使用して新しいソケットを強引にバインドすることを許可するというものです。 しかし、上記の例では、bindに失敗しているわけではないため、この方法は使用できません。 よって、クライアントが接続を終了する前に、DisconnectExを呼び出す方法を検討します。

lpfnDisconnectEx(soc, NULL, TF_REUSE_SOCKET, 0);

DisconnectExの第3引数にTF_REUSE_SOCKETを指定した場合、 接続を切断すると共に、ソケットが再利用できるようになるとされています。 これで、先のTIME_WAITの問題を回避できるように思えますが、 実際にはそのように上手くいきません。 今度は、bindがWSAEADDRINUSEを返して失敗することになるからです。 こうなると、bindの前に次のコードを実行すればよいように思えます。

int optval = 1;
setsockopt(soc, SOL_SOCKET, SO_REUSEADDR, (char *)&optval, sizeof(int));

このようにすれば、bindに指定されたポート番号を持つソケットがTIME_WAITで残っていたとしても、 bindは成功することになります。 しかし、この方法を利用しても、最終的にはWSAGetOverlappedResultがWSAEADDRINUSEを返すようになっており、 有効な対策を見出していないのが現状です。



戻る