selectを使う

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

select

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

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

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

selectを使うサンプルコード

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

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


#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

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

 /* ファイルディスクリプタの最大値を計算します。selectで使います */
 int maxfd;

 /* 受信ソケットを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_addr = inet_addr("127.0.0.1");
 addr2.sin_addr.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);

 /* selectで監視するファイルディスクリプタの最大値を計算します */
 if (sock1 > sock2) {
   maxfd = sock1;
 } else {
   maxfd = sock2;
 }

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

	/* fdsに設定されたソケットが読み込み可能になるまで待ちます */
        /* 一つ目の引数はファイルディスクリプタの最大値+1にします */
	select(maxfd+1, &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);
	}
 }

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

 return 0;
}

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

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


#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

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

 sock = socket(AF_INET, SOCK_DGRAM, 0);

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

 dest1.sin_addr.s_addr = inet_addr("127.0.0.1");
 dest2.sin_addr.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));

 close(sock);

 return 0;
}

最後に

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

IPv6基礎検定

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