【C言語】selectを使用してタイムアウト付き受信を実現する

タイムアウト付きデータ受信の実現方法解説ページのアイキャッチ

このページでは、ソケット通信におから「タイムアウト付きのデータ受信」の実現方法について解説していきたいと思います。

データ受信を行う関数としては recvread が存在しますが、今回は recv 関数を例にタイムアウト付きのデータ受信を実現していきたいと思います(read でも同様の方法でタイムアウト付きのデータ受信を実現することができます)。

私が知っているタイムアウト付きデータ受信を実現する方法は下記の2つです。

  • select 関数と recv 関数を併用する
  • setsockopt 関数で)ソケット自体にタイムアウトを設定する

今回は前者の方法でタイムアウト付きデータ受信を実現していきたいと思います。

まず recv 関数と select 関数の動作、およびタイムアウト付きデータ受信の実現方法について解説し、さらには実際にタイムアウト付きデータ受信を行う関数例の紹介をしていきたいと思います。

recv 関数の動作

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 関数ではファイルディスクリプタが読み込み・書き込み可能になるまで待ち合わせを行うことができます。

MEMO

本来 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 関数では、引数 timeoutNULL 以外を指定することで待ち合わせの「タイムアウトを設定」することが可能という大きな違いがあります。

引数 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 は下記のようになります。

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 で指定することで、これらを足し合わせた時間のタイムアウト時間が設定されます。 

secusec 両方を 0 に設定すると 0 秒で(つまりすぐに)タイムアウトされるので注意してください。

MEMO

secusec 両方が 0 の場合に select 関数の引数 tvNULL に設定するような制御文を追加すれば、引数に応じてタイムアウトなしでデータ受信を行うようなことも可能です

関数の処理の説明

タイムアウト付きデータ受信を行う関数 recvWithTimeout で何を行っているかについて解説しておきます。

タイムアウト時間の設定

まず下記でタイムアウト時間を struct timeval tv 構造体に設定しています。

タイムアウト時間の設定
    /* タイムアウト時間を設定 */
    tv.tv_sec = sec;
    tv.tv_usec = usec;

単に tv の各メンバに引数で指定された secusec をセットしているだけです。

これを select 関数の引数に指定することで、読み込み可能な FD がなくても指定した時間が経過すれば select 関数が終了するようになります。

読み込み可能かどうかを監視する FD の集合を作成

続いて読み込み可能になったかどうかを監視する FD の集合 readfds を作成します。

今回は監視したい FD が sockfd のみですので、sockfd のみを集合に追加するようにしています。

この集合を作成しているのが下記部分になります。

FDの集合を作成
    /* 読み込みFD集合を空にする */
    FD_ZERO(&readfds);

    /* 読み込みFD集合にsockfdを追加 */
    FD_SET(sockfd, &readfds);

FD の集合を操作するためのマクロとして下記の4つが用意されています。

今回は sockfd のみの集合を作成したかったので、まず FD_ZERO で集合を空にし、続いて FD_SETsockfd を集合に追加することでこれを実現しています。

  • FD_ZERO:集合を空にする
  • FD_SET:指定した FD を集合に追加する
  • FD_CLR:指定した FD を集合から削除する
  • FD_ISSET:集合した FD が集合に存在するかを判断する 

今回は集合に追加する FD が1つなので FD_SET を実行するのも1度だけですが、複数の FD を集合に追加する場合は FD_SET を複数回実行することになります。

memo

select 関数では、監視する対象のソケットが一つの場合でも集合として fd_set の型として指定必要があります

直接引数にソケットの FD を渡すとダメなので注意してください

select 関数を実行

続いて select 関数を実行して sockfd が読み込み可能になる or タイムアウトが発生するのを待ちます。

select関数の実行
    /* 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 関数の引数 writefdsexceptfdsNULL にしています。

readfds は先程作成した sockfd のみが存在する集合を指定します。

引数で指定した集合の中で一番大きな FD は sockfd になりますので、第1引数の nfds には sockfd + 1 を指定し、さらに第5引数 timeout は先程タイムアウト時間を設定した tv を指定します。

以上のように引数を設定して select 関数を実行することで、下記が発生するまで select 関数が待つことになります。

  • エラーが発生した(戻り値:-1
  • タイムアウトが発生した(戻り値:0
  • sockfd が読み込み可能になった(戻り値:1
MEMO

select 関数でエラーもタイムアウトも発生しなかった時の戻り値は下記の FD の数になります

  • 読み込み可能になった FD
  • 書き込み可能になった FD
  • 例外が発生した FD

今回は集合に存在する FD が1つなので戻り値は 1 になりますが、複数存在する場合は 1 よりも大きい値が返却されるこももあるので注意してください

select 関数の戻り値が -1 の場合は select 関数でエラーが発生しているので recvWithTimeout 関数も -1 を返却してエラー終了するようにしています。

また select 関数の戻り値が 0 の場合はタイムアウトが発生しているので recvWithTimeout 関数はデータ受信でタイムアウトが発生したことを示す 0 を返却して終了するようにしています。

タイムアウト発生していない場合は recv 関数を実行

さらに select 関数の戻り値が -1 でも 0 でもない場合は、実際に recv 関数を実行して受信したデータの読み込みを行います。

recvの実行
    /* sockfdが読み込み可能なのでrecv実行 */
    ret_recv = recv(sockfd, buf, len, flags);

    return ret_recv;

readfdssockfd のみをセットした状態で実行した select 関数が 1 を返却するということは、sockfd はすでに読み込み可能状態であるため、recv 関数では読み込み待ちが即座に終了してデータの読み込みが行われるはずです。

また、今回は FD の集合に存在する FD が1つだけなので問題ないですが、2つ以上存在する場合は FD_ISSET マクロを実行して、どの FD が読み込み可能・書き込み可能になったかを調べる必要があります。

まとめ

今回は select 関数を利用した「タイムアウト付きデータ受信」の実現方法について解説しました。

下記のように処理を行うことでタイムアウト付きデータ受信を実現することができます。

  • select 関数を実行する
  • select 関数でタイムアウトが発生していなければ recv 関数を実行する

今回はデータ受信に限定して説明を行いましたが、これと同様の方法で「タイムアウト付きデータ送信」や「タイムアウト付きコネクト」なども実現することが可能です!

select 関数は個人的に使い方に結構癖があるなぁと思っています。

今回紹介した使い方は select 関数の利用例の中でもかなり簡単なものになると思いますので、実際の使用例や解説を確認して select 関数の使い方も理解していただければと思います!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です