EternalWindows
Winsock / 接続と待ち受け
対応するクライアントはこちら

ネットワーク通信が行われる場合、原則として接続をする側と接続を待つ側の2種類が存在します。 基本的に、この接続をする側がクライアントであり、接続を待つ側がサーバーとなるわけですが、 サーバーはクライアントよりも先に起動しておく必要があります。 そうでなければ、クライアントが接続を行うことができなくなるからです。 たとえば、connect関数を呼び出すにはサーバーのポート番号の格納したSOCKADDR構造体が必要になりますが、 サーバー上にこのポート番号を持つソケットが作成されていなければ、 connectの呼び出しは失敗することになります。 こうしたことから分かるように、サーバーが最初に行うべき作業は、 ソケットを作成してそれをサーバー上で使用できるようにすることです。 この作業は、ローカルアドレスとソケットの関連付けとも呼ばれ、bindで行うことになります。

int bind(
  SOCKET s,
  const struct sockaddr *name,
  int namelen
);

sは、ソケットの記述子を指定します。 nameは、アドレス情報を格納したSOCKADDR構造体のアドレスを指定します。 この情報がソケットと関連付けられることになります。 namelenは、nameのサイズを指定します。

ソケットの関連付けを終えたサーバーが次に行うべき処理は、 接続要求を受け入れるためのキューを作成することです。 クライアントがconnectを呼び出した場合、 このキューに接続要求が格納され、これをサーバーは取得することになります。 キューを作成するには、listenを呼び出します。

int listen(
  SOCKET s,
  int backlog
);

sは、bindに指定したソケットの記述子を指定します。 backlogは、キューに格納できる接続要求の最大数を指定します。

listenでキューを作成すれば、そのキューに接続要求が格納されるまで処理を待機することになります。 これには、acceptを呼び出します。

SOCKET accept(
  SOCKET s,
  struct sockaddr *addr,
  int *addrlen
);

sは、bindに指定したソケットの記述子を指定します。 addrは、接続してきたクライアントの情報を受け取るSOCKADDR構造体のアドレスを指定します。 不要な場合は、NULLを指定することができます。 addrlenは、addrのサイズを格納した変数のアドレスを指定します。 addrにNULLを指定した場合は、NULLを指定することができます。 戻り値は、クライアントと接続されたソケットが返ります。

acceptがソケットを返すことから分かるように、サーバーは2つ以上のソケットを管理することになります。 1つはbindからacceptまでの呼び出しに必要なソケットであり、リッスンソケットと呼ばれています。 これは、クライアントからの接続要求を受信するためのソケットです。 そしてもう1つは、acceptが返すソケットであり、 これは実際にサーバーがクライアントと通信するためのソケットであることから、 サーバーソケットと呼ばれています。 注意しなければならいのは、クライアントのソケットと接続されるのは、 リッスンソケットではなくサーバーソケットであるという点です。 このため、接続を行うクライアントの数だけ、サーバーソケットは増えることになります。

acceptが返すSOCKADDR構造体にはクライアントのアドレス情報が格納されていますから、 接続してきたクライアントについての情報を取得したい場合は便利な存在といえます。 たとえば、次に示すgetnameinfoを呼び出せば、クライアントのホスト名を取得することができます。

int WSAAPI getnameinfo(
  const struct sockaddr FAR *sa,
  socklen_t salen,
  char FAR *host,
  DWORD hostlen,
  char FAR *serv,
  DWORD servlen,
  int flags
);

saは、SOCKADDR構造体のアドレスを指定します。 salenは、saのサイズを指定します。 hostは、ホスト名を受け取るバッファを指定します。 hostlenは、hostのサイズを指定します。 servは、サービス名またはポート番号を受け取るバッファを指定します。 servlenは、servのサイズを指定します。 flagsは、0または定義されている定数を指定します。 NI_NAMEREQDを指定した場合は、ホスト名を取得できなかった場合に関数がエラーを返し、 NI_NUMERICSERVを指定した場合はservにポート番号が返ります。

今回のサーバープログラムは、対応するクライアントプログラムが接続を行うまで待機し、 接続を受信した場合にメッセージボックスを表示します。 クライアントプログラムを起動してもサーバープログラムが反応しない場合は、 タスクマネージャなどで強制的にサーバープログラムを終了させてください。

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

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WSADATA          wsaData;
	ADDRINFO         addrHints;
	LPADDRINFO       lpAddrList;
	SOCKET           socListen;
	SOCKET           socServer;
	SOCKADDR_STORAGE sockAddr;
	int              nAddrLen;
	char             szHostName[256];
	char             szBuf[256];
	
	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, "3000", &addrHints, &lpAddrList) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return 0;
	}

	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 0;
	}
	
	if (listen(socListen, 1) == SOCKET_ERROR) {
		closesocket(socListen);
		freeaddrinfo(lpAddrList);
		WSACleanup();
		return 0;
	}

	MessageBox(NULL, TEXT("接続要求を受信するまで待機します。"), TEXT("OK"), MB_OK);
	
	nAddrLen = sizeof(SOCKADDR_STORAGE);
	socServer = accept(socListen, (LPSOCKADDR)&sockAddr, &nAddrLen);
	if (socServer == INVALID_SOCKET) {
		closesocket(socListen);
		closesocket(socServer);
		freeaddrinfo(lpAddrList);
		WSACleanup();
		return 0;
	}

	getnameinfo((LPSOCKADDR)&sockAddr, nAddrLen, szHostName, sizeof(szHostName), NULL, 0, 0);
	wsprintfA(szBuf, "%sが接続しました。", szHostName);
	MessageBoxA(NULL, szBuf, "OK", MB_OK);

	closesocket(socListen);
	closesocket(socServer);
	freeaddrinfo(lpAddrList);
	WSACleanup();

	return 0;
}

