C 言語で echo サーバを作ってみよう (2)

前へ << C 言語で echo サーバを作ってみよう (1) C言語で ftp クライアントを作ってみよう (1) >> 次へ

select によるマルチスレッドサーバ

C 言語において select の使い方を説明します。

echo-server-select.c

  117: int
  118: main(){
  119:   fd_set target_fds;
  120:   fd_set org_target_fds;
  121:   int sock_optval = 1;
  122:   int port = 5000;
  123:                                 /* リスニングソケットを作成 */
  124:   listening_socket = socket(AF_INET, SOCK_STREAM, 0);
  125: 
  126:                                 /* ソケットオプション設定 */
  127:   if ( setsockopt(listening_socket, SOL_SOCKET, SO_REUSEADDR,
  128:                   &sock_optval, sizeof(sock_optval)) == -1 ){
  129:     perror("setsockopt");
  130:     exit(1);
  131:   }
  132:                                 /* アドレスファミリ・ポート番号・IPアドレス設定 */
  133:   sin.sin_family = AF_INET;
  134:   sin.sin_port = htons(port);
  135:   sin.sin_addr.s_addr = htonl(INADDR_ANY);
  136: 
  137:   if ( bind(listening_socket, (struct sockaddr *)&sin, sizeof(sin)) < 0 ){
  138:     perror("bind");
  139:     exit(1);
  140:   }
  141: 
  142:   if ( listen(listening_socket, SOMAXCONN) == -1 ){
  143:     perror("listen");
  144:     exit(1);
  145:   }
  146:   printf("ポート %d を見張ります。\n", port);
まずは main 関数から。これまでのように
  • socket でソケット生成
  • bind でポート 5000 番にソケット割り付け
  • listen でコネクション受け付け
を行います。次に select(2) に関する部分です。
  119:   fd_set target_fds;
  120:   fd_set org_target_fds;
fd_set というのは select(2) に渡す構造体です。 どのディスクリプタを監視対象にするかを指定します。 select(2) は渡された fd_set 構造体を書き換えてしまうので、 org_target_fds にデータをセットし、select(2) を呼ぶ前に org_target_fdstarget_fds にコピーすることにします。
  148:                                 /* 監視対象のディスクリプタ一覧をゼロクリア */
  149:   FD_ZERO(&org_target_fds);
  150:                                 /* リスニングソケットを監視対象に追加 */
  151:   FD_SET(listening_socket, &org_target_fds);
まずは FD_ZERO でゼロクリアします。 これによりいずれのディスクリプタも監視対象にしないことになります。 FD_ZERO は FreeBSD 4.2R では /usr/include/sys/types.h で
#define FD_ZERO(p) bzero(p, sizeof(*(p)))
と定義されているマクロです。次に FD_SET で リスニングソケットのビットを立てます。これにより リスニングソケットが select(2) の監視対象となります。
ここから先は、全て select(2) でディスクリプタをチェックしてから 行います。
  153:   while (1){
  154:     int i;
  155:     time_t now_time;
  156:     struct timeval waitval;     /* select に待ち時間を指定するための構造体 */
  157:     waitval.tv_sec  = 2;        /* 待ち時間に 2.500 秒を指定 */
  158:     waitval.tv_usec = 500;
  159: 
  160:                                 /* org_target_fds を target_fds にコピー */
  161:     memcpy(&target_fds, &org_target_fds, sizeof(org_target_fds));
  162: 
  163:     select(FD_SETSIZE, &target_fds, NULL, NULL, &waitval);
waitval に 2.500 を指定します。select(2) を呼んで、 どのソケットも読み出し可能にならない場合は、2.5 秒経過すると select(2) から戻ることになります。

memcpy(3) で org_target_fds から target_fds にコピーし、 select(2) を呼びます。select(2) の第一引数には何個分のディスクリプタを 監視対象にするかを指定します。例えば第一引数に 5 を指定すると、 ディスクリプタ 0番〜4番の 5個のディスクリプタ (なおかつ FD_SET で 監視対象に指定されているもの) についてチェックが行われます。

ここでは FD_SETSIZE を指定していますが、これは多くの環境では

#define FD_SETSIZE 1024
となっています。これは select(2) が扱えるディスクリプタの最大数です。 これでは毎回 0〜1023 番のディスクリプタ (そのうちFD_SET されているもの) が チェックされます。CPU 資源を無駄使いになりますので、自前で最大数を 指定するのがよいのですが、ここではサンプルということで手抜きをしています。

なお、select(2) を呼ぶと waitvaltarget_fds が 更新されてしまうので、毎ループごとに値をセットし直す必要があります。

  166:     for ( i=0 ; i<FD_SETSIZE ; i++ ){
  167:       if ( FD_ISSET(i, &target_fds) ){
  168:         printf("ディスクリプタ %d 番が読み込み可能です。\n", i);
  169: 
  170:         if ( i == listening_socket ){
  171:           int new_sock;
  172:                                 /* 新しいクライアントがやってきた */
  173:           new_sock = accept_new_client(i);
  174:           if ( new_sock != -1 ){
  175:                                 /* 監視対象に新たなソケットを追加 */
  176:             FD_SET(new_sock, &org_target_fds);
  177:           }
  178:         } else {
  179:           int read_size;
  180:                                 /* 接続済みソケットからデータが送信されてきた */
  181:           read_size = read_and_reply(i);
  182: 
  183:           if ( read_size == -1 || read_size == 0 ){
  184:                                 /* 切断したソケットを監視対象から削除 */
  185:             FD_CLR(i, &org_target_fds);
  186:           }
  187:         }
  188:       }
  189:     }
各ディスクリプタに対して FD_ISSET を使って 読み込みが可能かどうかを調べます。FD_ISSET が真を返せば 読み込み可能であることがわかります。

ディスクリプタがリスニングソケットであれば、

新たなクライアントが やってきたということなので、下請け関数 accept_new_client を呼びます。 accept_new_client は accept(2) を行い、新たなクライアントとの 接続を確立し、新クライアントのソケット (ソケットディスクリプタ) を返します。 これを受け取り、FD_SET で監視対象に追加します。
リスニングソケットでなければ (クライアントと結ばれているソケットが読み出し可能なら)
クライアントがメッセージを送ってきたということなので、 下請け関数 read_and_reply を呼び出します。 この関数はソケットから read(2) してクライアントからのメッセージを受け取り、 クライアントにそのままメッセージを送り返します。 read_and_reply は read(2) の戻り値をそのまま返します。 もし戻り値が 0 か -1 なら、コネクション切断かエラー発生ということなので、 FD_CLR を使って監視対象から削除します。
なお、下請け関数 accept_new_clientread_and_reply は 以下のようになっています。
   29: /*-----------------------------------------------------
   30:   引数でリスニングソケットを受け取り、accept し、
   31:   client_info に新しいクライアントの情報を登録する。
   32:   戻り値は新しいクライアントのソケットディスクリプタ。
   33:   ただしエラー発生時は -1 を返す。
   34:   -----------------------------------------------------*/
   35: int
   36: accept_new_client(int sock){
   37:   int len;
   38:   int new_socket;
   39:   struct hostent *peer_host;
   40:   struct sockaddr_in peer_sin;
   41:   
   42:   len = sizeof(sin);
   43:   new_socket = accept(listening_socket, (struct sockaddr *)&sin, &len);
   44: 
   45:   if ( new_socket == -1 ){
   46:     perror("accept");
   47:     exit(1);
   48:   }
   49: 
   50:   if ( new_socket > FD_SETSIZE-1 ){
   51:     return -1;
   52:   }
   53:                                 /* ここから先はデバッグ用の情報取得 */
   54:   len = sizeof(peer_sin);
   55:   getpeername(new_socket,
   56:               (struct sockaddr *)&peer_sin, &len);
   57:   
   58:   peer_host = gethostbyaddr((char *)&peer_sin.sin_addr.s_addr,
   59:                             sizeof(peer_sin.sin_addr), AF_INET);
   60: 
   61:                                 /* ホスト名 */
   62:   strncpy(client_info[new_socket].hostname, peer_host->h_name,
   63:           sizeof client_info[new_socket].hostname);
   64:                                 /* IP アドレス */
   65:   strncpy(client_info[new_socket].ipaddr, inet_ntoa(peer_sin.sin_addr),
   66:           sizeof client_info[new_socket].ipaddr);
   67:                                 /* ポート番号 */
   68:   client_info[new_socket].port = ntohs(peer_sin.sin_port);
   69:                                 /* 現在時刻を最終アクセス時刻として記録しておく */
   70:   time(&client_info[new_socket].last_access);
   71:   
   72:   printf("接続: %s (%s) ポート %d  ディスクリプタ %d 番\n",
   73:          client_info[new_socket].hostname,
   74:          client_info[new_socket].ipaddr,
   75:          client_info[new_socket].port,
   76:          new_socket);
   77:   return new_socket;
   78: }
   81: /*-----------------------------------------------------
   82:   引数でソケットディスクリプタを受け取り、そのソケットから
   83:   read(2) で文字列を読み込み、文字列をそのままクライアントに
   84:   送信する。read(2) の戻り値をそのまま返す。
   85:   -----------------------------------------------------*/
   86: int
   87: read_and_reply(int sock){
   88:   int read_size;
   89:   char buf[BUF_LEN];
   90: 
   91:   read_size = read(sock, buf, sizeof(buf)-1);
   92:   
   93:   if ( read_size == 0 || read_size == -1 ){
   94:     printf("%s (%s) ポート %d  ディスクリプタ %d 番からの接続が切れました。\n",
   95:            client_info[sock].hostname,
   96:            client_info[sock].ipaddr,
   97:            client_info[sock].port,
   98:            sock);
   99:     close(sock);
  100:     client_info[sock].last_access = 0;
  101:   } else {
  102:                                 /* 文字列終端を \0 で terminate */
  103:     buf[read_size] = '\0';
  104:     printf("%s (%s) ポート %d  ディスクリプタ %d 番からのメッセージ: %s", 
  105:            client_info[sock].hostname,
  106:            client_info[sock].ipaddr,
  107:            client_info[sock].port,
  108:            sock,
  109:            buf);
  110:     write(sock, buf, strlen(buf));
  111:     time(&client_info[sock].last_access);
  112:   }
  113:   return read_size;
  114: }
この下請け関数の中では、デバッグ表示用にクライアントの情報を保持しておく以下のような 構造体を使用しています。
   17: typedef struct CLIENT_INFO {
   18:   char hostname[BUF_LEN];       /* ホスト名 */
   19:   char ipaddr[BUF_LEN];         /* IP アドレス */
   20:   int port;                     /* ポート番号 */
   21:   time_t last_access;           /* 最終アクセス時刻 */
   22: } CLIENT_INFO;
   23: 
   24: CLIENT_INFO client_info[FD_SETSIZE];
例えばディスクリプタ4番の情報は
  • client_info[4].hostname (ホスト名)
  • client_info[4].ipaddr (IP アドレス)
  • client_info[4].port (ポート番号)
  • client_info[4].hostname (最終アクセス時刻)
のように扱います。

タイムアウト処理

せっかく select(2) を使っているので、一つ機能を追加しましょう。 一定時間アクセスがないクライアントとのコネクションを切断してしまう タイムアウト機能です。

下請け関数 accept_new_clientread_and_reply で 最終アクセス時刻を client_info に記録しておきます。

   70:   time(&client_info[new_socket].last_access);
  111:     time(&client_info[sock].last_access);
そして select(2) から戻った後、全ソケットの最終更新時刻をチェックします。
  191:     time(&now_time);          /* 現在時刻を取得 */
  192: 
  193:     for ( i=0 ; i<FD_SETSIZE ; i++ ){
  194:                                 /* 監視対象でないソケットはスキップ */
  195:       if ( ! FD_ISSET(i, &org_target_fds) )  continue;
  196:                                 /* リスニングソケットはスキップ */
  197:       if ( i == listening_socket ) continue;
  198:       
  199:       if ( now_time-10 > client_info[i].last_access ){
  200:         printf("%s (%s) ポート %d  ディスクリプタ %d 番から10秒以上アクセスがありません。切断します。\n",
  201:                client_info[i].hostname,
  202:                client_info[i].ipaddr,
  203:                client_info[i].port,
  204:                i);
  205:         close(i);
  206:                                 /* 切断したソケットを監視対象から削除 */
  207:         FD_CLR(i, &org_target_fds);
  208:       }
  209:     }
FD_ISSET を使って監視対象でないソケットはスキップします。 また、リスニングソケットはタイムアウトの対象外にしたいので、これも スキップします。 そして最終アクセス時刻 client_info[ディスクリプタ].last_access が 現在時刻より10秒以上前ならタイムアウトしたものとみなして、 close(2) してソケットをクローズし、FD_CLR で監視対象から外します。

実行結果

このプログラムを実行した結果は以下の通りです。telnet コマンドを同時に2つ実行して、 片方をタイムアウト、もう片方はクライアント側からコネクションを切断した結果です (黄色の文字は注釈文で、実際は表示されません)。
ポート 5000 を見張ります。
新たなクライアント (=クライアント A) が接続してきた
ディスクリプタ 3 番が読み込み可能です。
接続: localhost (127.0.0.1) ポート 2716  ディスクリプタ 4 番
クライアント A からのメッセージ
ディスクリプタ 4 番が読み込み可能です。
localhost (127.0.0.1) ポート 2716  ディスクリプタ 4 番からのメッセージ: I'm client A.
新たなクライアント (=クライアント B) が接続してきた
ディスクリプタ 3 番が読み込み可能です。
接続: localhost (127.0.0.1) ポート 2718  ディスクリプタ 5 番
クライアント A からのメッセージ
ディスクリプタ 4 番が読み込み可能です。
localhost (127.0.0.1) ポート 2716  ディスクリプタ 4 番からのメッセージ: Hello..
クライアント B からのメッセージ
ディスクリプタ 5 番が読み込み可能です。
localhost (127.0.0.1) ポート 2718  ディスクリプタ 5 番からのメッセージ: I'm client B.
クライアント A がタイムアウト
localhost (127.0.0.1) ポート 2716  ディスクリプタ 4 番から10秒以上アクセスがありません。切断します。
クライアント B がコネクション切断
ディスクリプタ 5 番が読み込み可能です。
localhost (127.0.0.1) ポート 2718  ディスクリプタ 5 番からの接続が切れました。

全ソース

このプログラムのソース全文は以下の通りです。

echo-server-select.c

    1: /* $Id: echo-server-select.c,v 1.2 2005/06/11 20:25:10 68user Exp $ */
    2: 
    3: #include <stdio.h>
    4: #include <sys/types.h>
    5: #include <sys/socket.h>
    6: #include <time.h>
    7: #include <netdb.h>
    8: #include <string.h>
    9: #include <sys/time.h>
   10: #include <unistd.h>
   11: #include <netinet/in.h>
   12: #include <arpa/inet.h>
   13: 
   14: #define BUF_LEN  256  /* バッファのサイズ */
   15: 
   16:                                 /* クライアントの情報を保持する構造体 */
   17: typedef struct CLIENT_INFO {
   18:   char hostname[BUF_LEN];       /* ホスト名 */
   19:   char ipaddr[BUF_LEN];         /* IP アドレス */
   20:   int port;                     /* ポート番号 */
   21:   time_t last_access;           /* 最終アクセス時刻 */
   22: } CLIENT_INFO;
   23: 
   24: CLIENT_INFO client_info[FD_SETSIZE];
   25: 
   26: int    listening_socket;
   27: struct sockaddr_in sin;
   28: 
   29: /*-----------------------------------------------------
   30:   引数でリスニングソケットを受け取り、accept し、
   31:   client_info に新しいクライアントの情報を登録する。
   32:   戻り値は新しいクライアントのソケットディスクリプタ。
   33:   ただしエラー発生時は -1 を返す。
   34:   -----------------------------------------------------*/
   35: int
   36: accept_new_client(int sock){
   37:   int len;
   38:   int new_socket;
   39:   struct hostent *peer_host;
   40:   struct sockaddr_in peer_sin;
   41:   
   42:   len = sizeof(sin);
   43:   new_socket = accept(listening_socket, (struct sockaddr *)&sin, &len);
   44: 
   45:   if ( new_socket == -1 ){
   46:     perror("accept");
   47:     exit(1);
   48:   }
   49: 
   50:   if ( new_socket > FD_SETSIZE-1 ){
   51:     return -1;
   52:   }
   53:                                 /* ここから先はデバッグ用の情報取得 */
   54:   len = sizeof(peer_sin);
   55:   getpeername(new_socket,
   56:               (struct sockaddr *)&peer_sin, &len);
   57:   
   58:   peer_host = gethostbyaddr((char *)&peer_sin.sin_addr.s_addr,
   59:                             sizeof(peer_sin.sin_addr), AF_INET);
   60: 
   61:                                 /* ホスト名 */
   62:   strncpy(client_info[new_socket].hostname, peer_host->h_name,
   63:           sizeof client_info[new_socket].hostname);
   64:                                 /* IP アドレス */
   65:   strncpy(client_info[new_socket].ipaddr, inet_ntoa(peer_sin.sin_addr),
   66:           sizeof client_info[new_socket].ipaddr);
   67:                                 /* ポート番号 */
   68:   client_info[new_socket].port = ntohs(peer_sin.sin_port);
   69:                                 /* 現在時刻を最終アクセス時刻として記録しておく */
   70:   time(&client_info[new_socket].last_access);
   71:   
   72:   printf("接続: %s (%s) ポート %d  ディスクリプタ %d 番\n",
   73:          client_info[new_socket].hostname,
   74:          client_info[new_socket].ipaddr,
   75:          client_info[new_socket].port,
   76:          new_socket);
   77:   return new_socket;
   78: }
   79: 
   80: 
   81: /*-----------------------------------------------------
   82:   引数でソケットディスクリプタを受け取り、そのソケットから
   83:   read(2) で文字列を読み込み、文字列をそのままクライアントに
   84:   送信する。read(2) の戻り値をそのまま返す。
   85:   -----------------------------------------------------*/
   86: int
   87: read_and_reply(int sock){
   88:   int read_size;
   89:   char buf[BUF_LEN];
   90: 
   91:   read_size = read(sock, buf, sizeof(buf)-1);
   92:   
   93:   if ( read_size == 0 || read_size == -1 ){
   94:     printf("%s (%s) ポート %d  ディスクリプタ %d 番からの接続が切れました。\n",
   95:            client_info[sock].hostname,
   96:            client_info[sock].ipaddr,
   97:            client_info[sock].port,
   98:            sock);
   99:     close(sock);
  100:     client_info[sock].last_access = 0;
  101:   } else {
  102:                                 /* 文字列終端を \0 で terminate */
  103:     buf[read_size] = '\0';
  104:     printf("%s (%s) ポート %d  ディスクリプタ %d 番からのメッセージ: %s", 
  105:            client_info[sock].hostname,
  106:            client_info[sock].ipaddr,
  107:            client_info[sock].port,
  108:            sock,
  109:            buf);
  110:     write(sock, buf, strlen(buf));
  111:     time(&client_info[sock].last_access);
  112:   }
  113:   return read_size;
  114: }
  115: 
  116: 
  117: int
  118: main(){
  119:   fd_set target_fds;
  120:   fd_set org_target_fds;
  121:   int sock_optval = 1;
  122:   int port = 5000;
  123:                                 /* リスニングソケットを作成 */
  124:   listening_socket = socket(AF_INET, SOCK_STREAM, 0);
  125: 
  126:                                 /* ソケットオプション設定 */
  127:   if ( setsockopt(listening_socket, SOL_SOCKET, SO_REUSEADDR,
  128:                   &sock_optval, sizeof(sock_optval)) == -1 ){
  129:     perror("setsockopt");
  130:     exit(1);
  131:   }
  132:                                 /* アドレスファミリ・ポート番号・IPアドレス設定 */
  133:   sin.sin_family = AF_INET;
  134:   sin.sin_port = htons(port);
  135:   sin.sin_addr.s_addr = htonl(INADDR_ANY);
  136: 
  137:   if ( bind(listening_socket, (struct sockaddr *)&sin, sizeof(sin)) < 0 ){
  138:     perror("bind");
  139:     exit(1);
  140:   }
  141: 
  142:   if ( listen(listening_socket, SOMAXCONN) == -1 ){
  143:     perror("listen");
  144:     exit(1);
  145:   }
  146:   printf("ポート %d を見張ります。\n", port);
  147: 
  148:                                 /* 監視対象のディスクリプタ一覧をゼロクリア */
  149:   FD_ZERO(&org_target_fds);
  150:                                 /* リスニングソケットを監視対象に追加 */
  151:   FD_SET(listening_socket, &org_target_fds);
  152: 
  153:   while (1){
  154:     int i;
  155:     time_t now_time;
  156:     struct timeval waitval;     /* select に待ち時間を指定するための構造体 */
  157:     waitval.tv_sec  = 2;        /* 待ち時間に 2.500 秒を指定 */
  158:     waitval.tv_usec = 500;
  159: 
  160:                                 /* org_target_fds を target_fds にコピー */
  161:     memcpy(&target_fds, &org_target_fds, sizeof(org_target_fds));
  162: 
  163:     select(FD_SETSIZE, &target_fds, NULL, NULL, &waitval);
  164: 
  165:                                 /* ソケットが読み出し可能か順にチェック */
  166:     for ( i=0 ; i<FD_SETSIZE ; i++ ){
  167:       if ( FD_ISSET(i, &target_fds) ){
  168:         printf("ディスクリプタ %d 番が読み込み可能です。\n", i);
  169: 
  170:         if ( i == listening_socket ){
  171:           int new_sock;
  172:                                 /* 新しいクライアントがやってきた */
  173:           new_sock = accept_new_client(i);
  174:           if ( new_sock != -1 ){
  175:                                 /* 監視対象に新たなソケットを追加 */
  176:             FD_SET(new_sock, &org_target_fds);
  177:           }
  178:         } else {
  179:           int read_size;
  180:                                 /* 接続済みソケットからデータが送信されてきた */
  181:           read_size = read_and_reply(i);
  182: 
  183:           if ( read_size == -1 || read_size == 0 ){
  184:                                 /* 切断したソケットを監視対象から削除 */
  185:             FD_CLR(i, &org_target_fds);
  186:           }
  187:         }
  188:       }
  189:     }
  190: 
  191:     time(&now_time);          /* 現在時刻を取得 */
  192: 
  193:     for ( i=0 ; i<FD_SETSIZE ; i++ ){
  194:                                 /* 監視対象でないソケットはスキップ */
  195:       if ( ! FD_ISSET(i, &org_target_fds) )  continue;
  196:                                 /* リスニングソケットはスキップ */
  197:       if ( i == listening_socket ) continue;
  198:       
  199:       if ( now_time-10 > client_info[i].last_access ){
  200:         printf("%s (%s) ポート %d  ディスクリプタ %d 番から10秒以上アクセスがありません。切断します。\n",
  201:                client_info[i].hostname,
  202:                client_info[i].ipaddr,
  203:                client_info[i].port,
  204:                i);
  205:         close(i);
  206:                                 /* 切断したソケットを監視対象から削除 */
  207:         FD_CLR(i, &org_target_fds);
  208:       }
  209:     }
  210:   }
  211:   close(listening_socket);
  212:   return 0;
  213: }
前へ << C 言語で echo サーバを作ってみよう (1) C言語で ftp クライアントを作ってみよう (1) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。