EternalWindows
Winsock / データの送受信(XP編)
対応するクライアントはこちら

Windows XPから追加されたAcceptExという関数は、acceptと比べて大きく異なる点が1つあります。 それは、AcceptExがクライアントからの接続を受け入れるだけでなく、 クライアントからの最初の送信データも受信するという点です。 言い換えれば、クライアントがconnectを呼び出しただけでは、AcceptExの処理は完了しません。 ConnectExを呼び出すか、connectの後にsendを呼び出すことによって、初めてAcceptExの処理が完了することになります。 AcceptExは、次のように定義されています。

BOOL AcceptEx(
  SOCKET sListenSocket,
  SOCKET sAcceptSocket,
  PVOID lpOutputBuffer,
  DWORD dwReceiveDataLength,
  DWORD dwLocalAddressLength,
  DWORD dwRemoteAddressLength,
  LPDWORD lpdwBytesReceived,
  LPOVERLAPPED lpOverlapped
);

sListenSocketは、リッスンソケットの記述子を指定します。 sAcceptSocketは、クライアントと接続したいサーバーソケットを指定します。 lpOutputBufferは、バッファを指定します。 このバッファには、sendで送信されたデータ、サーバーのアドレス、クライアントのアドレスの3つが連続して格納されています。 dwReceiveDataLengthは、lpOutputBufferにおいて、クライアントから送信されたデータを受け取る範囲のサイズを指定します。 dwLocalAddressLengthは、サーバーのアドレスのサイズを指定します。 アドレスはSOCKADDR_IN構造体で表すことができますが、実際にはこれに+16をしなければならないようです。 dwRemoteAddressLengthは、クライアントのアドレスのサイズを指定します。 サイズの指定方法は、dwLocalAddressLengthと同一です。 lpdwBytesReceivedは、実際にlpOutputBufferに格納されたデータのサイズを指定します。 lpOverlappedは、OVERLAPPED構造体のアドレスを指定します。 NULLを指定することはできません。

AcceptExはブロッキングを行わない設計になっているため、 クライアントがconnectとsendを呼び出す前に制御を返すことになります。 サーバーはAcceptExが返った後に他の作業を行い、 クライアントからの接続を確認したくなった段階で待機処理を行います。 このような、最初に操作を開始しておき、しばらく経ってから操作の完了を確認する処理はオーバーラップと呼ばれています。 アプリケーションは、操作の完了を確認したくなった時点でWSAGetOverlappedResultを呼び出します。

BOOL WSAAPI WSAGetOverlappedResult(
  SOCKET s,
  LPWSAOVERLAPPED lpOverlapped,
  LPDWORD lpcbTransfer,
  BOOL fWait,
  LPDWORD lpdwFlags
);

sは、ソケットの記述子を指定します。 lpOverlappedは、WSAOVERLAPPED構造体のアドレスを指定します。 lpcbTransferは、データのサイズを受け取る変数のアドレスを指定します。 fWaitは、操作が完了するまで待機するかどうかを指定します。 この完了というのは、sに指定したソケットに対して変化が生じた場合のことを意味します。 また、OVERLAPPED.hEventにイベントオブジェクトを指定している場合は、 完了時にオブジェクトがシグナル状態になります。 TRUEを指定した場合は、待機することになります。 lpdwFlagsは、完了状態を表す定数を受け取る変数のアドレスを指定します。

サーバーにとって有用になる関数には、AcceptExの他にTransmitFileがあります。 この関数は、ファイルハンドルを指定することでファイル内のデータを送信することができるので、 明示的にファイルからデータを取得するという処理が省略されることになります。 ファイルをクライアントに返すHTTPサーバーなどにとっては、この関数が非常に便利となります。

BOOL TransmitFile(
  SOCKET hSocket,
  HANDLE hFile,
  DWORD nNumberOfBytesToWrite,
  DWORD nNumberOfBytesPerSend,
  LPOVERLAPPED lpOverlapped,
  LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
  DWORD dwFlags
);

hSocketは、ソケットの記述子を指定します。 hFileは、データを送信したいファイルのハンドルを指定します。 ハンドルがFILE_FLAG_SEQUENTIAL_SCANを指定して開かれている場合、 キャッシュマネージャのパフォーマンスが向上することになります。 nNumberOfBytesToWriteは、ファイル内のデータを送信するバイト数を指定します。 0を指定すると、全データが送信されることになります。 nNumberOfBytesPerSendは、0でよいと思われます。 lpOverlappedは、OVERLAPPED構造体のアドレスを指定します。 lpTransmitBuffersは、ファイル内のデータの前後に追加したいデータを表すTRANSMIT_FILE_BUFFERS構造体のアドレスを指定します。 dwFlagsは、送信方法について表す定数を指定します。 0を指定しても問題ありません。

