send と recv と送信バッファ溢れ

send と recv の使い方と送信バッファ溢れについて。

manpage

Manpage of SEND
Manpage of RECV

SEND

non-blocking の場合。
send が失敗して外部変数 errno に EINTR が設定されている場合は、再度送信を試みる必要がある。
データをソケットの送信バッファに入れることが出来ない場合、外部変数 errno に EAGAIN か EWOULDBLOCK が設定される。
いつデータが送信できるようになるかを知るために select などのシステムコールを使うことが出来る。(後述:送信バッファ溢れ)

以下、ソースコードから抜粋した send のサンプル。

int res = 0;  
char* p = buffer; // 送るデータ  
size_t len = buffer_size; // 送るデータのサイズ  
while( len > 0 ){  
    while( true ){  
        res = send( socket, p, len, MSG_DONTWAIT ); // 一度に全部送れるとは限らない  
        if( errno != EINTR ) break; // システム割り込みチェック  
    }  
    if( res < 0 ){  
        if( errno == EAGAIN ||  
            errno == EWOULDBLOCK ){  
            // 書き込めない状態。それなりの対応が必要(後述:送信バッファ溢れ)  
        }  
        else{  
            // なんらかのエラー(コネクション切断とか)  
        }  
    }  
    len -= res;  
    p += res;  
}  

RECV

non-blocking の場合。
操作が停止するような場合、外部変数 errno に EAGAIN か EWOULDBLOCK が設定される。

以下、ソースコードから抜粋した recv のサンプル。

int res = 0;  
while( true ){  
    res = recv( socket, buffer, buffer_size, MSG_DONTWAIT );  
    if( errno != EINTR ) break;  
}  
if( res == 0 ){  
    // EOF  
}  
  
if( res < 0 ){  
    if( errno == EAGAIN ||  
        errno == EWOULDBLOCK ){  
        // not ready yet.  
    }  
    else{  
        // error.  
    }  
}  

送信バッファ溢れ

データ送信時に送信バッファが溢れてしまった場合、書き込み可能なタイミングを見計らって、溢れたデータを再送信してあげる必要がある。
送信バッファが溢れたとき、send 関数は失敗し、errno に EAGAIN, EWOULDBLOCK を格納する。
対策として例えば、送信データをキューなり可変長バッファなりで一時的に保持し、select などで書き込み可能になるタイミングを監視して、再送信する方法がある。

サンプル:sendbuffer-overflow
事前に libevent をインストールしておく必要あり。
動作環境 Linux のみ。(送信バッファと受信バッファの数値を linux/sockios.h 使って確認してる為)
1.サーバは受信時に3秒間待機する。
2.クライアントはサーバへデータを送信しまくり、EAGAIN or EWOULDBLOCK を発生させる。
3.エラー発生後、クライアントは書き込み可能イベントを監視し、検知したらデータを再送信する。
2ー3の動作を繰り返し、サーバに規定のデータ(0 - 499 までの連番)が全部届いたら成功。

インストール

$ git clone http://github.com/utahta/sendbuffer-overflow.git  
$ cd sendbuffer-overflow  
$ ./configure --prefix=/path/to/sendbuffer-overflow --with-libevent=/path/to/libevent  
$ make  
$ make install  

実行

コマンドプロンプトをふたつ立ち上げ、1つめにサーバを起動します。

$ /path/to/sendbuffer-overflow/bin/sbt_server  

2つめにクライアントを起動します。

$ /path/to/sendbuffer-overflow/bin/sbt_client  

手元では、以下のような結果になりました。

$ /path/to/sendbuffer-overflow/bin/sbt_server  
connected. sd:7  
recv buffer size:274 sd:7  
waiting 3 sec.  
... 省略  
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:495  
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:496  
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:497  
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:498  
message received. sd:7 str:abcdefghijklnmopqrstuvwxyz:499 // 499 まで届いた  
$ /path/to/sendbuffer-overflow/bin/sbt_client  
send buffer size. :50436  
... 省略  
send buffer size:48498 res:0 sd:6  
send buffer size:48772 res:0 sd:6  
send buffer size:49046 res:0 sd:6  
not ready yet: Resource temporarily unavailable // 書き込み失敗  
send buffer size:49152 res:-10 sd:6  
not ready yet: Resource temporarily unavailable  
send buffer size:49152 res:-10 sd:6  
... 省略  
resend buffer size:88558 res:0 sd:6 // 書き込み可能になったので再送信  

このサンプルでは、可変長処理をさぼってあらかじめ大きなバッファを確保してごまかしてたり。
終了処理に手抜きがあったり。(delete してなかったり)
とりあえずサンプルということで。