bindの呼び出しにはSOCKADDR構造体が必要になるため、 クライアントプログラムと同じようにgetaddrinfoで作成することになります。 ただし、今回はbindのために初期化するため、ai_flagsにAI_PASSIVEを指定します。 getaddrinfoの第1引数にNULLを指定した場合は、 ループバックアドレスからの接続を受信するようになります。 特定のネットワークからの接続を受信したいような場合は、 そのIPアドレスを指定することになります。 第2引数に指定するポート番号は、クライアントで指定した値と同一にしておくことになります。

bindの呼び出しを終え、listenによって接続要求を受け入れるキューを作成したならば、 acceptを呼び出して接続要求を受信するまで待機することになります。 今回のプログラムでは、acceptを呼び出す前にメッセージボックスを表示していますが、 これは待機状態に入ることを示す合図のようなものです。 この合図に応答した後、対応するクライアントプログラムを起動すれば、 クライアントのconnect呼び出しによりacceptは制御を返し、 クライアントと接続されたサーバーソケットを取得することができます。 今回は、このサーバーソケットを使用して実際に通信を行っていませんが、 その代わりとしてgetnameinfoでクライアントのホスト名を取得しています。 getnameinfoの第1引数に指定しているのは、acceptが返したクライアントのアドレス情報ですが、 この型はSOCKADDR構造体ではなくSOCKADDR_STORAGE構造体になっています。 SOCKADDR構造体を使用してしまうと、IPv6(AF_INET6)の使用時に問題が発生してしまうため、 IPv4とIPv6のどちらにも対応するSOCKADDR_STORAGE構造体を使用しています。

従来のbind呼び出し

今回のプログラムでは、bindの呼び出しに必要なSOCKADDR構造体を取得するためにgetaddrinfoを呼び出していましたが、 この構造体は明示的に初期化することもできます。 次に例を示します。

#include <winsock2.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WSADATA     wsaData;
	SOCKET      socListen;
	SOCKET      socServer;
	SOCKADDR    sockAddr;
	SOCKADDR_IN sockAddrIn;
	LPHOSTENT   lpHostnet;
	int         nAddrLen;
	char        szBuf[256];
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	socListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	sockAddrIn.sin_family      = AF_INET;
	sockAddrIn.sin_port        = htons(3000);
	sockAddrIn.sin_addr.s_addr = INADDR_ANY;

	if (bind(socListen, (SOCKADDR *)&sockAddrIn, sizeof(SOCKADDR)) == SOCKET_ERROR) {
		MessageBox(NULL, TEXT("ローカルアドレスとソケット関連付けに失敗しました。"), NULL, MB_ICONWARNING);
		closesocket(socListen);
		WSACleanup();
		return 0;
	}

	if (listen(socListen, 1) == SOCKET_ERROR) {
		closesocket(socListen);
		WSACleanup();
		return 0;
	}
	
	MessageBox(NULL, TEXT("接続要求を受信するまで待機します。"), TEXT("OK"), MB_OK);

	nAddrLen = sizeof(SOCKADDR);
	socServer = accept(socListen, &sockAddr, &nAddrLen);
	if (socServer == INVALID_SOCKET) {
		closesocket(socListen);
		closesocket(socServer);
		WSACleanup();
		return 0;
	}
	
	lpHostnet = gethostbyaddr((char *)&((LPSOCKADDR_IN)&sockAddr)->sin_addr, 4, AF_INET);
	wsprintfA(szBuf, "%sが接続しました。", lpHostnet->h_name);
	MessageBoxA(NULL, szBuf, "OK", MB_OK);

	closesocket(socListen);
	closesocket(socServer);
	WSACleanup();

	return 0;
}

SOCKADDR構造体に格納されている情報は、SOCKADDR_IN構造体と同一になっています。 SOCKADDR_IN構造体を使用すれば、各情報を維持しているメンバにアクセスすることができるため、 アドレスファミリを表すsin_family、ポート番号を表すsin_port、 IPアドレスを表すsin_addrを初期化することになります。 htonsという関数は、バイトオーダーをビッグエンディアンに変換する関数であり、 INADDR_ANYはローカルアドレスを表す特別な定数です。 bindはローカルアドレスに対して行うべきものですから、明示的に数値化したIPアドレスを指定する必要はありません。

getaddrinfoを呼び出さなければ、Windows XP以前でも動作可能なプログラムとなりますが、 このような互換性について考慮する場合は、同じくWindows XPから登場したgetnameinfoの呼び出しも避けなければなりません。 この関数は、SOCKADDR構造体に格納されたIPアドレスから関連するホスト名を取得する関数ですが、 この動作はgethostbyaddrでも実現することができます。 第1引数にIPアドレスを格納しているsin_addrメンバを指定し、 残りの引数はIPアドレスがIPv4かIPv6のどちらであるかを指定します。 IPv4である場合は第2引数に4を指定し、第3引数にAF_INETを指定します。



戻る