setsockopt(2)でSO_SNDTIMEO, SO_RCVTIMEOが指定できない?

忘れたころに使いどころが出てくるソケットまわりの話.socketの設定にはsetsockopt(2)でいろいろやるのだけれど,今回は以下の2ケースのタイムアウト設定に挑戦して,ぜーんぶうまくいかなかったけれど,いろいろ試行錯誤したから失敗記事にしてみる.

  • (1) 接続先ホストに対してconnect(2)失敗のタイムアウトを指定したい.
  • (2) 接続した状態で接続先ホストがダウンしたときなど,read, writeの応答なしのタイムアウトを指定したい.

今回は,以下の環境で実験した.ふたつの環境で挙動が違ったから,また泣きたくなる.

(1) connectタイムアウトの設定

まず(1)から.以下の記事を見ると,どうやらsetsockopt(2)でSO_SNDTIMEOを設定すればよさそう.

なんだ,簡単じゃないかと,エラー処理のない書き殴りソースを書いてみる.

  • connect.c
//
// % echo "fugafuga" | nc -l -p 4649
//
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int
main (int argc, char *argv[])
{
 int sock = socket( PF_INET, SOCK_STREAM, 0 );

 struct timeval send_tv;
 send_tv.tv_sec  = 1;
 send_tv.tv_usec = 0;
 setsockopt( sock, SOL_SOCKET, SO_SNDTIMEO, &send_tv, sizeof(send_tv) );

 struct timeval recv_tv;
 recv_tv.tv_sec = 5;
 recv_tv.tv_sec = 0;
 // コメントアウトすると1秒でTIMEOUTされない
 setsockopt( sock, SOL_SOCKET, SO_RCVTIMEO, &recv_tv, sizeof(recv_tv) );

 struct sockaddr_in sa;
 memset( &sa, 0, sizeof( sa ) );
 // sa.sin_len    = sizeof( sa );
 sa.sin_family = AF_INET;
 sa.sin_port   = htons( 4649 );
 sa.sin_addr.s_addr = inet_addr( "127.0.0.1" );

 connect( sock, (struct sockaddr *)&sa, sizeof( sa ) );

 char buff[ 1024 ];
 read( sock, buff, 1024 );
 printf("%s\n", buff );

 return 0;
}

ちゃんとつながったかどうかを確認するために,netcatを使って4649ポートをlistenする.

% echo "fugafuga" | nc -l -p 4649

別の端末から以下を実行

% gcc -o connect connect.c
% ./connect
fugafuga

ちゃんと4649ポートに接続してメッセージを受け取ったことがわかる.


さて疑似的にホストがダウンしていることを再現するため,iptablesによるパケットフィルタリングを行う.

# iptables -A INPUT -p tcp -s 127.0.0.1 --dport 4649 -j DROP
(設定解除は # iptables -D INPUT -p tcp -s 127.0.0.1 --dport 4649 -j DROP)

これで,localhostから来る4649ポート宛のパケットはdropされる.再びconnectを実行

% ./connect
(X秒経過)
%

(A)では,SO_SNDTIMEOの時間でタイムアウトした.ただし,SO_RCVTIMEOを設定しないと指定した時間でタイムアウトしなかった.(謎の挙動)
(B)では,SO_SNDTIMEO,SO_RCVTIMEOいずれの時間とも関係のない21秒でタイムアウトした.


Linuxのコネクトタイムアウトは,明示的には指定できないものらしい.

(2) readタイムアウトの設定

今度は「接続は成功したのだけれど,readのタイミングでホストの応答がなくなった時にタイムアウトする時間」を指定したいというケース.このケースだと上記のnetcatを使ったサーバだと接続したタイミングでクライアントのreadバッファに入ってしまうので,scanf()で待ち時間を作るというテクい方法で再現してみる.

実験手順

  • serverを起動
  • clientを起動,scanf()で待ちが発生
  • iptables でパケットフィルタする
  • clientのscanf()を実行,readができない状態に陥る

という感じ.ソースコードは以下のとおり.

  • server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int
main (int argc, char *argv[])
{
 int sock = socket( PF_INET, SOCK_STREAM, 0 );

 struct timeval send_tv;
 send_tv.tv_sec  = 1;
 send_tv.tv_usec = 0;
 // 意味ない?
 setsockopt( sock, SOL_SOCKET, SO_SNDTIMEO, &send_tv, sizeof(send_tv) );

 struct timeval recv_tv;
 recv_tv.tv_sec = 5;
 recv_tv.tv_sec = 0;
 // 意味ない?
 setsockopt( sock, SOL_SOCKET, SO_RCVTIMEO, &recv_tv, sizeof(recv_tv) );

 struct sockaddr_in sa;
 memset( &sa, 0, sizeof( sa ) );
 // sa.sin_len    = sizeof( sa );
 sa.sin_family = AF_INET;
 sa.sin_port   = htons( 4649 );
 sa.sin_addr.s_addr = inet_addr( "127.0.0.1" ); // localhostからのみ

 struct sockaddr_in client;
 int client_len = sizeof( client );

 bind( sock, (struct sockaddr *)&sa, sizeof( sa ) );

 listen( sock, 1 );

 int client_sock = accept( sock, (struct sockaddr *)&client,
(socklen_t *)&client_len );

 char buff[ 1024 ];
 read( client_sock, buff, 1024 );
 printf("%s\n", buff );

 char *msg = "bye";
 write( client_sock, msg, strlen(msg) + 1 );

 return 0;
}
  • client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int
main (int argc, char *argv[])
{
  int sock = socket( PF_INET, SOCK_STREAM, 0 );
 
  struct timeval send_tv;
  send_tv.tv_sec  = 2;
  send_tv.tv_usec = 0;
  setsockopt( sock, SOL_SOCKET, SO_SNDTIMEO, &send_tv, sizeof(send_tv) );
 
  struct timeval recv_tv;
  recv_tv.tv_sec = 5;
  recv_tv.tv_sec = 0;
  setsockopt( sock, SOL_SOCKET, SO_RCVTIMEO, &recv_tv, sizeof(recv_tv) );
 
  struct sockaddr_in sa;
  memset( &sa, 0, sizeof( sa ) );
  // sa.sin_len    = sizeof( sa );
  sa.sin_family = AF_INET;
  sa.sin_port   = htons( 4649 );
  sa.sin_addr.s_addr = inet_addr( "127.0.0.1" );
  connect( sock, (struct sockaddr *)&sa, sizeof( sa ) );
 
  char input[ 1024 ];
  printf("Input message> ");
  scanf("%s", input);
 
  write( sock, input, strlen( input ) + 1 );
 
  char buff[ 1024 ];
  read( sock, buff, 1024 );
  printf("%s\n", buff );
 
  return 0;
}

実験してみる.

端末1でサーバを起動

% ./server

端末2でクライアントを起動

% ./client
Input message>

いまだ! iptablesを設定

# iptables -A INPUT -p tcp -s 127.0.0.1 --dport 4649 -j DROP

いまだ! clientでメッセージを入力

% ./client
Input message> hoge

そして設定したSO_RCVTIMEO通りに動くか確認.ちーん.5秒でも1秒でも返ってきませんでした.なんでー??

というわけで,今回は疲れたのでwriteの方は確認していないけれど,おそらく同じような挙動になると思われる.うーむ.

どこかのページでSO_RCVTIMEOの実装はPOSIXではあくまで推奨になっているので環境によっては実装されてないよ,Linuxでは実装されてないよーという書き込みを見たのだけれど,見つからなくなっていた...まぼろし?