今回のサーバープログラムは、対応するクライアントプログラムからファイル名を受信し、 そのファイルのデータをクライアントに送信します。

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

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

#define WM_SOCKET WM_APP

SOCKET InitializeWinsock(LPSTR lpszPort);
int GetSocketIndex(SOCKET soc, SOCKET socServer[], int nMaxSocketCount);
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-server");
	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 int               nMaxSocketCount = 10;
	static SOCKET            socListen = INVALID_SOCKET;
	static SOCKET            socServer[10];
	static LPFN_ACCEPTEX     lpfnAcceptEx = NULL;
	static LPFN_TRANSMITFILE lpfnTransmitFile = NULL;
	static HWND              hwndListBox = NULL;
	
	switch (uMsg) {

	case WM_CREATE: {
		int   i;
		GUID  guidAcceptEx = WSAID_ACCEPTEX;
		GUID  guidTransmitFile = WSAID_TRANSMITFILE;
		DWORD dwBytes;

		hwndListBox = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		socListen = InitializeWinsock("6000");
		if (socListen == INVALID_SOCKET)
			return -1;

		WSAIoctl(socListen, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidAcceptEx, sizeof(GUID), &lpfnAcceptEx, sizeof(LPVOID), &dwBytes, NULL, NULL);
		if (lpfnAcceptEx == NULL)
			return -1;
		
		WSAIoctl(socListen, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidTransmitFile, sizeof(GUID), &lpfnTransmitFile, sizeof(LPVOID), &dwBytes, NULL, NULL);
		if (lpfnTransmitFile == NULL)
			return -1;

		for (i = 0; i < nMaxSocketCount; i++)
			socServer[i] = INVALID_SOCKET;

		WSAAsyncSelect(socListen, hwnd, WM_SOCKET, FD_ACCEPT);

		return 0;
	}
	
	case WM_SOCKET:
		switch (WSAGETSELECTEVENT(lParam)) {

		case FD_ACCEPT: {
			int           i;
			TCHAR         szBuf[256];
			TCHAR         szHandleName[256];
			DWORD         dwBytes;
			BYTE          data[1024];
			WSAOVERLAPPED overlapped;
			BOOL          bResult;
			DWORD         dwFlgas;
			
			i = GetSocketIndex(INVALID_SOCKET, socServer, nMaxSocketCount);
			socServer[i] = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

			overlapped.hEvent = NULL;
			bResult = lpfnAcceptEx(socListen, socServer[i], data, 1024 - ((sizeof(SOCKADDR_IN) + 16) * 2), sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &dwBytes, &overlapped);

			if (!bResult && WSAGetLastError() == WSA_IO_PENDING)
				WSAGetOverlappedResult(socListen, &overlapped, &dwBytes, TRUE, &dwFlgas);

			WSAAsyncSelect(socListen, hwnd, WM_SOCKET, FD_ACCEPT);
			WSAAsyncSelect(socServer[i], hwnd, WM_SOCKET, FD_READ | FD_CLOSE);

			lstrcpyn(szHandleName, (LPTSTR)data, dwBytes);
			wsprintf(szBuf, L"No%d(%s) 接続", i + 1, szHandleName);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
			
			break;
		}

		case FD_READ: {
			int    i;
			int    nLen;
			TCHAR  szBuf[256];
			TCHAR  szData[256];
			HANDLE hFile;

			i = GetSocketIndex((SOCKET)wParam, socServer, nMaxSocketCount);

			nLen = sizeof(szData);
			recv(socServer[i], (char *)szData, nLen, 0);
			
			wsprintf(szBuf, TEXT("No%d %s"), i + 1, szData);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

			hFile = CreateFile(szData, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
			if (hFile == INVALID_HANDLE_VALUE) {
				char szNotFound[] = "no file";
				
				nLen = sizeof(szNotFound);
				send(socServer[i], szNotFound, nLen, 0);
			}
			else {
				int                   nTransmitByte = 10;
				TRANSMIT_FILE_BUFFERS buffers;

				buffers.Head       = NULL;
				buffers.HeadLength = 0;
				buffers.Tail       = "\0";
				buffers.TailLength = 1;

				lpfnTransmitFile(socServer[i], hFile, nTransmitByte, 0, NULL, &buffers, 0);

				CloseHandle(hFile);
			}	
			break;
		}

		case FD_CLOSE: {
			int   i;
			TCHAR szBuf[256];

			i = GetSocketIndex((SOCKET)wParam, socServer, nMaxSocketCount);

			wsprintf(szBuf, TEXT("No%d 切断"), i + 1);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
			
			shutdown(socServer[i], SD_BOTH);
			closesocket(socServer[i]);
			socServer[i] = INVALID_SOCKET;

			break;
		}

		}
		return 0;

	case WM_SIZE:
		MoveWindow(hwndListBox, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY: {
		int i;

		if (socListen != INVALID_SOCKET) {
			closesocket(socListen);
			WSACleanup();
		}

		for (i = 0; i < nMaxSocketCount; i++) {
			if (socServer[i] != INVALID_SOCKET) {
				shutdown(socServer[i], SD_BOTH);
				closesocket(socServer[i]);
			}
		}

		PostQuitMessage(0);
		return 0;
	}

	default:
		break;

	}

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

SOCKET InitializeWinsock(LPSTR lpszPort)
{
	WSADATA    wsaData;
	ADDRINFO   addrHints;
	LPADDRINFO lpAddrList;
	SOCKET     socListen;
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

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

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

	socListen = socket(lpAddrList->ai_family, lpAddrList->ai_socktype, lpAddrList->ai_protocol);
	
	if (bind(socListen, lpAddrList->ai_addr, (int)lpAddrList->ai_addrlen) == SOCKET_ERROR) {
		MessageBox(NULL, TEXT("ローカルアドレスとソケット関連付けに失敗しました。"), NULL, MB_ICONWARNING);
		closesocket(socListen);
		freeaddrinfo(lpAddrList);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	if (listen(socListen, 1) == SOCKET_ERROR) {
		closesocket(socListen);
		freeaddrinfo(lpAddrList);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	freeaddrinfo(lpAddrList);

	return socListen;
}

int GetSocketIndex(SOCKET soc, SOCKET socServer[], int nMaxSocketCount)
{
	int i;

	for (i = 0; i < nMaxSocketCount; i++) {
		if (socServer[i] == soc)
			break;
	}

	return i;
}

mswsock.dllに実装されているAcceptExやTransmitFileを呼び出す場合は、 WSAIoctlによって関数アドレスを取得し、そのアドレスを使用して呼び出すべきとされています。 よって、第2引数にSIO_GET_EXTENSION_FUNCTION_POINTERを指定し、 第3引数に関数のGUIDを指定しています。 WSAAsyncSelectでリッスンソケットにFD_ACCEPTを指定しているため、 クライアントが接続してきた段階で第3引数のメッセージがウインドウに送られることになります。

case FD_ACCEPT: {
	int           i;
	TCHAR         szBuf[256];
	TCHAR         szHandleName[256];
	DWORD         dwBytes;
	BYTE          data[1024];
	WSAOVERLAPPED overlapped;
	BOOL          bResult;
	DWORD         dwFlgas;
	
	i = GetSocketIndex(INVALID_SOCKET, socServer, nMaxSocketCount);
	socServer[i] = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	overlapped.hEvent = NULL;
	bResult = lpfnAcceptEx(socListen, socServer[i], data, 1024 - ((sizeof(SOCKADDR_IN) + 16) * 2), sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &dwBytes, &overlapped);

	if (!bResult && WSAGetLastError() == WSA_IO_PENDING)
		WSAGetOverlappedResult(socListen, &overlapped, &dwBytes, TRUE, &dwFlgas);

	WSAAsyncSelect(socListen, hwnd, WM_SOCKET, FD_ACCEPT);
	WSAAsyncSelect(socServer[i], hwnd, WM_SOCKET, FD_READ | FD_CLOSE);

	lstrcpyn(szHandleName, (LPTSTR)data, dwBytes);
	wsprintf(szBuf, L"No%d(%s) 接続", i + 1, szHandleName);
	SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
	
	break;
}

AcceptExの第2引数にはクライアントと接続するためのソケットを指定しなければならないため、 事前にsocketでソケットを作成しておきます。 dataは、送信データ、サーバーのアドレス、クライアントのアドレスの3つを格納するバッファであり、 これは第3引数に指定します。 第4引数は送信データのサイズであり、これはdataのサイズからアドレス部分を除いた値になります。 アドレスのサイズは、sizeof(SOCKADDR_IN)に16を足したものになります。 AcceptExがWSA_IO_PENDINGを返したということは、まだ処理が完了していないことを意味するため、 WSAGetOverlappedResultを呼び出すことによって処理が完了するまで待機します。 ただし、FD_ACCEPTのメッセージが送られてきた時点で既にクライアントは接続を試みているため、 WSAGetOverlappedResultは直ちに制御を返すはずです。 接続されたサーバーソケットに対してWSAAsyncSelectを実行するのは、 クライアントからの受信と切断をメッセージによって検出するためですが、 リッスンソケットに対して再びWSAAsyncSelectを呼び出す必要はあるのでしょうか。 実は、AcceptExは処理が完了した場合に、ソケットの状態をデフォルトに変更するため、 引き続きクライアントからの接続をメッセージによって検出したい場合は、 WSAAsyncSelectを呼び出す必要があるのです。 dataの先頭にはクライアントからの送信データが格納されており、 これをクライアントのハンドルネームとして扱っています。

クライアントからデータが届いた場合は、次の処理が実行されます。

case FD_READ: {
	int    i;
	int    nLen;
	TCHAR  szBuf[256];
	TCHAR  szData[256];
	HANDLE hFile;

	i = GetSocketIndex((SOCKET)wParam, socServer, nMaxSocketCount);

	nLen = sizeof(szData);
	recv(socServer[i], (char *)szData, nLen, 0);
	
	wsprintf(szBuf, TEXT("No%d %s"), i + 1, szData);
	SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

	hFile = CreateFile(szData, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		char szData[] = "no file";
		
		nLen = sizeof(szData);
		send(socServer[i], szData, nLen, 0);
	}
	else {
		int                   nTransmitByte = 10;
		TRANSMIT_FILE_BUFFERS buffers;

		buffers.Head       = NULL;
		buffers.HeadLength = 0;
		buffers.Tail       = "\0";
		buffers.TailLength = 1;

		lpfnTransmitFile(socServer[i], hFile, nTransmitByte, 0, NULL, &buffers, 0);

		CloseHandle(hFile);
	}	
	break;
}

クライアントから送信されるデータはファイル名を想定しているため、 recvで受信したデータをCreateFileに指定しています。 このとき、パフォーマンスを向上させるためにFILE_FLAG_SEQUENTIAL_SCANを指定しています。 ファイルが存在しない場合は、存在しないことを示すデータを送信しますが、 存在する場合はTransmitFileを呼び出してファイル内の先頭10バイトを送信するようにしています。 TRANSMIT_FILE_BUFFERS構造体は、送信データの先頭と最後にデータを追加することができるため、 この例では最後のデータを示すTailに"\0"文字を指定し、 TailLengthにそのサイズを指定しています。 TransmitFileで送信されるデータには自動で"\0"文字が追加されることはありませんから、 クライアントからの要求がテキストファイルであるとわかっている場合は、有効な処理といえます。 なお、TransmitFileでテキストファイルを送信した場合は、 相手がchar型として文字列を受信することになる点に注意してください。

GetAcceptExSockaddrsについて

AcceptExの第3引数に返されるデータには、クライアントが送信したデータ、サーバーのアドレス、クライアントのアドレスの3つが格納されています。 クライアントが送信したデータは、先頭に格納されている関係上、容易に取得することができますが、 残り2つのアドレスを取得するには、GetAcceptExSockaddrsを呼び出すことになります。

void ShowClientAddress(SOCKET soc, LPVOID lpData, DWORD dwDataSize)
{
	LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs;
	GUID                      guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;
	DWORD                     dwBytes;
	LPSOCKADDR                lpAddrLocal;
	LPSOCKADDR                lpAddrRemote;
	int                       nLocalLength;
	int                       nRemoteLength;
	char                      szHostName[256];

	WSAIoctl(soc, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidGetAcceptExSockaddrs, sizeof(GUID), &lpfnGetAcceptExSockaddrs, sizeof(LPVOID), &dwBytes, NULL, NULL);
	lpfnGetAcceptExSockaddrs(lpData, dwDataSize - ((sizeof(SOCKADDR_IN) + 16) * 2), sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &lpAddrLocal, &nLocalLength, &lpAddrRemote, &nRemoteLength);

	getnameinfo(lpAddrRemote, nRemoteLength, szHostName, sizeof(szHostName), NULL, 0, 0);
	MessageBoxA(NULL, szHostName, "OK", MB_OK);
}

GetAcceptExSockaddrsを呼び出すには、WSAIoctlで関数のアドレスを取得します。 GetAcceptExSockaddrsの第1引数はAcceptExが返したデータを受け取ったバッファを指定し、 第2引数はバッファのサイズを指定します。 ただし、サーバーのアドレスとクライアントアドレスのサイズは除いておきます。 第3引数はサーバーのアドレスのサイズ、第4引数はクライアントのアドレスのサイズを指定します。 第5引数はサーバーのアドレスを受け取る変数のアドレス、第6引数はそのサイズを受け取る変数のアドレスを指定します。 これらの変数には、NULLを指定することはできません。 第6引数はクライアントのアドレスを受け取る変数のアドレス、第7引数はそのサイズを受け取る変数のアドレスを指定します。 これらの変数にも、NULLを指定することはできません。 上記の例では、クライアントのホスト名を取得するために、 取得したクライアントアドレスをgetnameinfoに指定します。



戻る