EternalWindows
Winsock / 接続と待ち受け
対応するサーバーはこちら

ソケットを作成したアプリケーションは、このソケットを通じてデータの送受信を行うことになります。 ただし、TCP(IPPROTO_TCP)を利用したデータの送受信を行うためには、事前に通信相手のソケットに接続しておく必要があります。 接続を行うことで、これから両者でデータの送受信を行うことを伝えることができ、 データが確実に届くことが保証されることになります。 通信相手のソケットに接続するには、connectを呼び出します。

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

sは、ソケットの記述子を指定します。 nameは、接続したいサーバーのアドレス情報を格納しているSOCKADDR構造体のアドレスを指定します。 namelenは、nameのサイズを指定します。 戻り値は、関数が成功した場合に0、失敗した場合にSOCKET_ERRORが返ります。

Winsockでは、通信相手のIPアドレスやポート番号をsockaddr構造体で表すことになります。 ポート番号とは、ソケットに割り当てられる番号のことですが、 サービス(アプリケーション)を識別する番号として考えられることもよくあります。 ポート番号の範囲は0から65535までであり、 この中の0から1023はIANA(Internet Assigned Numbers Authority)によって用途が定められています。 たとえば、HTTPサーバーのポート番号は80にすべきことになっているため、 HTTPサーバーからファイルを受信したいクライアントは、 ポート番号80を指定してサーバーに接続すればよいことになります。 既知のポート番号(WELL KNOWN PORT NUMBER)があるおかけで、相手が提供しているサービスからポート番号が判明することが可能になり、 事前に相手のポート番号を尋ねるような処理が不要になります。

connectの引数から分かるように、この関数はポート番号やIPアドレスを直接要求するのではなく、 ポート番号やIPアドレスを格納したSOCKADDR構造体を要求しています。 よってアプリケーションは、ポート番号やIPアドレスからSOCKADDR構造体を作成する必要があります。 これには、getaddrinfoを呼び出します。

int WSAAPI getaddrinfo(
  const char *nodename,
  const char *servname,
  const struct addrinfo *hints,
  struct addrinfo **res
);

nodenameは、ホスト名またはIPアドレスを指定します。 servnameは、ポート番号を指定します。 hintsは、addrinfo構造体を作成するためのヒントを指定します。 ここで述べているヒントとは、通信に使用する通信規格やソケットの種類を決定するためのものです。 resは、作成されたADDRINFO構造体を受け取る変数のアドレスを指定します。 この構造体にSOCKADDR構造体が含まれています。 関数が成功した場合は0を返します。

getaddrinfoの第1引数に、IPアドレスではなくホスト名を指定した場合、 関数が制御を返すまで時間がかかることになります。 これは、通信において最終的に必要となるのがIPアドレスであるため、 ホスト名が指定された場合はこれをIPアドレスに変換する必要があるからです。 この変換処理の仕組みはDNSと呼ばれます。 NULLを指定した場合は、ループバックアドレスを指定したものと解釈されます。

getaddrinfoによって作成されたADDRINFO構造体は、 不要になった時点でfreeaddrinfoによって開放することになります。

void freeaddrinfo(
  struct addrinfo *ai
);

aiは、開放するADDRINFO構造体のアドレスを指定します。

今回のクライアントプログラムは、対応するサーバープログラムに対してconnectで接続を行います。 事前にサーバープログラムが起動されていない場合は、接続に失敗することになります。

#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     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("localhost", "3000", &addrHints, &lpAddrList) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return 0;
	}

	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);
		closesocket(soc);
		freeaddrinfo(lpAddrList);
		WSACleanup();
		return 0;
	}
	
	MessageBox(NULL, TEXT("サーバーに接続しました。"), TEXT("OK"), MB_OK);

	closesocket(soc);
	freeaddrinfo(lpAddrList);
	WSACleanup();

	return 0;
}

connectを呼び出すにはSOCKADDR構造体が必要になるため、 これを含んでいるADDRINFO構造体をgetaddrinfoで取得することになります。 少し複雑なのは、第3引数に指定するヒント情報の型もADDRINFO構造体になっている点ですが、 最終的に必要となるのは第4引数に返されるADDRINFO構造体です。 第3引数に指定するADDRINFO構造体は、第4引数に返されるADDRINFO構造体を初期化するための情報を与えているに過ぎません。

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

ここで指定する情報は、どのような通信を行うかといったものであり、 socket関数に指定する情報と同一です。 ai_familyはアドレスファミリを受け取るメンバであり、 AF_INETはインターネットを利用した通信を行うことを意味します。 ai_socktypeはソケットタイプであり、SOCK_STREAMは接続指向の通信を行うことを意味しています。 そして、ai_protocolは通信に使用するプロトコルであり、 IPPROTO_TCPはTCPを使用した通信を行います。 ここで指定した各メンバの値は、第4引数に返されるADDRINFO構造体に指定されることになります。 getaddrinfoは次のように呼ばれています。

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

