【C言語】select関数の使い方(複数ソケットの監視)

このページにはプロモーションが含まれています

このページでは、C言語における select 関数について説明していきます。

select 関数は非常に便利な関数で、これを使いこなすことで自身で開発できるプログラムの幅が一気に広がります。ですが、その反面、使い方が難しい関数でもあります。出来るだけ多くの方に select 関数の使い方を理解していただけるよう、詳しく&分かりやすく解説していきますので、select 関数に興味のある方は是非このページを読み進めていただければと思います。

また、このページでは Linux や MaxOS で select 関数を利用することを前提として解説していきますが、Windows にも select 関数が存在しますし、同様の使い方で利用できるはずです。ただ、インクルードするファイルなどが異なるため、その点は注意していただければと思います。とりあえず、select 関数を使うときのポイントなどは Windows ユーザーの方でも参考になると思います。

select 関数

まずは、select 関数自体について解説していきます。

select は複数の FD を監視する関数

この select は複数のファイルディスクリプタを監視する関数となります。

FD とは

ファイルディスクリプタとは、プログラムが使用するファイルやソケットなどを識別する識別子となります。以降、ファイルディスクリプタのことを FD と略します。

例えば、下記ページでも解説しているように、ソケット通信を行うために必要となるソケットは socket 関数で作成可能です。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

そして、socket 関数の返却値は、作成されたソケットの FD となります。この FD の型は int で、つまりは FD の実体は単なる整数となります。なんですが、この FD により、1つのプログラムの複数ソケットを作成したような場合でもプログラム内で各ソケットを識別することが可能となります。

例えば、ソケット関連の関数では引数として FD が指定可能なものが多いです。これは、プログラムの開発者が、どのソケットを利用してデータの送信やデータの受信を行うのかを指定できるようにするためです。

ソケット通信でFDを指定して特定のソケットからデータの受信を行う様子

特定の FD に対してデータの読み込み / 書き込みが行われる流れ

ここで、もう少し詳しく、特定の FD に対してデータの読み込み(受信も含む)やデータの書き込み(送信も含む)が行われる流れについて説明したいと思います。ここでは例として recv 関数で説明していきますが、他の関数においても、特定の FD に対して読み込みや書き込みが行われる基本的な流れは同様になると思います。

recv 関数では、第1引数に特定のポートに関連付けられたソケットの FD を指定するようになっています(この関連付けは bind 関数により行われます)。そして、recv 関数を実行すると、まず第1引数で指定した FD のソケットが読み込み可能な状態になるまで FD が監視されることになります(その FD に関連づけられたポートに対してデータが送信されてくると読み込み可能状態になる)。この監視されている間、関数実行側は待ち状態となります。そして、読み込み可能な状態になった際に、recv 関数がデータを読み込んで recv 関数を終了するようになっています。 

recv関数の処理の流れ

逆に、そのソケットが読み込み可能な状態にならなければ、recv 関数はずっと待ちの状態になります。

MEMO

タイムアウトや非同期設定を行うことも可能ですが、このページでは、これらの設定を行わない前提で解説をしていきます

要は、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 になることを監視することは scanfrecv 関数等でも行えるのですが、前述の通り、これらが監視できるのは特定の1つの FD のみです。

それに対し、select 関数では複数の FD を同時に監視することが可能です。そのため、この関数を利用することで、マルチスレッドやマルチプロセスのような難しいことを行わなくても複数の FD の同時監視を実現することが出来ます。

select関数が複数のFDを監視する様子

使い方に関しては後述で詳しく解説しますが、select 関数では引数で指定された FD の集合に含まれるいずれかの FD が Ready 状態になる or タイムアウトが発生するまで集合に含まれる全ての FD の監視が行われます。

したがって、タイムアウトが発生せずに select 関数が終了した際には、いずれかの FD が Ready 状態になっていることになりますので、その FD に対して読み込みや書き込みを実行すれば、待つことなく読み込みや書き込みが実行することが行えることになります。

select関数が複数のFDを監視する様子2

このような、複数の FD の同時監視はソケット通信でよく利用されます。1つのプログラムで複数のソケットを作成して別々のポートで受信待ちを行いたい場合は、select 関数で複数のソケット (FD) を監視する事になります。そして、select 関数が終了したタイミングで、Ready になったソケットからデータの読み込み (データの受信) を行うようなプログラムを作ることが多いです。

select 関数の使い方 では、この複数のソケットを監視する例も用いながら select 関数の使い方について解説していきたいと思います。

