selectを使う

ここでは、selectを使って複数のソケットからデータを受け取る方法を説明したいと思います。

select

普通の状態では、recvやrecvfromはデータが受信できるまでブロッキングします。 ソケットを一つしか利用していない場合にはブロッキングは非常に便利なのですが、ソケットが複数になると困ってしまいます。 複数のソケットを扱うとき、片方のソケットでブロッキングしたままになってしまうと他のソケットにデータが到着しても受信が出来なくなってしまいます。 そのため、複数のソケットを扱っていると、どのソケットからデータが受信可能か知りたくなります。

ブロッキングとは、関数が返ってこない事を表します。 例えば、recvはデータを受信して関数が戻ってきます。 言い方を変えると、データを受信するまでブロックしています。 recvやrecvfromをブロッキングしないノンブロッキング方式で使う事も可能ですが、ここではブロッキング方式のまま使う方法を説明します。

そのような機能を提供するのがselectです。 selectを使うと、データが受信可能なソケットでのみrecvやrecvfromを実行することができます。 selectは登録したソケットをブロッキング状態で監視し、どれかがデータ受信するとブロッキング状態を解除します。

selectを使うサンプルコード

selectは受信可能かどうかだけではなく、送信可能かどうか、エラーがあるかなどもチェックできますがここでは受信可能かどうかをチェックする方法のみをとりあげます。

下記サンプルコードでは、UDPソケットを2つ作成しています。 作成した2つのUDPソケットは、selectに登録してselectはブロッキング状態に入ります。 UDPソケットにデータが到着するとrecvを行い、受信した内容を表示します。


#include <stdio.h>
#include <winsock2.h>

int
main()
{
 SOCKET sock1, sock2;
 struct sockaddr_in addr1, addr2;
 fd_set fds, readfds;
 char buf[2048];
 WSADATA wsaData;

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

 // 受信ソケットを2つ作ります
 sock1 = socket(AF_INET, SOCK_DGRAM, 0);
 sock2 = socket(AF_INET, SOCK_DGRAM, 0);

 addr1.sin_family = AF_INET;
 addr2.sin_family = AF_INET;

 addr1.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
 addr2.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

 // 2つの別々のポートで待つために別のポート番号をそれぞれ設定します
 addr1.sin_port = htons(11111);
 addr2.sin_port = htons(22222);

 // 2つの別々のポートで待つようにbindします
 bind(sock1, (struct sockaddr *)&addr1, sizeof(addr1));
 bind(sock2, (struct sockaddr *)&addr2, sizeof(addr2));

 // fd_setの初期化します
 FD_ZERO(&readfds);

 // selectで待つ読み込みソケットとしてsock1を登録します
 FD_SET(sock1, &readfds);
 // selectで待つ読み込みソケットとしてsock2を登録します
 FD_SET(sock2, &readfds);

 // 無限ループです
 // このサンプルでは、この無限ループを抜けません
 while (1) {
	// 読み込み用fd_setの初期化
	// selectが毎回内容を上書きしてしまうので、毎回初期化します
	memcpy(&fds, &readfds, sizeof(fd_set));

	// fdsに設定されたソケットが読み込み可能になるまで待ちます
	select(0, &fds, NULL, NULL, NULL);

	// sock1に読み込み可能データがある場合
	if (FD_ISSET(sock1, &fds)) {
		// sock1からデータを受信して表示します
		memset(buf, 0, sizeof(buf));
		recv(sock1, buf, sizeof(buf), 0);
		printf("%s\n", buf);
	}

	// sock2に読み込み可能データがある場合
	if (FD_ISSET(sock2, &fds)) {
		// sock2からデータを受信して表示します
		memset(buf, 0, sizeof(buf));
		recv(sock2, buf, sizeof(buf), 0);
		printf("%s\n", buf);
	}
 }

 // このサンプルでは、ここへは到達しません
 closesocket(sock1);
 closesocket(sock2);

 WSACleanup();

 return 0;
}

selectを使うサンプルコードにデータを送信するコード

selectのサンプルコードは、UDPの11111番と22222番のポートでデータを待っています。 UDPを使って11111番と22222番ポートにデータを送信するサンプルを以下に示します。


#include <stdio.h>
#include <winsock2.h>

int
main()
{
 SOCKET sock;
 struct sockaddr_in dest1, dest2;
 char buf[1024];
 WSADATA wsaData;

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

 sock = socket(AF_INET, SOCK_DGRAM, 0);

 dest1.sin_family = AF_INET;
 dest2.sin_family = AF_INET;

 dest1.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
 dest2.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

 dest1.sin_port = htons(11111);
 dest2.sin_port = htons(22222);

 memset(buf, 0, sizeof(buf));
 _snprintf(buf, sizeof(buf), "data to port 11111");
 sendto(sock,
	buf, strlen(buf), 0, (struct sockaddr *)&dest1, sizeof(dest1));

 memset(buf, 0, sizeof(buf));
 _snprintf(buf, sizeof(buf), "data to port 22222");
 sendto(sock,
	buf, strlen(buf), 0, (struct sockaddr *)&dest2, sizeof(dest2));

 closesocket(sock);

 WSACleanup();

 return 0;
}

最後に

ここでは、UDPを使ったサンプルを示しましたが、TCPを使っても同様の事が出来ます。 TCPの場合は、acceptした後のソケットだけではなく、listenしているソケットに対するselectも可能です。 また、winsockにはWindowsイベント方式でイベントが飛んでくるselectもあります。

IPv6基礎検定

YouTubeチャンネルやってます!