このページでは、C言語で select
関数を利用する例として、TCP 通信で複数ポートからの接続待ちを行う例を示していきます。
select
関数の使い方に関しては下記ページで説明していますので、select
関数をご存知ない方や、select
関数の使い方を知らない方は下記ページを先に読んでみていただくことをオススメします。
Contents
select
関数
前述のとおり、select
関数については下記ページで解説していますので、詳しくは下記ページを参照していただきたのですが、超簡単に言えば、select
関数は「複数の FD に対して “Ready になったかどうか?” を監視する関数」になります。FD はファイルディスクリプタの略になります。
例えば、select
関数を利用すれば、ソケット通信で複数のソケットに対して “受信可能になったかどうか?” を監視するようなことが可能となります。1つのソケットに対する監視は recv
等の他の関数でも実現可能ですが、複数のソケットに対する監視に関しては select
関数の利用が必要となります。
TCP 通信における select
関数
また、先ほど紹介したページでは、特に UDP 通信を行う際の select
関数の使い方について説明しています。それに対し、このページでは、”TCP 通信” を行う際の select
関数の使い方について説明していきます。select
関数の使い方自体に関しては UDP 通信でも TCP 通信でも同じなのですが、この2つの通信では、select
関数を実行するタイミングが異なります。なぜ、実行するタイミングが異なるのか?この辺りを、これらの通信の特徴を踏まえながら説明していきたいと思います。
スポンサーリンク
UDP 通信では recv
の前で select
関数を利用する
まず、UDP 通信の場合は、複数のソケットを利用しているプログラムで recv
関数によってデータの受信待ちを行う直前に select
関数を利用することが多いです。
これは、複数のソケットで受信待ちを行っており、どのソケットに対してデータの送信が行われてくるかが分からないからです。基本的に、recv
関数では特定の1つのソケットに対しての受信待ちを行うことになります。なので、次にデータが送信されてくる特定のソケットに対して recv
関数を実行しておく必要があります。ですが、1つのプログラムで複数のソケットを利用しており、 次にどのソケットに対してデータが送信されてくるかが分からないような場合は、どのソケットに対して recv
を実行すれば良いかが定まらないことになります。
なので、データが送信されてくる可能性のある全ソケットに対して監視を行う必要があります。そのため、複数のソケットを監視するために select
関数を利用することになります。select
関数では複数の FD (ソケット含) の読み込み (受信)・書き込み (送信)・例外に対する監視を行うことが可能です。
そして、Ready になったソケットがあれば select
関数は終了しますので、select
関数終了後に、その Ready になったソケットに対して recv
関数を実行すれば、確実に “データが送信されてきたソケット” に対してデータの受信を行うことが出来るようになります。
要は、select
関数は、複数の FD のうち、どの FD にデータが書き込まれるかが分からない、すなわち、どの FD が読み込みに対して Ready になるかが分からない時に利用することで効果を発揮する関数ということです(説明の都合上、読み込みに限定していますが、書き込みや例外に関しても同様のことが言えます)。
もちろんタイムアウトを実現するために利用する場合は、次に Ready になる FD が分かりきっていても使うこともありますが、それ以外は基本的に、どの FD が Ready になるか分からない時に select
関数を利用することが多いと思います。
TCP 通信では accept
の前で select
関数を利用する
こういった背景があるため、TCP 通信の場合は、UDP 通信とは違って select
関数は recv
関数ではなく、accept
関数を実行する直前に利用することが多いです。つまり、TCP 通信と UDP 通信の場合とで select
関数を実行するタイミングが異なります。実行するタイミングが異なるのは、TCP 通信はデータの送受信を行う前に接続を確立するプロトコルであるからになります。
UDP 通信と TCP 通信との違いに関しては下記ページで解説していますので詳しくは下記ページを参照してみていただければと思いますが、 これらの決定的な違いはデータの送受信を行う前に接続を確立しているか否かになります。TCP 通信の場合は、データの送受信を行う前に接続を確立している必要があります。
【C言語】UDP通信を行うそして、TCP 通信の場合、接続を確立したソケットに対してデータの送受信を行うことになります。したがって、TCP 通信の場合はデータの送受信を行う段階では既にデータが送信されてくるソケットが確定していることになります。なので、データの受信を行う recv
関数を実行する前に select
関数を実行してもあまり意味がないということになります。
ですが、接続を確立するソケットに関しては、実際に接続の要求を受けないと、どのソケットに対して接続が要求されているかが分かりません。そして、接続の要求を受け付けるのは、クライアントとサーバーの関係におけるサーバーが実行する accept
関数になります。
この accept
関数では、接続の要求を受け付けるソケットを指定する必要がありますが、どのソケットに接続要求が送信されてくるかが分かりません。なので、サーバーが複数のソケットで接続の要求を受け付けるのであれば、“どのソケットに接続要求が送信されてくるかが分からない accept
関数を実行する直前のタイミング” で select
関数を実行し、複数のソケットに対して Ready になったかどうかを監視してやれば良いことになります。
そして、この場合に監視するのはソケットに対する読み込みになります。C言語のソケット関数において、サーバーに対して接続要求を送信するのは connect
関数になります。そして、クライアントから connect
関数が実行されると「接続したい」ということを伝えるデータがサーバーに対して送信されることになります。つまり、「接続したい」ということを伝えるデータが特定のソケットに対して書き込みされることになります。
したがって、クライアントから connect
関数が実行される前にサーバーで select
関数を実行してソケットに対して読み込みの監視を行っていれば、クライアントから connect
関数が実行されてデータが送信されてくると、その接続を要求されたソケットが読み込みに対して Ready になります。
なので、その Ready になったソケットに対して accept
を実行すれば、確実に接続要求を送信してきた相手と接続確立することができます。そして、その後は通常通りにデータの送受信を行ってやれば良いこだけです。
ということで、TCP 通信の場合、select
関数を実行するのはサーバー側で、その実行タイミングは accept
関数の実行前というケースが多いと思います。そして、select
関数では読み込みに対して監視を行いますので、readfds
引数に監視対象の全てのソケットの FD を追加した FD の集合を指定して select
関数を実行してやれば良いことになります。
この FD の集合や readfds
引数に関しては下記ページで詳細を解説していますので、詳しくは下記ページをご参照いただければと思います。
また、ここでの解説の中で登場した accept
関数や connect
関数、さらには TCP 通信におけるこれらの関数の実行シーケンス等に関しては下記ページで解説していますので、これらについて詳しく知りたい方は別途下記ページをご参照いただければと思います。
複数ポートからの接続受付を行うプログラム
最後に、複数ポートからの接続受付を行うプログラムのサンプルソースコードと、その使い方を説明してい置きます。
ソケット通信においては、ソケットとポートが関連付けられ、そのソケットに対して accept
関数を実行すれば、その関連付けられたポートで接続受付の待ちが行われることになります。なので、ここまで説明してきたように、accept
関数を実行するタイミングの前に複数のソケットに対して select
関数を利用し、読み込みに対して監視を行うことで、実質的に複数のポートでの接続受付が行われることになります。
ここでは、これを行うプログラムのソースコードの実例を示していきます。
スポンサーリンク
サーバー側のプログラム
まず、サーバー側のプログラムのサンプルソースコードである server.c
は下記のようなものになります。
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
/* 監視対象のFDを用意する*/
int fd1 = socket(AF_INET, SOCK_STREAM, 0);
int fd2 = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr1, addr2;
/* fd1が受信待ちするポートを50001に設定 */
memset(&addr1, 0, sizeof(struct sockaddr_in));
addr1.sin_family = AF_INET;
addr1.sin_port = htons(50001);
addr1.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(fd1, (const struct sockaddr *)&addr1, sizeof(addr1));
/* fd2が受信待ちするポートを50002に設定 */
memset(&addr2, 0, sizeof(struct sockaddr_in));
addr2.sin_family = AF_INET;
addr2.sin_port = htons(50002);
addr2.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(fd2, (const struct sockaddr *)&addr2, sizeof(addr2));
listen(fd1, 5);
listen(fd2, 5);
while (1) {
/* FDの集合を用意する */
fd_set read_fd_set;
FD_ZERO(&read_fd_set);
FD_SET(fd1, &read_fd_set);
FD_SET(fd2, &read_fd_set);
/* 監視対象とする FD の最大値を求める */
int max_fd;
if (fd1 < fd2) {
max_fd = fd2;
} else {
max_fd = fd1;
}
/* 用意したデータを引数に指定してselect関数を実行する */
int ret = select(max_fd + 1, &read_fd_set, NULL, NULL, NULL);
/* 返却値を確認する */
if (ret == 0) {
printf("タイムアウトが発生しました\n");
return 0;
} else if (ret == -1) {
printf("エラーが発生しました\n");
return -1;
} else {
printf("いずれかのFDがReadyになりました\n");
}
/* Ready になった FD に読み込みや書き込みを実行する */
unsigned char buf[256];
if (FD_ISSET(fd1, &read_fd_set)) {
printf("fd1がReadyになりました\n");
/* fd1で接続を受け付け */
int c_sock = accept(fd1, NULL, NULL);
/* 接続を確立した相手からデータを受信 */
recv(c_sock, buf, sizeof(buf), 0);
printf("%s\n", buf);
close(c_sock);
}
if (FD_ISSET(fd2, &read_fd_set)) {
printf("fd2がReadyになりました\n");
/* fd2で接続を受け付け */
int c_sock = accept(fd2, NULL, NULL);
/* 接続を確立した相手からデータを受信 */
recv(c_sock, buf, sizeof(buf), 0);
printf("%s\n", buf);
close(c_sock);
}
}
/* 用意した FD をクローズする */
close(fd1);
close(fd2);
}
このプログラムでは、2つのソケットの生成を行った後、それらのソケットの FD に対し、読み込み可能になったかどうかの監視を select
関数で監視するようになっています。これらのソケットの FD はそれぞれ fd1
と fd2
の変数で管理されており、fd1
のソケットがポート 50001
、fd2
のソケットがポート 50002
に bind
関数によって関連付けられており、これらに対して接続要求が送信されてくると select
関数が終了するようになっています。
そして、select
関数が終了すれば、接続要求が送信されてきた FD、すなわち Ready となった FD のソケットに対して accept
を実行して接続を受け付けて接続要求を送信してきた相手との接続を確立しています。その後は、接続を確立した相手とデータの送受信を行えば良いだけで、上記の例ではデータの受信のみを recv
関数で実行するようにしています。
下記ページでも解説しているように、監視対象とする FD を全て “FD の集合” に追加したり、select
関数終了後に、Ready となった FD を調べたりする必要がある点が select
関数の使い方のポイントになると思います。このあたりの詳細に関しては、最初にも紹介した下記ページをご参照いただければと思います。
また、ここで示したソースコードではタイムアウトを設定していませんが、select
関数では FD が Ready になったかどうかを監視する時間にタイムアウトを設定することも可能です。これに関しても上記のページで説明していますので、詳しく知りたい方は上記のページをご参照いただければと思います。
クライアント側のプログラム
続いて、クライアント側のプログラムのソースコード例を紹介します。下記の client.c
が、そのクライアント側のプログラムのソースコードになります。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
int x;
char message[256];
struct sockaddr_in addr;
printf("書き込み先のFDを決めるために 1 or 2 を入力してください:");
scanf("%d", &x);
if (x == 1) {
/* 送信先のポートを50001に設定 */
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(50001);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
sprintf(message, "Write data to fd1 (50001)");
} else if (x == 2) {
/* 送信先のポートを50002に設定 */
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(50002);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
sprintf(message, "Write data to fd2 (50002)");
}
/* 接続要求を送信 */
connect(fd, (struct sockaddr *)&addr, sizeof(addr));
/* 接続後に、接続した相手にデータを送信 */
send(fd, message, strlen(message) + 1, 0);
close(fd);
}
この client.c
は、server.c
のプログラムで select
関数によって監視されている FD に対して connect
関数で接続要求を送信することを役割としたプログラムのソースコードになります。
この接続要求の送信により、この接続要求を受信したソケットの FD が Ready 状態となり、server.c
のプログラムで実行されている select
関数が終了することになります。そして、その後に server.c
のプログラムで accept
関数が実行されて接続が確立され、その後にデータの送受信が行われるようになっています。
server.c
で管理される FD を Ready 状態にするプログラムが存在しなければ、server.c
で実行されている select
関数は、タイムアウトも設定されていないため永遠に終了しないことになります。そして、その FD を Ready 状態にするプログラムのソースコードの例が、上記の client.c
となります。
また、server.c
では2つの FD に対して select
関数による監視が行われるため、client.c
のプログラムからは、その2つのどちらか一方の FD のソケットに対して接続要求が行われるようになっています。そして、どちらのソケットに対して接続要求を行うのかについては、プログラム実行直後に実行される scanf
により、ユーザーによって指定できるようにしています。
具体的には、プログラム実行直後に 1
を入力すれば server.c
のプログラムが管理する fd1
のソケットに対して接続要求が送信されます。もっと具体的に言うと、fd1
に関連付けられたポート 50001
に対して接続要求が送信されることになります。そして、これにより server.c
の fd1
が Ready となります。同様に、2
を入力すると server.c
における fd2
が Ready となることになります。
プログラムの実行手順
次は、先ほど示した2つのソースコードのプログラムを実行する手順について説明していきます。
まず、先ほど紹介した2つのソースコードを、1つ目を server.c
、2つ目を client.c
という名前で同じフォルダに保存してください。
続いて、ターミナルを2つ開き、それぞれのターミナルで、先ほどソースコードをファイル保存した先のフォルダに移動します。
移動後、一方のターミナルで下記の2つのコマンドを実行します。1つ目のコマンドの実行により、server.c
がコンパイルされて server.exe
が生成され、2つ目のコマンドの実行により client.c
がコンパイルされて client.exe
が生成されます。
gcc server.c -o server.exe
gcc clientc. -o client.exe
次は、生成された .exe
ファイルを実行していきます。
最初に一方のターミナルで下記コマンドを実行して server.exe
を実行します。
./server.exe
server.exe
を実行したら、次は他方のターミナルで下記コマンドを実行して client.exe
を実行します。
./client.exe 書き込み先のFDを決めるために 1 or 2 を入力してください:
次は、client.exe
を起動しているターミナルで 1
を入力してエンターキーを押してみましょう。エンターキーを押せば、client.exe
はポート 50001
に対してデータを送信し、その後にプログラムを終了するようになっています。
続いて server.exe
を起動しているターミナルを確認してみましょう。こちらのターミナルには下記のような出力が行われると思います。server.c
のソースコードより、1行目からは、いずれかの FD が Ready になったこと(つまり select
関数が 0
or 1
を返却して終了したこと)、2行目では Ready になったのが fd1
であることが読み取れます。また、3行目では、client.c
から受信したデータが出力されています。
いずれかのFDがReadyになりました fd1がReadyになりました Write data to fd1 (50001)
この表示が行われるのは、client.exe
からポート 50001
対して接続要求のデータの送信が行われたため、50001
に関連付けられている fd1
のソケットが読み込み可能となって select
関数が終了し、その後、読み込み可能になった FD に対して accept
関数が実行されて client.exe
のソケットと server.exe
の fd1
のソケットの間の接続が確立され、さらに接続が確立したソケット間でデータの送受信が行われたからになります(server.exe
は client.exe
から "Write data to fd1 (50001)"
というデータを受信し、それを printf
で出力している)。
同様に、先ほど client.exe
を起動したターミナルで再度 client.exe
を起動し、2
を入力してエンターキーを押してみましょう。これにより、client.exe
からポート 50002
に対してデータの送信が行われることになります。server.exe
を起動しているターミナルへの表示内容を確認すれば、今度は fd2
が Ready になったことが確認できると思います。
いずれかのFDがReadyになりました fd2がReadyになりました Write data to fd2 (50002)
こんな感じで、select
関数を利用することで複数のポートを監視することができ、これにより1つのプログラムから複数のポートからの接続要求の受付を行うことが可能となります。
スポンサーリンク
まとめ
このページでは、C言語での select
関数の利用例、特に TCP 通信での複数ポートからの接続待ちの行い方について説明しました!
select
関数が効果的に利用できるのは、複数の FD のうち、どの FD が Ready になるかが分からない、もしくは動的に決まるようなケースとなります。特に TCP 通信の場合は、複数のソケットで接続要求の受付を行う場合に、どのソケットに対して接続要求が送信されてくるかが分からないため、接続要求の受付を行う accept
関数を実行する前に select
関数を利用することが多いです。
select
関数は使い方は結構難しいですが、使いこなせるようになると開発できるプログラムの幅が広がり、よりプログラミングを楽しめるようになると思います。是非 select
関数の特徴や使い方は理解しておくようにしましょう!
また、接続要求やデータの受信だけでなく、標準入力への入力の監視を select
関数で行うこともでき、これにより標準入力への入力にタイムアウトを設定するようなこともできます。これに関しては下記ページで解説していますので、もっと select
関数について詳しくなりたい方は是非読んでみてください!