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

クライアントとサーバーが互いに接続できれば、実際にデータを送信したり受信したりできるようになります。 データを送信するには、sendを呼び出します。

int send(
  SOCKET s,
  const char *buf,
  int len,
  int flags
);

sは、接続済みソケットの記述子を指定します。 bufは、送信したいデータを格納しているバッファを指定します。 lenは、送信したいデータのサイズを指定します。 flagsは、送信方法について定義されている定数を指定します。 基本的には、0を指定します。 戻り値は、送信されたデータのサイズが返ります。 エラーが発生した場合は、SOCKET_ERRORが返ります。

送信されたデータを受信するには、recvを呼び出します。

int recv(
  SOCKET s,
  char *buf,
  int len,
  int flags
);

sは、接続済みソケットの記述子を指定します。 bufは、受信したデータを格納するバッファを指定します。 lenは、bufのサイズを指定します。 flagsは、受信方法について定義されている定数を指定します。 基本的には、0を指定します。 戻り値は、受信したデータのサイズが返ります。 また、相手が接続を閉じた場合は、0が返ります。 エラーが発生した場合は、SOCKET_ERRORが返ります。

不要になったソケットはclosesocketで閉じなければなりませんが、 ソケットデータの送受信に使用した場合は、この関数の前にshutdownを呼び出すことになります。 この関数は、データの送受信の片方または両方をできなくします。

int shutdown(
  SOCKET s,
  int how
);

sは、接続済みソケットの記述子を指定します。 howは、無効にする種類を表す定数を指定します。 SD_SENDは送信を無効にし、SD_RECEIVEは受信を無効にします。 SD_BOTHを指定すると、送信と受信共に無効になります。

次に、sendとrecvの呼び出し方の例を示します。

TCHAR szBuf[256];
TCHAR szData[] = TEXT("sample");
int   nLen;

connect(soc, ...);

nLen = sizeof(szData);
send(soc, szData, nLen, 0);

nLen = sizeof(szBuf)
recv(soc, szBuf, nLen, 0);

MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

前節で述べたように、connectが成功したということはサーバーに接続できたということですから、 sendを呼び出してサーバーにデータを送信することができます。 この例では、第2引数に指定しているszDataがsampleという文字列を格納していますから、 この文字列がサーバーに送信されることになります。 sendの後にrecvを呼び出しているのは、先に送信したsampleという文字列に対する応答を、 サーバーから受信するためです。 recvはデータを受信するまで制御を返さないため、後続の処理であるMessageBoxが呼ばれるのは、 サーバーがsendを呼び出してそのデータがクライアントに届いてからとなります。

recvのような、目的の動作が完了するまで制御を返さない仕組みは、ブロッキングと呼ばれています。 ブロッキングは、動作が完了したタイミングを正確に知るうえで欠かせないものですが、 GUIアプリケーションなどにとっては不都合になることがあります。 たとえば、上記のコードをウインドウプロシージャで実行した場合、 recvで待機している間は他のメッセージを処理することができなくなるという問題があります。 これを防ぐには、新しくスレッドを作成し、そこでrecvを呼び出す方法が考えられます。

DWORD WINAPI ThreadProc(LPVOID lpParamater)
{
	SOCKET soc = *((SOCKET *)lpParamater); // メインスレッドからソケットのハンドルを受け取る
	TCHAR  szData[256];
	int    nLen;
	int    nResult;

	while (!g_bExitThread) {
		nLen = sizeof(szData);
		nResult = recv(soc, (char *)szData, nLen, 0);
		if (nResult == 0) {
			MessageBox(NULL, TEXT("サーバーとの接続が切断されました。"), TEXT("OK"), MB_OK);
			break;
		}
		else if (nResult > 0) {
			TCHAR  szBuf[256];
			wsprintf(szBuf, TEXT("%dバイト受信しました。\n%s"), nResult, szData);
			MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);	
		}
		else
			;
	}

	return 0;
}

新しく作成されたスレッドはこの関数内でrecvを呼び出し、受信したデータを表示します。 このようにすれば、メインスレッドが待機することはなくなりますし、 基本的にスレッドはrecvによって待機した状態にいますから、CPU使用率が上がるようなこともなくなります。 ただし、上記コードには全くの問題がないわけではありません。 メインスレッドはアプリケーションが終了すべき段階になると、 g_bExitThreadをTRUEにしてスレッドがループから抜けられるようにするのですが、 スレッドがrecvで待機している場合は、この変数の値が確認されることはありません。 つまり、g_bExitThreadをTRUEにしても、スレッドが直ちにループから抜けられるとは限りません。 メインスレッドが終了すれば、受信スレッドも強制的に終了することになりますが、 リソースを適切に開放するという点などにおいて、できればスレッドは自分自身で終了するべきといえます。

