このページでは、ソケット通信におから「タイムアウト付きのデータ受信」の実現方法について解説していきたいと思います。
データ受信を行う関数としては recv
や read
が存在しますが、今回は recv
関数を例にタイムアウト付きのデータ受信を実現していきたいと思います(read でも同様の方法でタイムアウト付きのデータ受信を実現することができます)。
私が知っているタイムアウト付きデータ受信を実現する方法は下記の2つです。
select
関数とrecv
関数を併用する- (
setsockopt
関数で)ソケット自体にタイムアウトを設定する
今回は前者の方法でタイムアウト付きデータ受信を実現していきたいと思います。
まず recv
関数と select
関数の動作、およびタイムアウト付きデータ受信の実現方法について解説し、さらには実際にタイムアウト付きデータ受信を行う関数例の紹介をしていきたいと思います。
Contents
recv
関数の動作
recv
関数は一言で言うと通信相手からデータを受信する関数です。
recv
関数は下記のように定義されています。
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv
関数は sockfd
で接続した通信相手からデータを受信し、その受信したデータを buf
のアドレスに読み込むという関数になります。
この時に読み込む最大のサイズが len
になります。
さらに flags
指定により recv
関数の動作の詳細を設定することが可能です。が、今回は flags
指定は 0
、つまりデフォルトの動作で recv
関数が動作する前提で解説を進めていきます。
recv
関数についてもう少し詳細に言うと下記のように動作する関数になります。
- データが読み込み可能になるまで待つ
- 読み込み可能になったら
buf
にデータを読み込む
この読み込みが終わった時に関数が終了します。
逆に言うと、データが読み込み可能になるまで関数が終了しません。
なので、通信相手がデータを送信してこなかったらずっと recv
関数で待たされる可能性があります(通信相手が接続をクローズしたり、エラーが発生したりしても終了する)。
select
関数の動作
一方で、select
関数は「ファイルディスクリプタが読み込み・書き込み可能になったかどうか」を監視する関数になります。さらに、select
関数ではファイルディスクリプタが読み込み・書き込み可能になるまで待ち合わせを行うことができます。
本来 select
関数はデータの入出力(ファイルへの書き込みや読み込み・ソケット通信での送信や受信)を多重化することを目的に使用されることが多いです
ですが、今回はタイムアウト付きデータ受信を行うために使用します
そのため、解説も多重化ではなくタイムアウト付きデータ受信を行うために必要な観点に絞って説明させていただきます
select
関数の定義は下記のようになります。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
ファイルディスクリプタというのは、データへアクセスするファイルや標準入出力などを識別するための識別子です(以降ファイルディスクリプタは FD と略します)。
ソケット通信に限定して考えると、要は socket
関数や accept
関数の戻り値が FD になります。例えば recv
関数や send
関数では、この FD を第1引数に指定して実行することで、どのソケットを利用して通信を行うかを指定することができます。
で、select
関数の引数で使用されている fd_set
は、その FD の集合を表す型になります(この集合の作り方などは後述で説明します)。
fd_set
型の引数はそれぞれ下記のようになります(第1引数の nfds
には、これらの集合にセットした FD の最大値 + 1
を指定します)。
readfds
:読み込み可能になったかどうかを監視したい FD の集合(のアドレス)writefds
:書き込み可能になったかどうかを監視したい FD の集合(のアドレス)exceptfds
:例外が発生したかどうかを監視したい FD の集合
これらに対し、select
関数では引数で指定した集合に含まれる FD を監視し、下記が発生するまで待ち合わせを行います。
readfds
の集合に含まれる FD のどれかが読み込み可能になったwritefds
の集合に含まれる FD のどれかが書き込み可能になったexceptfds
集合に含まれる FD のどれかに例外が発生した
そして、上記のどれかが発生したら select
関数は終了します。
ですので、recv
関数でデータが読み込み可能になるまで待つこともできますが、select
関数でも同様のことを行うことが可能です(ただし select
関数では実際のデータの読み込み処理は行われない。待つだけ)。
ただし、select
関数では、引数 timeout
に NULL
以外を指定することで待ち合わせの「タイムアウトを設定」することが可能という大きな違いがあります。
引数 timeout
にタイムアウト時間を設定することで、下記が “発生しなくても” タイムアウト時間が経過したタイミングで select
関数を終了することができます。
readfds
の集合に含まれる FD のどれかが読み込み可能になったwritefds
の集合に含まれる FD のどれかが書き込み可能になったexceptfds
集合に含まれる FD のどれかに例外が発生した
これを利用してタイムアウト付きデータ受信を実現していきたいと思います。
スポンサーリンク
タイムアウト付きデータ受信の実現
ここまで recv
関数と select
関数の動作について解説してきましたが、ここで2つの関数の動作をデータ受信に関して簡単にまとめておきたいと思います。
まず recv
関数は下記の動作を行う関数です。
- データが読み込み可能になるまで待つ
- 読み込み可能になったら
buf
にデータを読み込む
ただし、前者の処理はデータが読み込み可能になるまで無限に待ち続けることになります。
一方、select
関数は、下記を「タイムアウト付き」で動作させることができる関数です(書き込み等については省略しています)。
- データが読み込み可能になるまで待つ
要は、「データが読み込み可能になるまで待つ」は recv
関数でも select
関数でも行うことが可能です。
ただし、select
関数では「タイムアウト付き」でこれを実行できます。
ですので、下記のように処理を行うようにすればタイムアウト付きデータ受信を行うことができます。
select
関数を実行する- データが読み込み可能になるまで待つ
select
関数でタイムアウトが発生していなければrecv
関数を実行する- データが読み込み可能になるまで待つ
- 読み込み可能になったら
buf
にデータを読み込む
select
関数でタイムアウト発生した場合は recv
関数を行わないので、recv
関数での無限待ちを防ぐことができます。
「データが読み込み可能になるまで待つ」が2回行われることになりますが、select
関数ですでにデータが読み込み可能になるまで待った状態で recv
関数が実行されますので、recv
関数での「データが読み込み可能になるまで待つ」は即座に終了してすぐにデータの読み込み処理が行われることになります。
タイムアウト付きデータ受信を行う関数例
最後に、ここまでの解説を踏まえてタイムアウト付きデータ受信を行う関数の例を紹介し、続いてこの関数で行っている処理の説明をしていきたいと思います。
タイムアウト付きデータ受信を行う関数
タイムアウト付きデータ受信を行う関数 recvWithTimeout
は下記のようになります。
#include <sys/socket.h>
#include <sys/select.h>
#include <stdio.h>
/*
* タイムアウト付きデータ受信
* sockfd:データ受信を行うソケット
* buf:受信したデータを格納するバッファ
* len:バッファのサイズ
* flags:recv関数のオプション
* sec:タイムアウト時間(秒)
* usec:タイムアウト時間(マイクロ秒)
* 戻り値:
* エラー時:-1
* 相手の接続が終了:0
* タイムアウト発生:0
* データ受信成功時:受信したデータサイズ
*/
int recvWithTimeout(int sockfd, void *buf, size_t len, int flags, unsigned int sec, unsigned int usec) {
struct timeval tv;
fd_set readfds;
int ret_select;
int ret_recv;
/* タイムアウト時間を設定 */
tv.tv_sec = sec;
tv.tv_usec = usec;
/* 読み込みFD集合を空にする */
FD_ZERO(&readfds);
/* 読み込みFD集合にsockfdを追加 */
FD_SET(sockfd, &readfds);
/* selectでreadfdsのどれかが読み込み可能になるまでorタイムアウトまで待ち */
ret_select = select(sockfd + 1, &readfds, NULL, NULL, &tv);
/* 戻り値をチェック */
if (ret_select == -1) {
/* select関数がエラー */
printf("select error\n");
return -1;
}
if (ret_select == 0) {
/* 読み込み可能になったFDの数が0なのでタイムアウトと判断 */
printf("timeout!!\n");
return 0;
}
/* sockfdが読み込み可能なのでrecv実行 */
ret_recv = recv(sockfd, buf, len, flags);
return ret_recv;
}
スポンサーリンク
関数の引数の説明
下記の引数は recv 関数のものと全く同じになります。
sockfd
buf
len
flags
下記の引数はタイムアウト時間を設定するためのパラメータになります。
sec
:タイムアウト時間(秒単位)usec
:タイムアウト時間(マイクロ秒単位)
秒単位を sec
で、さらにマイクロ秒単位を usec
で指定することで、これらを足し合わせた時間のタイムアウト時間が設定されます。
sec
と usec
両方を 0
に設定すると 0
秒で(つまりすぐに)タイムアウトされるので注意してください。
sec
と usec
両方が 0
の場合に select
関数の引数 tv
を NULL
に設定するような制御文を追加すれば、引数に応じてタイムアウトなしでデータ受信を行うようなことも可能です
関数の処理の説明
タイムアウト付きデータ受信を行う関数 recvWithTimeout
で何を行っているかについて解説しておきます。
タイムアウト時間の設定
まず下記でタイムアウト時間を struct timeval tv
構造体に設定しています。
/* タイムアウト時間を設定 */
tv.tv_sec = sec;
tv.tv_usec = usec;
単に tv
の各メンバに引数で指定された sec
と usec
をセットしているだけです。
これを select
関数の引数に指定することで、読み込み可能な FD がなくても指定した時間が経過すれば select
関数が終了するようになります。
読み込み可能かどうかを監視する FD の集合を作成
続いて読み込み可能になったかどうかを監視する FD の集合 readfds
を作成します。
今回は監視したい FD が sockfd
のみですので、sockfd
のみを集合に追加するようにしています。
この集合を作成しているのが下記部分になります。
/* 読み込みFD集合を空にする */
FD_ZERO(&readfds);
/* 読み込みFD集合にsockfdを追加 */
FD_SET(sockfd, &readfds);
FD の集合を操作するためのマクロとして下記の4つが用意されています。
今回は sockfd
のみの集合を作成したかったので、まず FD_ZERO
で集合を空にし、続いて FD_SET
で sockfd
を集合に追加することでこれを実現しています。
FD_ZERO
:集合を空にするFD_SET
:指定した FD を集合に追加するFD_CLR
:指定した FD を集合から削除するFD_ISSET
:集合した FD が集合に存在するかを判断する
今回は集合に追加する FD が1つなので FD_SET
を実行するのも1度だけですが、複数の FD を集合に追加する場合は FD_SET
を複数回実行することになります。
select
関数では、監視する対象のソケットが一つの場合でも集合として fd_set
の型として指定必要があります
直接引数にソケットの FD を渡すとダメなので注意してください
select
関数を実行
続いて select
関数を実行して sockfd
が読み込み可能になる or タイムアウトが発生するのを待ちます。
/* selectでreadfdsのどれかが読み込み可能になるまでorタイムアウトまで待ち */
ret_select = select(sockfd + 1, &readfds, NULL, NULL, &tv);
/* 戻り値をチェック */
if (ret_select == -1) {
/* select関数がエラー */
printf("select error\n");
return -1;
}
if (ret_select == 0) {
/* 読み込み可能になったFDの数が0なのでタイムアウトと判断 */
printf("timeout!!\n");
return 0;
}
今回は読み込み可能になったかどうかのみを監視したかったので、select
関数の引数 writefds
と exceptfds
は NULL
にしています。
readfds
は先程作成した sockfd
のみが存在する集合を指定します。
引数で指定した集合の中で一番大きな FD は sockfd
になりますので、第1引数の nfds
には sockfd + 1
を指定し、さらに第5引数 timeout
は先程タイムアウト時間を設定した tv
を指定します。
以上のように引数を設定して select
関数を実行することで、下記が発生するまで select
関数が待つことになります。
- エラーが発生した(戻り値:
-1
) - タイムアウトが発生した(戻り値:
0
) sockfd
が読み込み可能になった(戻り値:1
)
select
関数でエラーもタイムアウトも発生しなかった時の戻り値は下記の FD の数になります
- 読み込み可能になった FD
- 書き込み可能になった FD
- 例外が発生した FD
今回は集合に存在する FD が1つなので戻り値は 1
になりますが、複数存在する場合は 1
よりも大きい値が返却されるこももあるので注意してください
select
関数の戻り値が -1
の場合は select
関数でエラーが発生しているので recvWithTimeout
関数も -1
を返却してエラー終了するようにしています。
また select
関数の戻り値が 0
の場合はタイムアウトが発生しているので recvWithTimeout
関数はデータ受信でタイムアウトが発生したことを示す 0
を返却して終了するようにしています。
タイムアウト発生していない場合は recv
関数を実行
さらに select
関数の戻り値が -1
でも 0
でもない場合は、実際に recv
関数を実行して受信したデータの読み込みを行います。
/* sockfdが読み込み可能なのでrecv実行 */
ret_recv = recv(sockfd, buf, len, flags);
return ret_recv;
readfds
に sockfd
のみをセットした状態で実行した select
関数が 1
を返却するということは、sockfd
はすでに読み込み可能状態であるため、recv
関数では読み込み待ちが即座に終了してデータの読み込みが行われるはずです。
また、今回は FD の集合に存在する FD が1つだけなので問題ないですが、2つ以上存在する場合は FD_ISSET
マクロを実行して、どの FD が読み込み可能・書き込み可能になったかを調べる必要があります。
まとめ
今回は select
関数を利用した「タイムアウト付きデータ受信」の実現方法について解説しました。
下記のように処理を行うことでタイムアウト付きデータ受信を実現することができます。
select
関数を実行するselect
関数でタイムアウトが発生していなければrecv
関数を実行する
今回はデータ受信に限定して説明を行いましたが、これと同様の方法で「タイムアウト付きデータ送信」や「タイムアウト付きコネクト」なども実現することが可能です!
select
関数は個人的に使い方に結構癖があるなぁと思っています。
今回紹介した使い方は select
関数の利用例の中でもかなり簡単なものになると思いますので、実際の使用例や解説を確認して select
関数の使い方も理解していただければと思います!