サーバーの作り方


これまで説明してきたネットワーク通信の方法を踏まえて、 サーバーとなるデーモンのプログラムの作り方に関連した色々な技術を紹介します。 先に紹介した inettcps.cc を基本にいろいろ 改造を加えていきます。まずこれの基本部分は以下のとおりでした。

int s = accept( sb, NULL, NULL ); char rmsg[64], smsg[64]; int len = recv( s, rmsg, sizeof(rmsg), 0 ); rmsg[len]='\0'; len = snprintf( smsg, sizeof(smsg), "Server recv: %s", rmsg ); len = send( s, smsg, len, 0 ); close(s);
inettcpsを走らせて、別の端末から telnet コマンドで以下のようにして inettcpsに接続して Hello! [return] と打つと以下のようなり 接続が終了して、同時にサーバーが終了します。
/home/naoki> telnet localhost 12345 Trying 127.0.0.1... Connected to localhost.my.domain. Escape character is '^]'. Hello! Server received : Hello! Connection closed by foreign host. /home/naoki>
これを改造して、何度もサーバーに接続できるようにします。 そのためには accept から close までをループの中に組み込みます。
while(1){ int s = accept( sb, NULL, NULL ); char rmsg[64], smsg[64]; len = recv( s, rmsg, sizeof(rmsg), 0 ); rmsg[len]='\0'; len = snprintf( smsg, sizeof(smsg), "Server recv: %s", rmsg ); len = send( s, smsg, len, 0 ); close(s); }
これで少しデーモンらしくなりましたが、まだ同時には1つの クライアントしか受け付けられません。


次に、同時に複数のクライアントと接続できるように改造します。 同時に複数のクライアントと応対するためには、プロセスを分岐して 個々のクライアントにひとつのプロセスを対応させるようにします。 そのようなサーバーを作るにあたっての要点は以下のとおりです。

これを実装したプロセス分岐型のサーバーデーモンプログラム server.cc を以下に載せます。server.cc のソース
// 複数クライアントを同時に取り扱えるデーモンの例 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h> #include "integnet.h" // 接続先のポート #define PORT 12345 // 子プロセスが終了すると呼び出されるシグナルハンドラ void Wait( int ) { // 終了したすべての子プロセスの回収 while( waitpid(-1,NULL,WNOHANG) > 0 ); // シグナルハンドラの再設定 signal( SIGCHLD, Wait ); } int main( void ) { int sb = socket( AF_INET, SOCK_STREAM, 0 ); sockaddr_in addr = SOCKADDR_IN_INIT( AF_INET, htons(PORT), InAddr(htonl(INADDR_ANY)) ); if( bind( sb, (sockaddr*)&addr, sizeof(addr) ) < 0 ){ perror("bind"); exit(1); } // 最大同時4回線の接続を許可する設定 listen( sb, 4 ); // 子プロセスの終了時にシグナルハンドラが動くように設定 signal( SIGCHLD, Wait ); // サーバーループ while(1){ int s; // acceptで待機、エラーの場合はループ冒頭に戻る if( (s=accept( sb, NULL, NULL )) < 0 ) continue; // 子プロセスを作る if( fork() ){ //---- 親プロセスの作業 close(s); // クライアントへのソケットは要らない continue; // ループ冒頭に戻る } //---- 子プロセスの作業 close(sb); // 基本ソケットは要らない // データの受信と送信 char rmsg[64], smsg[64]; int len; len = recv( s, rmsg, sizeof(rmsg), 0 ); rmsg[len] = '\0'; len = snprintf( smsg, sizeof(smsg), "Server recv: %s", rmsg ); len = send( s, smsg, len, 0 ); close(s); exit(0); // 子プロセスはここで終了 } close(sb); return 0; }

このプログラムについて補足説明をしておきます。 Wait関数は SIGCHLDのシグナルが親プロセスに届いたときに 自動的に実行されるシグナルハンドラにします。終了した子プロセスがすべてを 回収するように waitpid関数をループで回しています。

Wait関数をSIGCHLDのシグナルハンドラに登録するために signal関数をプログラム冒頭で実行します。シグナルハンドラは 一旦実行されるとその登録が解約されてしまうので、シグナルハンドラ中で 自分自身で再び登録しています。

listenでは4回線まで同時接続できるように指示しています。 OSが4回線分の情報を扱うテーブルを用意してくれるのです。

acceptでクライアントと接続して、クライアント情報を表示したらすぐに forkでプロセスを分岐します。子プロセスにクライアントの応対を任せて、 親プロセスは次のクライアントを待つことにします。この親プロセスには もはや先のクライアントへのソケットは不要ですのでcloseします。 逆に子プロセスにとっては親から引き継いだベースソケットはもう要らないので closeします。

親は普通waitで子プロセスが終了するのを待つのですが、先にSIGCHLDで Wait関数を登録しておいたのでその明示的に wait を行う必要が無くなりました。 acceptで待っている間に子プロセスが終了すると、自動的に Wait関数に制御が 移り、終了した子プロセスを回収して、再び accept に戻ってくれます。 ただし、Linuxなどの非BSD系OSでは acceptに制御が戻った時に、accept関数が EINTR のエラーを返してしまうものがあります。その対策として acceptがエラーになったら continue で再度 accept を実行するようにしています。


目次

Copyright(C) by Naoki Watanabe. Jan 1st, 2001.
渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp