EternalWindows
Winsock / Winsockの位置づけ

ネットワークで繋がったコンピュータ同士が通信するための技術として、ソケットと呼ばれるものがあります。 これは、BSD系のUNIX OSで使用されているAPIであり、 ネットワークへのアクセスを提供するための関数で構成されています。 Winsockとは、このソケットの技術をWindows上で扱えるようにしたAPIであり、 BSDソケットの関数とWindows固有の関数で構成されています。 アプリケーションはどちらの関数を呼び出しても通信を行うことができますが、 UNIX OSへのコードの移植などを考慮してか、BSDソケットの関数が呼ばれることがよくあります。 Windows固有の関数には、Windows Sockets APIであることを示すWSAというプレフィックスがつくことがあります。

WinsockはネットワークAPIであるため、 これを使用すればネットワーク通信を行うことができます。 たとえばそれは、自作した2つのアプリケーション間におけるデータの送受信でも構いませんし、 インターネットを通じた不特定多数のユーザーとの対話でも構いません。 ただし、FTPやHTTPといった既存のプロトコルを用いて通信を行いたい場合は、 WinINetやWinHTTPなどのネットワークAPIを呼び出した方が、 アプリケーションの開発は容易になるでしょう。 独自のフォーマットでデータを送受信したい場合は、Winsockを使用することになります。

Winsockの関数を呼び出すためには、最初にWSAStartupでws2_32.dllを初期化することになります。 多くのWinsock関数はws2_32.dllに実装されており、WSAStartupの呼び出しに成功してから使用することができます。

int WSAStartup(
  WORD wVersionRequested,
  LPWSADATA lpWSAData
);

wVersionRequestedは、アプリケーションが要求するWinsockのバージョンを指定します。 lpWSADataは、Winsockの情報を格納したWSADATA構造体のアドレスを指定します。

アプリケーションは、Winsock関数の呼び出しが不要になった時点でWSACleanupを呼び出します。

int WSACleanup(void);

この関数に引数はありません。

どのようなネットワークAPIを呼び出すにしても、通信先を識別する必要性は生じます。 一般に、ネットワーク上のホスト(コンピュータ)を識別するために使用されるのは、 ホスト名かIPアドレスです。 IPアドレスというのは、ネットワーク上における実際のホストの位置を表す4バイトの数値であり、 "207.46.197.32"のような文字列で表すことができます。 しかし、これでは、一見してどのようなホストを表しているのかが分からないため、 人間に理解できる単語を含んだホスト名というものが利用されています。 ホスト名は、"microsoft.com"のような文字列のことであり、 文字列の内容からどのホストを表しているのかが分かることになります。 ただし、ホストの位置を識別するのはあくまでIPアドレスですから、 ホスト名だけではそのホストにアクセスすることはできません。 このため、ホスト名を使用する場合は、対応するIPアドレスを取得する必要があります。

ローカルコンピュータのホスト名は、gethostnameで取得することができます。

int gethostname(
  char *name,
  int namelen
);

nameは、ホスト名を受け取るバッファを指定します。 namelenは、nameのサイズを指定します。

特定のホストのアドレス情報を取得している場合は、 WSAAddressToStringを呼び出して文字列化されたIPアドレスを取得することができます。

INT WSAAPI WSAAddressToString(
  LPSOCKADDR lpsaAddress,
  DWORD dwAddressLength,
  LPWSAPROTOCOL_INFO lpProtocolInfo,
  LPTSTR lpszAddressString,
  LPDWORD lpdwAddressStringLength
);

lpsaAddressは、アドレス情報を格納したSOCKADDR構造体のアドレスを指定します。 dwAddressLengthは、lpsaAddressのサイズを指定します。 lpProtocolInfoは、WSAPROTOCOL_INFO構造体のアドレスを指定します。 不要な場合は、NULLを指定することができます。 lpszAddressStringは、IPアドレスを受け取るバッファを指定します。 lpdwAddressStringLengthは、lpszAddressStringのサイズを格納した変数のアドレスを指定します。

戻り値がint型である多くのWinsock関数は、0を返した場合に成功を意味し、 SOCKET_ERRORを返した場合に失敗を意味します。 失敗の場合は、WSAGetLastErrorを呼び出すことでエラーの詳細を取得することができます。

int WSAGetLastError(void);

戻り値は、エラーの内容を示す定数が返ります。

今回のプログラムは、ローカルコンピュータのホスト名とIPアドレスを表示します。 Winsock関数を呼び出すには、winsock2.hのインクルードとws2_32.libへのリンクが必要になります。

#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 lpAddrInfo;
	DWORD      dwSize;
	TCHAR      szIpAddress[256];
	char       szHostName[256];

	WSAStartup(MAKEWORD(2, 2), &wsaData);

	gethostname(szHostName, sizeof(szHostName));
	MessageBoxA(NULL, szHostName, "ホスト名", MB_OK);
	
	ZeroMemory(&addrHints, sizeof(addrinfo));
	addrHints.ai_family = AF_INET;

	if (getaddrinfo(szHostName, NULL, &addrHints, &lpAddrInfo) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return 0;
	}
	
	dwSize = sizeof(szIpAddress);
	WSAAddressToString(lpAddrInfo->ai_addr, (DWORD)lpAddrInfo->ai_addrlen, NULL, szIpAddress, &dwSize);

	MessageBox(NULL, szIpAddress, TEXT("IPアドレス"), MB_OK);

	freeaddrinfo(lpAddrInfo);
	WSACleanup();

	return 0;
}

このプログラムでは、winsock2.hの他にws2tcpip.hをインクルードしていますが、 これはWindows XP以降で追加された一部の関数(getaddrinfoなど)を呼び出す場合に必要になります。 また、windows.hはwinsock2.hによってインクルードされるため、 明示的にインクルードする必要はありません。 プログラムでは最初に、WSAStartupを呼び出してws2_32.dllを初期化しています。

WSAStartup(MAKEWORD(2, 2), &wsaData);

WSAStartupの第1引数は、使用するWinsockのバージョンになっています。 現在のWinsockはバージョン2.2が最新であるため、 MAKEWORDを上記のように呼び出すことで2.2を使用することを示します。 第2引数のwsaDataは今回のプログラムでは使用していませんが、 NULLを指定すると関数が失敗するので注意してください。 ホスト名の取得は、次のようになっています。

gethostname(szHostName, sizeof(szHostName));
MessageBoxA(NULL, szHostName, "ホスト名", MB_OK);

第1引数のバッファに、このコンピュータの名前が格納されることになります。 BSDソケットの関数は、文字列の型をchar型として扱っているため、 上記コードでは明示的にANSI版のMessageBoxを呼び出しています。

WSAAddressToStringを呼び出して文字列化したIPアドレスを取得するには、 SOCKADDR構造体が必要になります。 これは、getaddrinfoで取得できるADDRINFO構造体に含まれています。

ZeroMemory(&addrHints, sizeof(addrinfo));
addrHints.ai_family = AF_INET;

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

getaddrinfoは、第1引数から第3引数までに指定された情報を基にADDRINFO構造体を作成し、 それを第4引数に返します。 第1引数にホスト名を指定すると、そのホストの情報がADDRINFO構造体のSOCKADDR構造体に格納されます。 第3引数はNULLを指定することもできますが、AF_INETを指定しておくことで、 IPアドレスをIPv4形式で取得することができます。 IPv6形式で取得したい場合は、AF_INET6を指定します。 ちなみに、szHostNameの代わりに任意のホスト名("microsoft.com"など)を指定した場合、 当然ながらそのホスト情報を取得することができます。 getaddrinfoの詳細については、後の節で説明します。

SOCKADDR構造体を取得できれば、WSAAddressToStringで文字列化したIPアドレスを取得することができます。

dwSize = sizeof(szIpAddress);
WSAAddressToString(lpAddrInfo->ai_addr, (DWORD)lpAddrInfo->ai_addrlen, NULL, szIpAddress, &dwSize);

