このエントリーをはてなブックマークに追加
スポンサーリンク

ソケット入門(3)

1 2 3

タイムアウトの設定

前ページでは、selectにタイムアウトを設定する事で、クライアント側がソケットをクローズせずにフリーズしてしまった場合に、コネクションを維持したままにならずに、サーバー側から切断を切る事ができるようになりました。

このページでは、accept入力待ちの時にタイムアウトを設定してみましょう。

setsockop関数

setsockop関数でソケットに関するさまざまな設定を行う事ができます。さまざまなといっても私もあまりよくわかっておりませんで、今回使うタイムアウトに関する部分だけ説明します。

書式
int setsockopt(ソケットディスクリプタ, ソケットレベル, 設定する項目, 設定する値, 設定する値のバイト数)

ソケットディスクリプタ
事前にsocket関数によってソケットを作成し、socket関数のリターンコードをソケットディスクリプタとしてセットします。

ソケットレベル
ソケットレベルを指定します。ところでソケットレベルって何でしょう?ここで、JM Projectのマニュアルを見てみます。

ソケットAPI層でオプションを操作する為には、levelをSOL_SOCKETに指定する。他の全ての層でオプションを操作する為には、与えられたオプションの制御主体となるプロトコルのプロトコル番号を指定する。例えば、オプションが TCP プロトコルで解釈されるべきことを指示するには、levelにTCPのプロトコル番号を指定しなければならない。

意味はよくわかりませんが、これから作るプログラムがAPI層なのは間違いないので、SOL_SOCKETと指定します。

設定する項目
setsocketオプションでは、ソケットに関するさまざまな設定をする事ができますが、そのうちのどれを設定するかを指定します。今回は、SO_RCVTIMEOというソケット受信時のタイムアウトを設定する機能を使います。今回は使いませんが、送信時のタイムアウトの設定には、SO_SNDTIMEOを使います。

設定する値
設定する値の型は設定する項目によってさまざまですが、ここではどの項目であっても第4引数はchar*で指定するように統一されていますので、(char*)とキャストした上で記述します。

設定する値のバイト数
設定する値のバイト数を指定します。sizeof関数を使って第4引数の長さを指定します。

タイムアウトを設定する場合、値の所でint型を指定している文献をよくみかけますが、こちらでテストした環境(CentOS4.7のcc)ではint型を指定するとInvalid parameterのエラーになってしまうため、ここではtimeval型で定義しています。
/* タイムアウト時間を設定 */
socket_timeout.tv_sec  = D_SOCKET_TIMEOUT;
socket_timeout.tv_usec = 0;
printf("[%d]\n",sizeof(socket_timeout));
if ((ret=setsockopt(s_listen, SOL_SOCKET, SO_RCVTIMEO, 
       (char*)&socket_timeout, sizeof(socket_timeout)))!=0){
        perror(NULL);
        exit(EXIT_FAILURE);
}

accept関数

accept関数には、select関数のような明示的なタイムアウトの指定がありません。事前にsetsockopt関数によって設定しておいたタイムアウト時間になると、自動的にaccept関数から抜けます。

/* acceptを試みる */
aiteaddrlen=sizeof(aite);
printf("[P]Connection waiting ....\n");
s = accept(s_listen, (struct sockaddr *)&aite , &aiteaddrlen);

システム割り込み

acceptにタイムアウトを設定した場合、タイムアウト以外にもacceptから抜けてしまう可能性が生じます。

JMのsocket関数に関する説明に以下の記載があります。

以下のインターフェイスは、 SA_RESTART を使っているどうかに関わらず、シグナルハンドラにより割り込まれた後、再スタートすることは決してない。これらは、シグナルハンドラにより割り込まれると、常にエラー EINTR で失敗する。
setsockopt(2) を使ってタイムアウトが設定されているソケットインターフェース: accept(2), recv(2), recvfrom(2), recvmsg(2) で受信タイムアウト (SO_RCVTIMEO) が設定されている場合と、 connect(2), send(2), sendto(2), sendmsg(2) で送信タイムアウト (SO_SNDTIMEO) が設定されている場合。


つまり、シグナルによって割り込みがかかった場合、タイムアウトしていなくともacceptから抜けてしまうという事です。 親プロセスは、クライアントからの接続を待つだけでなく、終了した子プロセスを適切に埋葬する役割を持っています。しかし、acceptにタイムアウトを設定する事で、接続待ちの途中で子プロセスの埋葬の処理が走った場合に、そのまま接続待ちをやめてしまうわけです。