スポンサーリンク

select はタイムアウトの設定が可能な関数

また、select は複数の FD の同時監視だけでなく、タイムアウトを実現するために利用されることも多いです。

例えば scanf 関数は、一度実行すると標準入力への入力が行われない限り、つまり標準入力が Ready にならない限り永遠に待ち続ける関数になります。要は、ずっと標準入力の監視が行われている状態となります。

こんな標準入力への入力のような、通常ではタイムアウトが設定不可な読み込みや書き込みに対してタイムアウトを設けるためにも select 関数が利用されることがあります。

select 関数では引数 timeout によりタイムアウト時間を設定することが出来ます。タイムアウト時間が設定されていると、select 関数で監視を始めてからタイムアウト時間が経過しても FD が Ready にならない場合に関数が終了するようになっています。したがって、例えば、scanf 関数実行前にタイムアウト時間を設定した select 関数で標準入力の監視を行い、タイムアウト時間経過しても入力が無かった際には scanf 関数を実行しないようにしてやれば、標準入力への入力待ちにタイムアウトを設けることが出来ることになります。

select関数を利用した標準入力への入力待ちへのタイムアウト設定

ということで、select 関数は複数の FD の同時監視だけでなく、読み込みや書き込みに対してタイムアウトを設定したいような場合にも利用することがあります。

実際に、select 関数を利用して標準入力からの入力受付にタイムアウトを設定する手順を下記ページで解説していますので、興味があれば読んでみてください。「時間制限付き4択クイズ」の作り方なども説明しています。

【C言語】タイムアウト付きscanfを実現(時間制限ありの4択クイズのサンプル付き)

select 関数の使い方

次は、select 関数の使い方について説明していきたいと思います。

まず、select 関数は下記のように定義された関数となります。使用するためには sys/select.h をインクルードしておく必要があります。

select
#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つの引数 readfdswritefdsexceptfds には FD の集合(のアドレス)を指定する必要があります。これらは目的別に集合の指定を行う引数であり、readfds には読み込みに対する監視、writefds には書き込みに対する監視、exceptfds には例外に対する監視を行いたい FD の集合(のアドレス)を指定する必要があります。

これらを指定して select 関数を実行すれば、各目的別の FD の集合に含まれるいずれかの FD が Ready になるまで、より具体的には、readfds に含まれるいずれかの FD が読み込み可能になるまで or writefds に含まれるいずれかの FD が書き込み可能になるまで or exceptfds に含まれるいずれかの FD で例外が発生するまで監視が行われ、関数が待ち状態になります。

select関数が集合に含まれるFDに対して、目的別の監視を行う様子

そして、いずれかの FD が Ready 状態になったタイミングで select 関数が終了します。これらの FD の集合に含まれない FD は監視対象になりません。また、目的とは異なるイベントは監視されません。例えば、readfds の集合に含まれる FD が書き込み可能になったとしても、select 関数は監視状態のままとなります。

ということで、select 関数を使いこなすためには、これらの FD の集合の引数への指定が重要となります。ここからは、この FD の集合の用意の仕方も含めて、select 関数を使う手順を説明していきます。

ちなみに、今回は複数のソケットでの UDP 通信を行う際に各ソケットのデータの読み込み(データの受信)を監視するプログラムの例で select 関数の使い方を示していきます。UDP 通信について詳しく知りたい方がおられましたら、是非下記ページを読んでみてください。

C言語でUDP通信を行う方法の解説ページアイキャッチ 【C言語】UDP通信を行う

また、ソケット通信自体に関しては下記ページで解説していますので、こちらも興味があったり例の中で使用している各関数の意味合いが理解できないような場合は読んでみていただければと思います。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

監視対象とする FD を用意する

まずは、監視対象とする FD を用意する必要があります。

監視対象のFDを用意する様子

ソケットの場合であれば、socket 関数を実行すればソケットが生成され、その FD が返却値として得られます。また、プログラムには自動的に用意されている FD もあります。具体的には、標準入力 stdin、標準出力 stdout、標準エラー出力の stderr は作成しなくても各プログラム(プロセス)に用意されている FD であり、これらは stdio.h をインクルードすれば利用可能となります。

例えば、2つのソケットを監視対象とするのであれば、下記のような処理を行えば変数 fd1fd2 のそれぞれでソケットの FD を管理できることになります(この章ではインクルードやエラーハンドリングなどを省略したコードを紹介していきます)。

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 関数を複数同時に実行することはできず、fd1fd2 を同時に監視することはできませんが、これらの FD を追加した集合を readfds 引数に指定して select 関数を実行することで fd1fd2 の両方を同時に読み込みに対して監視することができるようになります。

