このページでは、C言語における select
関数について説明していきます。
select
関数は非常に便利な関数で、これを使いこなすことで自身で開発できるプログラムの幅が一気に広がります。ですが、その反面、使い方が難しい関数でもあります。出来るだけ多くの方に select
関数の使い方を理解していただけるよう、詳しく&分かりやすく解説していきますので、select
関数に興味のある方は是非このページを読み進めていただければと思います。
また、このページでは Linux や MaxOS で select
関数を利用することを前提として解説していきますが、Windows にも select
関数が存在しますし、同様の使い方で利用できるはずです。ただ、インクルードするファイルなどが異なるため、その点は注意していただければと思います。とりあえず、select
関数を使うときのポイントなどは Windows ユーザーの方でも参考になると思います。
select
関数
まずは、select
関数自体について解説していきます。
select
は複数の FD を監視する関数
この select
は複数のファイルディスクリプタを監視する関数となります。
FD とは
ファイルディスクリプタとは、プログラムが使用するファイルやソケットなどを識別する識別子となります。以降、ファイルディスクリプタのことを FD と略します。
例えば、下記ページでも解説しているように、ソケット通信を行うために必要となるソケットは socket
関数で作成可能です。
そして、socket
関数の返却値は、作成されたソケットの FD となります。この FD の型は int
で、つまりは FD の実体は単なる整数となります。なんですが、この FD により、1つのプログラムの複数ソケットを作成したような場合でもプログラム内で各ソケットを識別することが可能となります。
例えば、ソケット関連の関数では引数として FD が指定可能なものが多いです。これは、プログラムの開発者が、どのソケットを利用してデータの送信やデータの受信を行うのかを指定できるようにするためです。
特定の FD に対してデータの読み込み / 書き込みが行われる流れ
ここで、もう少し詳しく、特定の FD に対してデータの読み込み(受信も含む)やデータの書き込み(送信も含む)が行われる流れについて説明したいと思います。ここでは例として recv
関数で説明していきますが、他の関数においても、特定の FD に対して読み込みや書き込みが行われる基本的な流れは同様になると思います。
recv
関数では、第1引数に特定のポートに関連付けられたソケットの FD を指定するようになっています(この関連付けは bind
関数により行われます)。そして、recv
関数を実行すると、まず第1引数で指定した FD のソケットが読み込み可能な状態になるまで FD が監視されることになります(その FD に関連づけられたポートに対してデータが送信されてくると読み込み可能状態になる)。この監視されている間、関数実行側は待ち状態となります。そして、読み込み可能な状態になった際に、recv
関数がデータを読み込んで recv
関数を終了するようになっています。
逆に、そのソケットが読み込み可能な状態にならなければ、recv
関数はずっと待ちの状態になります。
タイムアウトや非同期設定を行うことも可能ですが、このページでは、これらの設定を行わない前提で解説をしていきます
要は、recv
関数は第1引数で指定された FD を監視し、読み込み可能状態になるのを待ち続けます。より具体的には、相手からデータが送信されてくるまで監視を続けることになります。
ここで重要なのは、特定の FD に対する読み込みや書き込みは2段階の処理で実現されると言う点になります。1つ目が、FD が Ready (読み込み可能 / 書き込み可能) になるまでの監視で、2つ目が Ready になった時に実際に行われる FD に対する読み込み / 書き込みになります。
また、ここでは recv
関数の例で説明を行いましたが、ファイルや標準入力・標準出力などに対する読み込みや書き込みも同様に、FD の監視と FD に対する読み込み / 書き込みで実現されています。例えば、scanf
関数を実行すると特定の FD に対して監視が行われます。この FD は標準入力となります。そして、ユーザー等が標準入力に文字列等を入力してエンターキーを押した際に標準入力が Ready となり、その後、標準入力から実際にデータの読み込みが行われる事になります。
複数の FD の監視
そして、ここまで登場してきたような recv
関数や scanf
関数では同時に1つの FD しか監視が行われません。これらだけでなく、C言語で利用可能な “FD に対して操作を行う関数” のほとんどが、同時に1つの FD しか監視できません。基本的に、プログラムで同時に実行可能な関数は1つのみなので、プログラムで同時に監視可能な FD は1つのみという事になります。
ただ、”基本的に” と言っているように、工夫すれば複数の FD を同時に監視することも可能です。例えば、下記ページで解説しているマルチスレッドを利用すればプログラム内で複数の関数を同時に実行することが可能となりますので、同時に複数の FD を監視することが可能となります。マルチプロセスでも同様のことが行えます。
入門者向け!C言語でのマルチスレッドをわかりやすく解説複数の FD を同時に監視する他の方法が、このページで解説している select
関数の利用になります。基本的には、select
関数は、ここまで説明してきたような “FD が Ready になることを監視する” のみの関数となります。Ready になることを監視することは scanf
や recv
関数等でも行えるのですが、前述の通り、これらが監視できるのは特定の1つの FD のみです。
それに対し、select
関数では複数の FD を同時に監視することが可能です。そのため、この関数を利用することで、マルチスレッドやマルチプロセスのような難しいことを行わなくても複数の FD の同時監視を実現することが出来ます。
使い方に関しては後述で詳しく解説しますが、select
関数では引数で指定された FD の集合に含まれるいずれかの FD が Ready 状態になる or タイムアウトが発生するまで集合に含まれる全ての FD の監視が行われます。
したがって、タイムアウトが発生せずに select
関数が終了した際には、いずれかの FD が Ready 状態になっていることになりますので、その FD に対して読み込みや書き込みを実行すれば、待つことなく読み込みや書き込みが実行することが行えることになります。
このような、複数の FD の同時監視はソケット通信でよく利用されます。1つのプログラムで複数のソケットを作成して別々のポートで受信待ちを行いたい場合は、select
関数で複数のソケット (FD) を監視する事になります。そして、select
関数が終了したタイミングで、Ready になったソケットからデータの読み込み (データの受信) を行うようなプログラムを作ることが多いです。
select 関数の使い方 では、この複数のソケットを監視する例も用いながら select
関数の使い方について解説していきたいと思います。
スポンサーリンク
select
はタイムアウトの設定が可能な関数
また、select
は複数の FD の同時監視だけでなく、タイムアウトを実現するために利用されることも多いです。
例えば scanf
関数は、一度実行すると標準入力への入力が行われない限り、つまり標準入力が Ready にならない限り永遠に待ち続ける関数になります。要は、ずっと標準入力の監視が行われている状態となります。
こんな標準入力への入力のような、通常ではタイムアウトが設定不可な読み込みや書き込みに対してタイムアウトを設けるためにも select
関数が利用されることがあります。
select
関数では引数 timeout
によりタイムアウト時間を設定することが出来ます。タイムアウト時間が設定されていると、select
関数で監視を始めてからタイムアウト時間が経過しても FD が Ready にならない場合に関数が終了するようになっています。したがって、例えば、scanf
関数実行前にタイムアウト時間を設定した select
関数で標準入力の監視を行い、タイムアウト時間経過しても入力が無かった際には scanf
関数を実行しないようにしてやれば、標準入力への入力待ちにタイムアウトを設けることが出来ることになります。
ということで、select
関数は複数の FD の同時監視だけでなく、読み込みや書き込みに対してタイムアウトを設定したいような場合にも利用することがあります。
実際に、select
関数を利用して標準入力からの入力受付にタイムアウトを設定する手順を下記ページで解説していますので、興味があれば読んでみてください。「時間制限付き4択クイズ」の作り方なども説明しています。
select
関数の使い方
次は、select
関数の使い方について説明していきたいと思います。
まず、select
関数は下記のように定義された関数となります。使用するためには sys/select.h
をインクルードしておく必要があります。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
各引数の意味合いは下記のようになります。
引数名 | 説明 |
nfds |
監視する FD の最大値 + 1 |
readfds |
読み込みを監視する FD の集合(のアドレス) |
writefds |
書き込みを監視する FD の集合(のアドレス) |
exceptfds |
例外発生を監視する FD の集合(のアドレス) |
timeout |
タイムアウト時間 |
この select
関数を使いこなす上で重要になるのが “FD の集合” となります。select
関数における3つの引数 readfds
、writefds
、exceptfds
には FD の集合(のアドレス)を指定する必要があります。これらは目的別に集合の指定を行う引数であり、readfds
には読み込みに対する監視、writefds
には書き込みに対する監視、exceptfds
には例外に対する監視を行いたい FD の集合(のアドレス)を指定する必要があります。
これらを指定して select
関数を実行すれば、各目的別の FD の集合に含まれるいずれかの FD が Ready になるまで、より具体的には、readfds
に含まれるいずれかの FD が読み込み可能になるまで or writefds
に含まれるいずれかの FD が書き込み可能になるまで or exceptfds
に含まれるいずれかの FD で例外が発生するまで監視が行われ、関数が待ち状態になります。
そして、いずれかの FD が Ready 状態になったタイミングで select
関数が終了します。これらの FD の集合に含まれない FD は監視対象になりません。また、目的とは異なるイベントは監視されません。例えば、readfds
の集合に含まれる FD が書き込み可能になったとしても、select
関数は監視状態のままとなります。
ということで、select
関数を使いこなすためには、これらの FD の集合の引数への指定が重要となります。ここからは、この FD の集合の用意の仕方も含めて、select
関数を使う手順を説明していきます。
ちなみに、今回は複数のソケットでの UDP 通信を行う際に各ソケットのデータの読み込み(データの受信)を監視するプログラムの例で select
関数の使い方を示していきます。UDP 通信について詳しく知りたい方がおられましたら、是非下記ページを読んでみてください。
また、ソケット通信自体に関しては下記ページで解説していますので、こちらも興味があったり例の中で使用している各関数の意味合いが理解できないような場合は読んでみていただければと思います。
【C言語】ソケット通信について解説監視対象とする FD を用意する
まずは、監視対象とする FD を用意する必要があります。
ソケットの場合であれば、socket
関数を実行すればソケットが生成され、その FD が返却値として得られます。また、プログラムには自動的に用意されている FD もあります。具体的には、標準入力 stdin
、標準出力 stdout
、標準エラー出力の stderr
は作成しなくても各プログラム(プロセス)に用意されている FD であり、これらは stdio.h
をインクルードすれば利用可能となります。
例えば、2つのソケットを監視対象とするのであれば、下記のような処理を行えば変数 fd1
と fd2
のそれぞれでソケットの FD を管理できることになります(この章ではインクルードやエラーハンドリングなどを省略したコードを紹介していきます)。
int fd1 = socket(AF_INET, SOCK_DGRAM, 0);
int fd2 = socket(AF_INET, SOCK_DGRAM, 0);
また、今回は2つのソケットでのデータの読み込み(データの受信)を監視する例を示すため、受信待ちを行う IP アドレスやポート番号を設定しておきたいと思います。これらは、下記のように bind
関数を実行することで設定可能です。
struct sockaddr_in addr1, addr2;
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));
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));
上記の処理を行うことで、fd1
はポート 50001
と関連付けられ、fd2
はポート 50002
と関連付けられることになります。そして、これらの FD を第1引数に指定して recv
関数を実行すれば、その第1引数に指定された FD が Ready (読み込み可能状態) になったかどうかの監視が行われるようになります。読み込み可能状態になれば、実際にデータの読み込みが行われて recv
関数は終了します。
そして、ソケットの場合、各 FD は関連付けられたポートに対してデータの送信が行われてきたときに Ready (読み込み可能状態) となるようになっています。したがって、他のプログラム等からポート 50001
にデータが送信されて来れば fd1
が Ready となり、ポート 50002
にデータが送信されて来れば fd2
が Ready となることになります。
要は、ソケットでのデータの受信の場合、関連付けられたポート番号にデータが送信されてくると Ready (読み込み可能状態) になることになります。
で、前述の通り、通常プログラムでは recv
関数を複数同時に実行することはできず、fd1
と fd2
を同時に監視することはできませんが、これらの FD を追加した集合を readfds
引数に指定して select
関数を実行することで fd1
と fd2
の両方を同時に読み込みに対して監視することができるようになります。
スポンサーリンク
FD の集合を用意する
監視対象の FD が用意できれば、次は FD の集合の用意を行います。今回は、2つのソケットのデータの読み込み(データの受信)を監視するため、select
関数における readfds
引数に指定する FD の集合を用意していきます。
まず、sys/select.h
をインクルードすれば fd_set
という型が利用可能になり、これが “FD の集合” を管理する型となります。
そして、fd_set
の変数が FD の集合となり、これに監視対象となる FD を追加していくことになります。
また、FD の集合への操作は、”FD の集合への操作用のマクロ” を利用して行うことになります。このマクロに関しても、sys/select.h
をインクルードすることで利用可能となります。
FD_ZERO
で FD の集合を空にする
FD の集合を利用する場合、まずは FD の集合を空にする必要があります。
FD の集合も、他の変数同様に、単に変数宣言をするだけだと不定値が格納されていることになり、fd_set
の変数の場合は不要な FD が FD の集合に含まれていることになります。
なので、最初に FD の集合を空にする必要があります。
そして、FD の集合を空にする操作は、マクロ FD_ZERO
によって行うことができます。堕1引数に FD の集合の変数のアドレスを指定して FD_ZERO
を実行すれば、その FD の集合が空になります。
fd_set read_fd_set;
FD_ZERO(&read_fd_set);
今回は読み込みのみの監視を行うため FD の集合は1つしか用意していませんが、読み込みだけでなく、書き込みや例外を監視するのであれば、これらを監視するための FD の集合を用意し、それぞれの FD の集合に対して FD_ZERO
を実行する必要があります。今後は説明は省略しますが、以降で紹介する FD の集合に対する操作に関しても同様のことが言えます。
FD_SET
で FD の集合に FD を追加する
続いて、空にした FD の集合に、監視対象とする FD を追加します。ここで、監視対象としたい FD を全て FD の集合に追加する必要があります。
FD の集合に FD を追加するためには FD_SET
というマクロを利用します。この FD_SET
は、第1引数に監視対象とする FD を、第2引数に FD の集合のアドレスを指定して実行します。FD_SET
は1回の実行で1つの FD しか追加できないため、追加したい FD の数だけ分、FD_SET
を実行する必要があります。
今回の例では fd1
と fd2
を監視するため、下記のように2回 FD_SET
を実行することになります。これにより、read_fd_set
という FD の集合に fd1
と fd2
が追加されることになります。
FD_SET(fd1, &read_fd_set);
FD_SET(fd2, &read_fd_set);
そして、この read_fd_set
のアドレスを select
関数の readfds
引数に指定すれば、select
関数実行によって fd1
と fd2
の読み込みの監視が行われることになります。
FD_CLEAR
で FD の集合から FD を削除する
今回の例の場合は不要ではあるのですが、FD_CLEAR
を実行して FD の集合から FD を削除するようなことも可能です。
FD_CLEAR
は第1引数に監視対象とする FD を、第2引数に FD の集合のアドレスを指定して実行します。
例えば、下記を実行すれば、read_fd_set
から fd1
を削除することができます。ただ、今回の例では fd1
と fd2
を監視するため、下記の処理は不要となります。
FD_CLEAR(fd1, &read_fd_set);
監視対象とする FD の最大値を求める
FD の集合が用意できれば、これら以外の select
関数の引数に指定するデータを用意していきます。
まず、select
関数の第1引数 nfds
には、監視対象とする FD の最大値 + 1
の値を指定する必要があります。select
関数では、nfds
未満を値とする FD のみしか監視されないようになっています。そのため、nfds
には、監視対象とする FD の最大値 + 1
を指定する必要があります。また、複数の FD の集合を select
関数の引数に指定する場合、それらの FD の集合すべての中から FD の最大値を求め、それに +1
した値を nfds
に指定する必要があります。
FD は前述のとおり単なる整数ですので、複数の整数の中から最大の値のものを見つけてやり、それに +1
した値を指定してやれば良いだけなので、nfds
に指定する値は簡単に求めることが可能です。
例えば、今回の場合は fd1
と fd2
のみを監視対象とするため、fd1
と fd2
の大きい方の値が監視対象となる FD の最大値となります。そして、FD が2つの場合は単に下記のように2つの変数を比較するだけで最大値が求まります。
int max_fd;
if (fd1 < fd2) {
max_fd = fd2;
} else {
max_fd = fd1;
}
上記で求まる max_fd
は 監視対象とする FD の最大値
ですので、select
関数実行時には max_fd + 1
の値を nfds
に指定する必要があります。
また、監視対象とする FD の数が多い場合も、例えば下記ページで紹介しているような方法で簡単に最大値は求めることが可能だと思います。
【C言語】最大値と最小値を求める方法タイムアウト時間を設定する
次は timeout
引数に指定するデータの用意を行っていきます。
timeout
引数には struct timeval
型の変数のアドレスを指定する必要があります。struct timeval
は構造体となっており、この型の変数は下記の2つのメンバーを持ちます。これらの2つのメンバーに値をセットし、その変数を select
関数の timeout
引数に指定して実行することで、監視を行う時間にタイムアウトを設定することが可能となります。
メンバー名 | 説明 |
tv_sec |
タイムアウト時間(秒) |
tv_usec |
タイムアウト時間(マイクロ秒) |
要は、タイムアウト時間における秒単位の時間を tv_sec
で、マイクロ秒単位の時間を tv_usec
にセットしてやれば良いだけです。
例えば、タイムアウト時間を 60.123456
に設定するのであれば、下記のように各メンバーに値をセットしてやれば良いことになります。
struct timeval tm;
tm.tv_sec = 60;
tm.tv_usec = 123456;
スポンサーリンク
用意したデータを引数に指定して select
関数を実行する
以上で、select
関数の引数に指定するデータが全て用意できたことになるので、後はこれらを各引数に指定して select
関数を実行してやれば良いです。
int ret = select(max_fd + 1, &read_fd_set, NULL, NULL, &tm);
select
関数では、下記の4つ引数に関しては NULL
を指定することも可能です。
readfds
writefds
exceptfds
timeout
例えば、上記の select
関数の実行例であれば、writefds
と exceptfds
に NULL
を指定しているため、書き込みと例外発生に関しては監視が行われないことになります。このように、監視が不要であれば集合を用意することなく NULL
を指定しても問題ありません。また、タイムアウトが不要であれば、timeout
引数に NULL
を指定してやれば良いです。
返却値を確認する
前述でも説明したように、select
関数を実行すると、引数に指定した FD の集合に含まれる各 FD の監視が始まります。そして、監視対象の FD が Ready になると select
関数は終了することになります。
ただし、監視対象の FD が Ready にならなくても、タイムアウトが発生した場合やエラーが発生した場合も select
関数は終了することになります。何が原因で select
関数が終了したかを判断するために、select
関数終了後にはまず select
関数の返却値を確認しましょう。
select
関数の返却値は下記のようになっており、返却値から select
関数が終了した理由を知ることが出来ます。
0
:タイムアウトが発生した-1
:エラーが発生した上記以外
:監視対象の FD が Ready になった
0
でも -1
でもない値が返却されたということは、監視対象の FD のいずれかが Ready になったことを意味しますので、次の節で説明するように Ready になった FD に対して読み込みや書き込み等を実施することになります。
逆に、0
or -1
が返却されたということは、監視対象の FD はまだ Ready になっていないことになりますので、メッセージを表示したりプログラムを終了したりと、読み込みや書き込み以外の処理を実施することになります。
例えば、下記は select
関数の返却値 ret
に応じてメッセージを表示する例となります。
if (ret == 0) {
printf("タイムアウトが発生しました\n");
return 0;
} else if (ret == -1) {
printf("エラーが発生しました\n");
return -1;
} else {
printf("いずれかのFDがReadyになりました\n");
}
上記では、else
節のみプログラムや関数を終了しないようにしています。この後に Ready になった FD に対して読み込み処理を行うためです。
また、エラーが発生した場合、つまり select
関数が -1
を返却した場合は、エラーが発生した原因を errno
から調べることも可能です。errno
に関しては下記ページで解説していますので、詳しくは下記ページを参照していただければと思います。
Ready になった FD を調べて読み込みや書き込みを実行する
select
関数が 0
でも -1
でもない値を返却したということは、監視対象の FD のいずれかが Ready になったということになります。次は、その Ready になった FD を調べて、その FD に対して実際に読み込みや書き込みを実施します。
どの FD が Ready になったのかについては返却値から調べるのではなく、引数に指定した FD の集合から調べることになります。select
関数では、Ready になった FD 以外が引数に指定された FD の集合から取り除かれることになります。
したがって、select
関数実行後に、引数に指定した FD の集合の中に存在している FD が “Ready になった FD” であることになります。そして、特定の FD が FD の集合の中に存在するかどうかは FD_ISSET
というマクロで調べることができます。
select
関数が終了した後に、第1引数に FD を、第2引数に FD の集合のアドレスを指定して FD_ISSET
を実行すれば、第1引数に指定した FD が第2引数に指定した FD の集合の中に存在していれば FD_ISSET
は 1
を返却しています。逆に、存在していなければ FD_ISSET
は 0
を返却します。
そして、前述の通り、select
関数が終了した後に、引数に指定した FD の集合の中に存在している FD は Ready 状態のもののみです。したがって、FD の集合の全 FD に対して FD_ISSET
の返却値を確認すれば、Ready となった FD を調べることができます。
そして、Ready になった FD に対して実際に読み込みや書き込みを実行すれば、それらの処理が即座に実行されることになります。
例えば、read_fd_set
の FD の集合に含まれる fd1
と fd2
のうち、Ready になった FD に対して recv
関数で読み込みを実施する処理は下記のようなものになります。下記では、recv
関数で読み込んだ(受信した)文字列を printf
で出力するようにしています。
unsigned char buf[256];
if (FD_ISSET(fd1, &read_fd_set)) {
printf("fd1がReadyになりました\n");
recv(fd1, buf, sizeof(buf), 0);
printf("%s\n", buf);
}
if (FD_ISSET(fd2, &read_fd_set)) {
printf("fd2がReadyになりました\n");
recv(fd2, buf, sizeof(buf), 0);
printf("%s\n", buf);
}
ここでポイントになるのが、同時に複数の FD が Ready になっている可能性があるという点になります。上記では、そのような場合に Ready になった全ての FD に対して読み込みが実行されるよう、全ての FD に対して FD_ISSET
で Ready かどうかを調べるようにしています。そして、全ての Ready になっている FD に対して recv
を実行するようにしています。
例えば、上記で2つ目の if
を else if
にしてしまうと、fd1
が Ready の場合、fd2
も同時に Ready になっていても fd2
に対する読み込みが行われないことになってしまいます。
このようなことが起こらないように、複数の FD が Ready になっている可能性も考慮して処理を記述する必要があります。
また、今回は読み込みに関する監視しかしていないため read_fd_set
にしか FD_ISSET
を実行していませんが、他の目的で監視を行う際には、他の FD の集合に対しても FD_ISSET
を実行して Ready になった FD を調べる必要があります。
さらに、今回の例では UDP 通信におけるソケットの読み込みに対する監視を行っていたため、Ready になった FD に対して recv
関数の実行を行っていますが、監視する対象に応じて実行する関数が異なることに注意してください。例えば、標準入力に対して読み込みに対する監視を行っていた場合は、Ready になった FD に対しては scanf
関数等を実行することになります。また、TCP 通信の場合は accept
を実行するケースが多いと思います。
ここまでの説明のように、select
関数は前処理だけでなく、実行後の後処理も適切に記述しておかないと、上手く使いこなすことができないことになるので注意してください。
スポンサーリンク
用意した FD をクローズする
ここまでが、select
関数を利用する場合の主な処理の流れとなります。
ここまで説明してきた処理の流れを何回も繰り返すような場合もありますが、最後に不要になった FD はクローズしてやることは忘れないようにしましょう。
close(fd1);
close(fd2);
select
関数の使い方のまとめ
select
関数の使い方の解説は以上となります。
最後に使い方をまとめておくと、select
関数は下記のような手順を踏んで使用する必要があります。適切に引数を用意する必要がある点や、実行後にも select
関数が終了した原因を調べたり、Ready になった FD を特定する必要があったりする点が select
関数を利用する時のポイントになると思います。また、集合というC言語ではあまり使用しない型を利用する必要がある点にも注意してください。
- 監視対象とする FD を用意する
- FD の集合を用意する
- 監視対象とする FD の最大値を求める
- タイムアウト時間を設定する
- 用意したデータを引数に指定して
select
関数を実行する - 返却値を確認する
- Ready になった FD を調べて読み込みや書き込みを実行する
- 用意した FD をクローズする
select
関数で複数のポートを監視するプログラムのサンプル
ここまで断片的にソースコードを紹介してきましたが、ここまでに紹介したコードを一つにまとめたものを、select
関数で複数のポートを監視するプログラムのサンプルとして紹介しておきます。
スポンサーリンク
複数のポートを監視するプログラムのソースコード
そのサンプルのソースコードは下記となります(エラーハンドリングの多くを省略しているので注意してください)。前述のとおり、UDP 通信におけるソケットの読み込みの監視を行う例となっています。
#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_DGRAM, 0);
int fd2 = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr1, addr2;
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));
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));
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;
}
/* タイムアウト時間を設定する */
struct timeval tm;
tm.tv_sec = 60;
tm.tv_usec = 123456;
/* 用意したデータを引数に指定してselect関数を実行する */
int ret = select(max_fd + 1, &read_fd_set, NULL, NULL, &tm);
/* 返却値を確認する */
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");
recv(fd1, buf, sizeof(buf), 0);
printf("%s\n", buf);
}
if (FD_ISSET(fd2, &read_fd_set)) {
printf("fd2がReadyになりました\n");
recv(fd2, buf, sizeof(buf), 0);
printf("%s\n", buf);
}
}
/* 用意した FD をクローズする */
close(fd1);
close(fd2);
}
ただ、上記だけだと fd1
と fd2
が読み込み可能になるまで待ち続けるだけのプログラムになってしまいます。FD が読み込み可能になるためには、書き込み側のプログラムも必要です。
ということで、書き込み側のプログラムのソースコードの例(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_DGRAM, 0);
int x;
char message[256];
struct sockaddr_in addr;
printf("書き込み先のFDを決めるために 1 or 2 を入力してください:");
scanf("%d", &x);
if (x == 1) {
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)");
sendto(fd, message, strlen(message) + 1, 0, (const struct sockaddr *)&addr, sizeof(addr));
} else if (x == 2) {
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)");
sendto(fd, message, strlen(message) + 1, 0, (const struct sockaddr *)&addr, sizeof(addr));
}
close(fd);
}
プログラムの説明
上記で紹介した2つのサンプルプログラムの動作について説明しておきます。
server.c
まず、最初に紹介した方のソースコード server.c
の動作について説明します。
server.c
をコンパイルして実行すると、ソケットの生成や bind
関数が実行された後に select
関数が実行され、2つのソケットの読み込みに対する監視が行われるようになっています。そして、各ソケット fd1
と fd2
はそれぞれ、bind
関数によってポート番号 50001
とポート番号 50002
に関連付けられています。
さらに、これらの fd1
と fd2
は読み込み(受信)に対して select
関数によって監視されることになります。
そのため、他のプログラムからポート番号 50001
or 50002
にデータが送信されてくると、そのポート番号に関連付けられたソケット fd1
or fd2
が読み込み可能となり、select
関数の監視、すなわち読み込み可能になるまでの待ちが終了し、次に読み込み可能となったソケットから recv
関数によって実際にデータの受信が行われるようになっています。
前述の通り、select
関数で引数に指定した FD の集合が書き換えられてしまうため、繰り返し実行する時は select
関数を実行するたびに FD の集合への操作を再度実施する必要があるので注意してください。
上記の server.c
においては、while
ループの内側の最初に FD の集合の操作を行うことで、selec
関数実行前に必ず FD の集合に fd1
と fd2
が追加された状態になるようにしています。
client.c
client.c
は server.c
のプログラムで監視されている FD に対してデータの書き込みを行うプログラムのソースコードとなっています。より具体的には、server.c
のプログラムで監視されている FD に関連付けられているポートに対してデータの送信を行うプログラムのソースコードとなっています。
まず client.c
は実行された直後に scanf
での文字列の入力待ちをするようになっています。ここで 1
を入力してエンターキーを押せば、自身の PC(アドレス 127.0.0.1
)のポート 50001
に対してデータが送信されることになります。したがって、server.c
のプログラムが監視している fd1
(fd1
はポート 50001
に関連付けられている)が読み込み可能となって select
関数が終了し、fd1
からデータの読み込みが行われることになります。そして、再び select
関数が実行されて fd1
と fd2
が監視されることになります。
同様に、2
を入力してエンターキーを押した場合はポート 50002
に対してデータが送信され、上記と同様の仕組みで fd2
からデータの読み込みが行われ、また select
関数が実行されて fd1
と fd2
が監視されることになります。
ということで、前述で紹介した server.c
と client.c
をそれぞれコンパイルしてプログラムを生成し、server.c
のプログラムを実行した後に client.c
のプログラムを実行して 1
or 2
を入力すれば server.c
のプログラムで読み込み可能になった FD に対してデータの読み込みが行われることになります。また、server.c
では select
関数に約 60
秒のタイムアウトを設定しているため、1
or 2
の入力を約 60
秒間行わなければ server.c
のプログラムでタイムアウトが発生する様子も確認できます。
プログラムの実行手順
次は、これらの動作を実際の手順を説明しながら確認していきたいと思います。
まず、前述で紹介した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 に対してデータの読み込みが行われているからになります。
同様に、先ほど client.exe
を起動したターミナルで再度 client.exe
を起動し、2
を入力してエンターキーを押してみましょう。これにより、client.exe
からポート 50002
に対してデータの送信が行われることになります。server.exe
を起動しているターミナルへの表示内容を確認すれば、今度は fd2
が Ready になったことが確認できると思います。
いずれかのFDがReadyになりました fd2がReadyになりました Write data to fd2 (50002)
最後に、server.exe
を起動したまま放置してみましょう。約 60
秒後に server.exe
を起動しているターミナルに下記のような表示が行われるはずです。これは server.exe
で実行している select
関数でタイムアウトが発生したからになります。
タイムアウトが発生しました
こんな感じで、select
関数を利用することで複数のポートを監視することができ、これにより1つのプログラムから複数のポートからのデータの受信を実現することが可能となります。また、タイムアウトを実現できることも確認していただけたと思います。
スポンサーリンク
まとめ
このページでは、C言語における select
関数の使い方について解説しました!
select
関数を利用することで複数の FD の監視を同時に実現することが可能となります。また、FD の監視に対してタイムアウトを実現することも可能です。
特に1つのプログラムから複数のポートに対して受信待ちを行いたいような場合は、select
関数を利用することで簡単に実現することができます。
関数の実行前に FD の集合を準備する必要があったり、関数実行後に FD の集合のチェックを行う必要があったりして関数の使い方としては結構難しいですが、慣れてしまえば簡単に使いこなすことができるようになると思います。
是非このページで解説した内容を理解していただき、select
関数を利用してみていただければと思います!