第1引数に指定しているlocalhostという文字列は、ローカルコンピュータを表す指標として機能します。 第2引数に指定しているポート番号が3000であることから、呼び出し側アプリケーションは、 ポート番号3000のソケットに接続を行いたいことが分かります。 既に述べたように、getaddrinfoの第1引数にIPアドレスではなくホスト名を指定した場合は、 DNSによるホスト名からIPアドレスへの変換が行われます。 これには多少の時間が掛かることになりますから、可能であればIPアドレスを指定したほうがよいといえます。

ソケットの作成は、次のように行われています。

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

socketに指定する情報は、どのような通信を行うかといったものですが、 これはgetaddrinfoで取得したADDRINFO構造体に格納されています。 よって、この構造体のメンバを参照することによって指定しています。

ADDRINFO構造体が初期化されていれば、その中に存在するsockaddr構造体も初期化されていますから、 これをconnectに指定してサーバーに接続することができます。

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);
	closesocket(soc);
	freeaddrinfo(lpAddrList);
	WSACleanup();
	return 0;
}

SOCKADDR構造体は、ADDRINFO.ai_addrから参照することができるため、これを第2引数に指定します。 第3引数は、SOCKADDR構造体のサイズですが、これはADDRINFO.ai_addrlenから参照することができます。 関数の戻り値がSOCKET_ERRORの場合は関数が失敗したことを意味するため、 WSAGetLastErrorを呼び出してエラーの原因を特定しようとしています。 比較的よく見られるエラーには、WSAECONNREFUSEDとWSAEHOSTUNREACHがあります。 前者は、サーバーがlisten関数を呼び出して、クライアントからの接続を受け入れる状態になってないことを意味します。 後者は、SOCKADDR構造体に格納されているIPアドレスを持つサーバーが存在しない場合や、 ポート番号を持つソケットが存在しないことを意味します。

従来のconnect呼び出し

getaddrinfoが登場するWindows XP以前では、connectに指定するSCOKADDR構造体を明示的に初期化するのが主流でした。 次に、この例を示します。

#include <winsock2.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WSADATA     wsaData;
	SOCKET      soc;
	SOCKADDR_IN sockAddrIn;
	DWORD       dwServerIPAddress;
	char        szHostName[] = "localhost";
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);
	
	dwServerIPAddress = inet_addr(szHostName);
	if (dwServerIPAddress == INADDR_NONE) {
		LPHOSTENT lpHostnet = gethostbyname(szHostName);
		if (lpHostnet == NULL) {
			MessageBox(NULL, TEXT("ホスト名からIPアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
			WSACleanup();
			return 0;
		}

		dwServerIPAddress = *((LPDWORD)lpHostnet->h_addr_list[0]);
	}

	soc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

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

	if (connect(soc, (SOCKADDR *)&sockAddrIn, sizeof(sockAddrIn)) == 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);
		closesocket(soc);
		WSACleanup();
		return 0;
	}
	
	MessageBox(NULL, TEXT("サーバーに接続しました。"), TEXT("OK"), MB_OK);

	closesocket(soc);
	WSACleanup();

	return 0;
}

SCOKADDR構造体を初期化するには、数値化されたIPアドレスが必要になります。 一般に、IPアドレスは127.0.0.1のような文字列で表しているため、 まずはinet_addrを呼び出してこの文字列を数値に変換する必要があります。 ただし、inet_addrに指定した文字列がIPアドレスではなくホスト名である場合は変換することができないため、 INADDR_NONEが返ることになります。 この場合は、まずホスト名から対応するIPアドレスを取得する必要があるため、 これを実現するgethostbynameを呼び出すことになります。 取得したHOSTENT構造体のh_addr_listメンバに、数値化されたIPアドレスが格納されています。

connectに指定すべき引数はSCOKADDR構造体ですが、 上記の例ではSCOKADDR_IN構造体を初期化しています。 SCOKADDR構造体はアドレス情報を1つのメンバで表していますが、 SCOKADDR_IN構造体はアドレス情報を複数のメンバに分けて表しているため、 明示的に初期化する場合はSCOKADDR_IN構造体を用いるのが便利といえます。 sin_portはポート番号を受け取るメンバですが、 バイトオーダーをビッグエンディアンに変換するためにhtonsを通じて指定することになります。 sin_addr.s_addrに、数値化されたIPアドレスを指定することになります。

connectで通信相手と接続できた場合、connectに指定したソケットには、 システムによってポート番号が割り当てられます。 これを確認したい場合は、getsocknameを呼び出してSCOKADDR構造体を取得することになります。 また、サーバーがクライアントのポート番号を確認したい場合は、 getpeernameを呼び出してSCOKADDR構造体を取得します。



戻る