スポンサーリンク

FD の集合を用意する

監視対象の FD が用意できれば、次は FD の集合の用意を行います。今回は、2つのソケットのデータの読み込み(データの受信)を監視するため、select 関数における readfds 引数に指定する FD の集合を用意していきます。

まず、sys/select.h をインクルードすれば fd_set という型が利用可能になり、これが “FD の集合” を管理する型となります。

そして、fd_set の変数が FD の集合となり、これに監視対象となる FD を追加していくことになります。

FDの集合がfd_set型の変数であることを示す図

また、FD の集合への操作は、”FD の集合への操作用のマクロ” を利用して行うことになります。このマクロに関しても、sys/select.h をインクルードすることで利用可能となります。

FD_ZERO で FD の集合を空にする

FD の集合を利用する場合、まずは FD の集合を空にする必要があります。

FD の集合も、他の変数同様に、単に変数宣言をするだけだと不定値が格納されていることになり、fd_set の変数の場合は不要な FD が FD の集合に含まれていることになります。

なので、最初に FD の集合を空にする必要があります。

FDの集合を空にする様子

そして、FD の集合を空にする操作は、マクロ FD_ZERO によって行うことができます。堕1引数に FD の集合の変数のアドレスを指定して FD_ZERO を実行すれば、その FD の集合が空になります。

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 の集合に FD を追加するためには FD_SET というマクロを利用します。この FD_SET は、第1引数に監視対象とする FD を、第2引数に FD の集合のアドレスを指定して実行します。FD_SET は1回の実行で1つの FD しか追加できないため、追加したい FD の数だけ分、FD_SET を実行する必要があります。

今回の例では fd1fd2 を監視するため、下記のように2回 FD_SET を実行することになります。これにより、read_fd_set という FD の集合に fd1fd2 が追加されることになります。

FDの集合にFDを追加する
FD_SET(fd1, &read_fd_set);
FD_SET(fd2, &read_fd_set);

そして、この read_fd_set のアドレスを select 関数の readfds 引数に指定すれば、select 関数実行によって fd1fd2 の読み込みの監視が行われることになります。

FD_CLEAR で FD の集合から FD を削除する

今回の例の場合は不要ではあるのですが、FD_CLEAR を実行して FD の集合から FD を削除するようなことも可能です。

FDの集合から特定のFDを取り除く様子

FD_CLEAR は第1引数に監視対象とする FD を、第2引数に FD の集合のアドレスを指定して実行します。

例えば、下記を実行すれば、read_fd_set から fd1 を削除することができます。ただ、今回の例では fd1fd2 を監視するため、下記の処理は不要となります。

FDの集合にFDを追加する
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 に指定する値は簡単に求めることが可能です。

例えば、今回の場合は fd1fd2 のみを監視対象とするため、fd1fd2 の大きい方の値が監視対象となる FD の最大値となります。そして、FD が2つの場合は単に下記のように2つの変数を比較するだけで最大値が求まります。

監視対象とするFDの最大値を求める
int max_fd;

if (fd1 < fd2) {
    max_fd = fd2;
} else {
    max_fd = fd1;
}

上記で求まる max_fd監視対象とする FD の最大値 ですので、select 関数実行時には max_fd + 1 の値を nfds に指定する必要があります。

また、監視対象とする FD の数が多い場合も、例えば下記ページで紹介しているような方法で簡単に最大値は求めることが可能だと思います。

C言語における最大値と最小値の求め方の解説ページアイキャッチ 【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 関数を実行してやれば良いです。

select関数の実行
int ret = select(max_fd + 1, &read_fd_set, NULL, NULL, &tm);

select 関数では、下記の4つ引数に関しては NULL を指定することも可能です。

  • readfds
  • writefds
  • exceptfds
  • timeout

例えば、上記の select 関数の実行例であれば、writefdsexceptfdsNULL を指定しているため、書き込みと例外発生に関しては監視が行われないことになります。このように、監視が不要であれば集合を用意することなく 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 に関しては下記ページで解説していますので、詳しくは下記ページを参照していただければと思います。

【C言語】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 というマクロで調べることができます。