SOCKADDR構造体は、ADDRINFO構造体のai_addrメンバに相当するため、 これをWSAAddressToStringの第1引数に指定します。 また、構造体のサイズはai_addrlenから参照することができます。 WSAAddressToString自体が、IPアドレスを取得する関数ではないことに注意してください。 IPアドレス自体は既にSOCKADDR構造体に格納されており、 これを文字列に変換するのがWSAAddressToStringの役割です。

IPアドレスの種類

使用するネットワークを明確に区別するという観点などから、 ホストに割り当てられているIPアドレスは複数存在することがあります。 これは、サーバーの開発時において特に重要な点といえるでしょう。 たとえば、サーバーがインターネット上のホストからアクセスを想定しているのであれば、 サーバーが使用するIPアドレスはインターネット上で一意に識別できるものでなければなりません。 逆に、サーバーがLANで構成されたローカルネットワークからのアクセスを想定しているのであれば、 サーバーが使用するIPアドレスはローカルネットワーク上で一意であれば十分といえるでしょう。 つまり、ホストに複数のIPアドレスが割り当てられていれば、 目的によってそれらを使い分けることができるようになるのです。 特に、ローカルからアクセスを想定しているのにも関わらず、 使用しているIPアドレスがインターネット上のものであれば、 不特定多数のアクセスが発生して危険ですから、 IPアドレスの種類というものを理解しておく必要があります。

種類 説明
グローバルアドレス これは、インターネット上で一意に識別されるIPアドレスである。 インターネットへの接続の際に、プロバイダから割り当てられるのが一般的である。
プライベートアドレス これは、ローカルネットワーク上で一意に識別されるIPアドレスである。 明示的に設定することもできるが、DHCPサーバーを通じて自動で取得することもできる。 このどちらも満たしていない場合は、APIPAと呼ばれる仕組みによって、 169.254.0.0〜169.254.255.255の範囲の中でIPアドレスが自動で割り当てられる。
ループバックアドレス これは、何らかのネットワーク上に存在する相手と通信を行うためのIPアドレスではなく、 ローカルコンピュータ上に存在するアプリケーション同士で通信を行うためのIPアドレスである。 このIPアドレスは、127.0.0.1であり、対応するホスト名は"localhost"である。

一般にネットワーク通信と言うと、ネットワークで繋がった複数のコンピュータによる通信のことを指しますが、 ループバックアドレスを使用すればネットワークに繋がれていない1台のコンピュータでも通信を行うことができます。 インターネット上からのアクセスを想定するサーバーを開発する場合でも、 最初はループバックアドレスを使用し、 ローカルコンピュータ上でテストを行うことになるはずです。

今回のプログラムで呼び出しているgetaddrinfoは、第1引数に指定されたホスト名を基にアドレス情報を返すわけですが、 ホストに複数のIPアドレスが割り当てられている場合は一体どのアドレス情報が返されることになるのでしょうか。 答えは、ループバックアドレスを除く全てのアドレス情報です。 実は、getaddrinfoが第4引数に返すバッファには複数のADDRINFO構造体が格納されていることがあり、 ai_nextメンバから次のADDRINFO構造体を参照できるようになっています。

LPADDRINFO lp;

for (lp = lpAddrInfo; lp != NULL; lp = lp->ai_next) {
	dwSize = sizeof(szIpAddress);
	WSAAddressToString(lp->ai_addr, (DWORD)lp->ai_addrlen, NULL, szIpAddress, &dwSize);
	MessageBox(NULL, szIpAddress, TEXT("IPアドレス"), MB_OK);
}

このようなコードを実行した場合は、lpAddrInfoに格納されている全てのADDRINFO構造体にアクセスできるようになります。 おそらく、インターネットに接続していない場合は、プライベートアドレスのみが表示されると思われますが、 インターネットに接続している場合は、グローバルアドレスとプライベートアドレスが表示されることになるでしょう。 getaddrinfoの第1引数に"localhost"を指定した場合は、 ループバックアドレスが表示されます。



戻る