上記の問題を解消するためには、ソケットのブロッキングモードを変更する方法があります。 ソケットのブロッキングモードを非ブロッキングにした場合、 recv(connectやacceptも)は直ちに制御を返すようになるため、 別スレッドによって変更された変数を確認できるようになります。 ブロッキングモードは、ioctlsocketで変更することができます。

int ioctlsocket(
  SOCKET s,
  long cmd,
  u_long *argp
);

sは、ソケットの記述子を指定します。 cmdは、ソケットに設定したいコマンドの種類を表す定数を指定します。 ブロッキングモードを変更する場合は、FIONBIOを指定します。 argpは、設定したい値を格納している変数のアドレスを指定します。 cmdがFIONBIOでargpが0でない値の場合、ソケットは非ブロッキングモードになり、 recvは直ちに制御を返すようになります。

今回のクライアントプログラムは、対応するサーバープログラムにデータを送信します。 これを取得したサーバーはクライアントに応答データを送信し、 クライアントはこれを別スレッドで受信します。 事前に、対応するサーバープログラムを起動しておいてください。

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

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

#define ID_SEND 100
#define ID_EDIT 200

BOOL g_bExitThread = FALSE;

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort);
DWORD WINAPI ThreadProc(LPVOID lpParamater);
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 HANDLE hThread = NULL;
	static SOCKET soc = INVALID_SOCKET;
	
	switch (uMsg) {

	case WM_CREATE: {
		DWORD dwThreadId;
		
		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", "4000");
		if (soc == INVALID_SOCKET)
			return -1;

		hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, &soc, 0, &dwThreadId);

		return 0;
	}

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

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

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

		nResult = send(soc, (char *)szData, nLen, 0);
		if (nResult == SOCKET_ERROR)
			MessageBox(NULL, TEXT("データの送信に失敗しました。"), NULL, MB_ICONWARNING);

		return 0;
	}

	case WM_DESTROY:
		if (hThread != NULL) {
			g_bExitThread = TRUE;
			WaitForSingleObject(hThread, 1000);
			CloseHandle(hThread);
		}

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

		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

DWORD WINAPI ThreadProc(LPVOID lpParamater)
{
	SOCKET soc = *((SOCKET *)lpParamater);
	TCHAR  szData[256];
	int    nLen;
	int    nResult;
	DWORD  dwNonBlocking = 1;

	ioctlsocket(soc, FIONBIO, &dwNonBlocking);

	while (!g_bExitThread) {
		nLen = sizeof(szData);
		nResult = recv(soc, (char *)szData, nLen, 0);
		if (nResult == 0 || WSAGetLastError() == WSAECONNRESET) {
			MessageBox(NULL, TEXT("サーバーとの接続が切断されました。"), TEXT("OK"), MB_OK);
			break;
		}
		else if (nResult > 0) {
			TCHAR  szBuf[256];
			wsprintf(szBuf, TEXT("%dバイト受信しました。\n%s"), nResult, szData);
			MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);	
		}
		else
			;

		Sleep(100);
	}

	return 0;
}

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort)
{
	WSADATA    wsaData;
	ADDRINFO   addrHints;
	LPADDRINFO lpAddrList;
	SOCKET     soc;
	
	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;
	}
	
	freeaddrinfo(lpAddrList);

	return soc;
}

WM_CREATEで呼び出しているInitializeWinsockは、 引数で指定されたサーバーのポートへ接続を行います。 この関数は、WSAStartupからconnectまでの呼び出しを行っているため、 この関数が成功した場合は、サーバーとの通信を行うことができます。 CreateThreadを呼び出しているのは、サーバーからのデータを受信するためのスレッドを作成するためです。 recvの呼び出しにはソケットが必要になるため、第4引数にソケットを指定することで第3引数の関数に渡そうとしています。

ウインドウに表示されているエディットコントロールに文字列を設定して「送信」ボタンを押した場合、 その文字列がサーバーに送信されることになります。 この処理は、WM_COMMANDで行われています。

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

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

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

	nResult = send(soc, (char *)szData, nLen, 0);
	if (nResult == SOCKET_ERROR)
		MessageBox(NULL, TEXT("データの送信に失敗しました。"), NULL, MB_ICONWARNING);

	return 0;
}