特定のFDがFDの集合に存在するかどうかを調べる様子

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 の集合に含まれる fd1fd2 のうち、Ready になった FD に対して recv 関数で読み込みを実施する処理は下記のようなものになります。下記では、recv 関数で読み込んだ(受信した)文字列を printf で出力するようにしています。

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 が Ready になっている可能性があるという点になります。上記では、そのような場合に Ready になった全ての FD に対して読み込みが実行されるよう、全ての FD に対して FD_ISSET で Ready かどうかを調べるようにしています。そして、全ての Ready になっている FD に対して recv を実行するようにしています。

例えば、上記で2つ目の ifelse 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 はクローズしてやることは忘れないようにしましょう。

FDをクローズする
close(fd1);
close(fd2);

select 関数の使い方のまとめ

select 関数の使い方の解説は以上となります。

最後に使い方をまとめておくと、select 関数は下記のような手順を踏んで使用する必要があります。適切に引数を用意する必要がある点や、実行後にも select 関数が終了した原因を調べたり、Ready になった FD を特定する必要があったりする点が select 関数を利用する時のポイントになると思います。また、集合というC言語ではあまり使用しない型を利用する必要がある点にも注意してください。

  • 監視対象とする FD を用意する
  • FD の集合を用意する
  • 監視対象とする FD の最大値を求める
  • タイムアウト時間を設定する
  • 用意したデータを引数に指定して select 関数を実行する
  • 返却値を確認する
  • Ready になった FD を調べて読み込みや書き込みを実行する
  • 用意した FD をクローズする

select 関数で複数のポートを監視するプログラムのサンプル

ここまで断片的にソースコードを紹介してきましたが、ここまでに紹介したコードを一つにまとめたものを、select 関数で複数のポートを監視するプログラムのサンプルとして紹介しておきます。

スポンサーリンク

複数のポートを監視するプログラムのソースコード

そのサンプルのソースコードは下記となります(エラーハンドリングの多くを省略しているので注意してください)。前述のとおり、UDP 通信におけるソケットの読み込みの監視を行う例となっています。

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_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);
}

ただ、上記だけだと fd1fd2 が読み込み可能になるまで待ち続けるだけのプログラムになってしまいます。FD が読み込み可能になるためには、書き込み側のプログラムも必要です。

ということで、書き込み側のプログラムのソースコードの例(client.c)も下記に掲載しておきます。

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つのソケットの読み込みに対する監視が行われるようになっています。そして、各ソケット fd1fd2 はそれぞれ、bind 関数によってポート番号 50001 とポート番号 50002 に関連付けられています。

さらに、これらの fd1fd2 は読み込み(受信)に対して select 関数によって監視されることになります。

server.cのプログラムがselect関数でfd1とfd2を監視している様子

そのため、他のプログラムからポート番号 50001 or 50002 にデータが送信されてくると、そのポート番号に関連付けられたソケット fd1 or fd2 が読み込み可能となり、select 関数の監視、すなわち読み込み可能になるまでの待ちが終了し、次に読み込み可能となったソケットから recv 関数によって実際にデータの受信が行われるようになっています。

前述の通り、select 関数で引数に指定した FD の集合が書き換えられてしまうため、繰り返し実行する時は select 関数を実行するたびに FD の集合への操作を再度実施する必要があるので注意してください。

上記の server.c においては、while ループの内側の最初に FD の集合の操作を行うことで、selec 関数実行前に必ず FD の集合に fd1fd2 が追加された状態になるようにしています。

client.c

client.cserver.c のプログラムで監視されている FD に対してデータの書き込みを行うプログラムのソースコードとなっています。より具体的には、server.c のプログラムで監視されている FD に関連付けられているポートに対してデータの送信を行うプログラムのソースコードとなっています。

client.cのプログラムがserver.cのプログラムによって監視されているFD(ポート)にデータを送信する様子

まず client.c は実行された直後に scanf での文字列の入力待ちをするようになっています。ここで 1 を入力してエンターキーを押せば、自身の PC(アドレス 127.0.0.1)のポート 50001 に対してデータが送信されることになります。したがって、server.c のプログラムが監視している fd1 (fd1 はポート 50001 に関連付けられている)が読み込み可能となって select 関数が終了し、fd1 からデータの読み込みが行われることになります。そして、再び select 関数が実行されて fd1fd2 が監視されることになります。

同様に、2 を入力してエンターキーを押した場合はポート 50002 に対してデータが送信され、上記と同様の仕組みで fd2 からデータの読み込みが行われ、また select 関数が実行されて fd1fd2 が監視されることになります。

ということで、前述で紹介した server.cclient.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 関数を利用してみていただければと思います!

同じカテゴリのページ一覧を表示