acceptが終了した際に、errnoにエラーコードがセットされます。この時に、EINTRが設定されていた場合は、割り込み処理が走ったという意味になりますので、処理を終了させずにそのままacceptに復帰させなければなりません。
while (1) {

        /* acceptを試みる */
        aiteaddrlen=sizeof(aite);
        printf("[P]Connection waiting ....\n");
        s = accept(s_listen, (struct sockaddr *)&aite , &aiteaddrlen);

        /* システム割り込みか? */
        if (s==-1 && errno==EINTR) {
                printf("[P]System interrupt.\n");
                continue;
        }

        /*  accept失敗  */
        if (s == -1) {
                fprintf(stderr,"[P]Accept faild.\n");
                perror(NULL);
                exit(EXIT_FAILURE);
        }

        break;
}

サンプルプログラム

以上の事をふまえたサンプルプログラムです。 Googleで検索したところ、いかにもこのサイトのものを元に作ったというものが出回っているようですが、 1ページ目、2ページ目で掲載しているサンプルは色々と問題も多く、このページで説明した事以外にも色々と修正しています。

スペルミスの修正
Reveived→Received

金田一少年の事件簿に、手品のトリックを書いたノートが出てくる話があり、その中に盗み見してマネすると死ぬ手品という罠がしかけてあったりしましたが、 このサイトのサンプルもあえてスペルミスを残しておいて、コピペすると恥をかくという罠をしかけておいた方が良かったかもしれません。

サブルーチンの宣言をmain()の前に追加
Redhat9についてきたccコンパイラではサブルーチンを後に宣言してもエラーにならないのですが、 CentOS4についてくるコンパイラでは後に宣言するとエラーになるようなので、頭にサブルーチンの宣言を追加しました。

dispipサブルーチンに注釈を追加
このサブルーチンではエラーが発生しませんので、リターンコードはvoidでも良かったのですが、サブルーチンは将来的な拡張によってエラーが返る仕様になる可能性があるため、エラーだった場合のリターンコードも使わないまでも一応は定義しています。

こういう作り方は実に美しくないのですが、納期直前の修羅場になって突然仕様変更が入った場合に助かる事もあります。

エラー発生時にexit(EXIT_FAILURE)で抜けるようにした
前バージョンではエラー発生時にreturn -1としていましたが、これをexit(EXIT_FAILURE)とする事で互換性を高くする事ができるだけでなく、exit関数を使うことによって開いていたディスクリプタを適切に閉じてから終了させる事ができるようになります。将来的にデーターベースと連動させる仕様になった場合でも、エラー時にデーターベースとの接続を閉じてくれるようになり便利です。

ダウンロード
サーバープログラム server3.c
クライアントプログラム client.c
上記をコンパイルするための Makefile

サンプルプログラムのカスタマイズ

バインドするIPを変更する
ソケットにバインドするIPアドレスは、hostnameで設定されたホスト名を逆引きしたものを使っています。そのため、/etc/hostsで自ホスト名が127.0.0.1に設定されているか、ローカルIPに設定されているか、グローバルIPに設定されているかでバインドされるIPアドレスが異なってしまいます。

これを固定値にする場合、
gethostname(hostname,256);
のところを、gethostnameではなくクライアントプログラムでやっているように、
strcpy(hostname, D_HOSTNAME);
みたいに固定値をセットする事でIPアドレスを固定させる事ができます。

ポート番号を変更する
これはdefineで定義しているので簡単です。
#define PORT (u_short)9000
ここを変更するだけです。

タイムアウト秒を変更する
これもdefineで定義しています。
#define D_SOCKET_TIMEOUT 10;
ここを変更するだけです。

クライアントの接続するIPを変更する
これもdefineで定義しています。
#define D_HOSTNAME "127.0.0.1"
ここを変更するだけです。

本格的に業務で使う場合
このサンプルはかなり短時間で作ったもので、検証もほとんどしていないため、業務で使うのはやめておいた方が無難です。 このサンプルはあくまで参考程度にして、1から作り直す方が良いかと思います。 っていうか、悪い事は言わないから、このサンプルを改変して業務に使うのはやめましょう。
1 2 3
スポンサーリンク