ボタンの識別子はID_SENDに設定しているため、これ以外の理由でWM_COMMANDが送られた場合はreturnするようにしています。 まず、GetWindowTextでエディットコントロールから文字列を取得すると共に、 その文字数を戻り値から取得します。 続いて、文字数に'\0'文字を含めるために+1をし、サイズに変換するためにsizeof(TCHAR)を掛けます。 これによって、送信するデータとサイズが揃ったことになりますから、 sendを呼び出すことでサーバーへ送信します。

ThreadProcは、CreateThreadによって作成されたスレッドが実行することになります。 この関数では、recvを呼び出してサーバーからデータを受信します。

DWORD WINAPI ThreadProc(LPVOID lpParamater)
{
	SOCKET soc = *((SOCKET *)lpParamater);
	TCHAR  szData[256];
	int    nLen;
	int    nResult;
	DWORD  dwNonBlocking = 1;

	ioctlsocket(soc, FIONBIO, &dwNonBlocking);

	while (!g_bExitThread) {
		nLen = sizeof(szData);
		nResult = recv(soc, (char *)szData, nLen, 0);
		if (nResult == 0 || WSAGetLastError() == WSAECONNRESET) {
			MessageBox(NULL, TEXT("サーバーとの接続が切断されました。"), TEXT("OK"), MB_OK);
			break;
		}
		else if (nResult > 0) {
			TCHAR  szBuf[256];
			wsprintf(szBuf, TEXT("%dバイト受信しました。\n%s"), nResult, szData);
			MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);	
		}
		else
			;

		Sleep(100);
	}

	return 0;
}

まず、ioctlsocketにFIONBIOと1を指定することで、ソケットを非ブロッキングモードに設定します。 これにより、recvはサーバーからデータを届いてない場合でも制御を返すことになります。 ただし、制御を返すということは、また直ぐにrecvを呼び出すことも意味しているため、 Sleepを呼び出すことで指定されたミリ秒だけ待機するようにしています。 そして、Sleepから制御が返った場合はg_bExitThreadの値を確認してから、 recvを呼び出すようにしています。 こうすることで、スレッドが終了するべきかどうかを確認できるようになります。 recvではデータが届いていない場合はSOCKET_ERRORを返しますが、 データを受信できた場合は0以上の値を返すことになっています。 よって、この場合は受信したデータを表示することができます。 0が返った場合は、サーバーが接続を切断したことを意味しているため、 これ以上サーバーからデータが送信されることはありません。 よって、ループをbreakすることになります。

最後に、メインスレッドが実行するWM_DESTROYの処理を確認します。

if (hThread != NULL) {
	g_bExitThread = TRUE;
	WaitForSingleObject(hThread, 1000);
	CloseHandle(hThread);
}

g_bExitThreadをTRUEにすることにより、スレッドは自身が終了すべきタイミングを知ることができます。 WaitForSingleObjectは、スレッドが終了するまで、または第2引数のミリ秒時間が経過するまで待機しますが、 タイムアウトによって関数が制御を返すことはないはずです。 今回のスレッドは100ミリ秒間隔でg_bExitThreadの値を確認しているため、 直ちにThreadProcからretuenすることになり、 それに伴ってWaitForSingleObjectも制御を返すことになります。

GetAddrInfoExについて

ホスト情報からアドレスを取得するgetaddrinfoには、 引数の文字列がchar型であるという欠点があります。 アプリケーションがUNICODEとして動作するという前提があるのならば、 GetAddrInfoWという関数を呼び出すこともできますが、 できれば全ての文字列はTCHAR型として扱うのが理想といえます。 Windows Vistaから登場したGetAddrInfoExは、文字列の型がTCHARであるため、 これを呼び出す例を示します。

SOCKET InitializeWinsock(LPTSTR lpszServerName, LPTSTR lpszPort)
{
	WSADATA     wsaData;
	ADDRINFOEX  addrHints;
	PADDRINFOEX pAddrList;
	SOCKET      soc;
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

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

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

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

	if (connect(soc, pAddrList->ai_addr, (int)pAddrList->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);
		FreeAddrInfoEx(pAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	FreeAddrInfoEx(pAddrList);

	return soc;
}

GetAddrInfoExを呼び出す場合は、ADDRINFO構造体ではなくADDRINFOEX構造体を呼び出します。 また、取得したアドレスを開放する関数は、FreeAddrInfoExになります。 GetAddrInfoExの第3引数は、アドレスを取得するために検索するネームスペースの識別子であり、 NS_DNSを指定した場合はDNSを通じてアドレスを取得しようとします。 第4引数は、明示的に参照したいネームサービスプロバイダーのGUIDを指定しますが、NULLで問題ありません。 pAddrList以降の引数については、現在はまだ使用されないためNULLを指定します。



戻る