システムプログラム(第8回): select()による複数のクライアントに対するサービスの同時提供

                                       筑波大学 システム情報系 情報工学域
                                       新城 靖
                                       <yas@cs.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2022/2022-07-13/echo-server-select.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2022/
http://www.coins.tsukuba.ac.jp/~yas/

echo-server-select

fork版 では、クライアントから接続要 求を受け付けるたびに、新しいプロセスを作っていた。以下の echo-server-select.cでは、1つのプロセスで実行している。
   1:	
   2:	/*
   3:	  echo-server-select.c -- 受け取った文字列をそのまま返すサーバ(select版)
   4:	  ~yas/syspro/ipc/echo-server-select.c
   5:	*/
   6:	#include <stdio.h>
   7:	#include <stdlib.h>     /* exit() */
   8:	#include <sys/types.h>  /* socket(), time(), select() */
   9:	#include <sys/socket.h> /* socket() */
  10:	#include <netinet/in.h> /* struct sockaddr_in */
  11:	#include <sys/time.h>   /* select() */
  12:	#include <unistd.h>     /* select() */
  13:	#include <netdb.h>      /* getnameinfo() */
  14:	#include <string.h>     /* memset() */
  15:	
  16:	extern  void echo_server_select( int portno, int ip_version );
  17:	extern  void echo_reply_select( int com );
  18:	extern  int  find_maxfds( fd_set *fds );
  19:	extern  int  echo_receive_request_and_send_reply_once( int com );
  20:	extern  int  echo_receive_request_fd( char *line, ssize_t size, int com );
  21:	extern  int  echo_send_reply_fd( char *line, ssize_t count, int com );
  22:	
  23:	extern  void print_my_host_port( int portno );
  24:	extern  void tcp_sockaddr_print( int com );
  25:	extern  void tcp_peeraddr_print( int com );
  26:	extern  void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
  27:	extern  int  tcp_acc_port( int portno, int pf );
  28:	
  29:	extern  ssize_t writen(int fd, const void *vptr, size_t n);
  30:	extern  ssize_t readline(int fd, void *vptr, size_t maxlen);
  31:	
  32:	int
  33:	main( int argc, char *argv[] )
  34:	{
  35:	        int portno, ip_version;
  36:	
  37:	        if( !(argc == 2 || argc==3) ) {
  38:	                fprintf(stderr,"Usage: %s portno {ipversion}\n",argv[0] );
  39:	                exit( 1 );
  40:	        }
  41:	        portno = strtol( argv[1],0,10 );
  42:	        if( argc == 3 )
  43:	                ip_version = strtol( argv[2],0,10 );
  44:	        else
  45:	                ip_version = 46; /* Both IPv4 and IPv6 by default */
  46:	        echo_server_select( portno, ip_version );
  47:	}
  48:	

main() 関数の部分は、fork版 とほと んど同じである。
  49:	void
  50:	echo_server_select( int portno, int ip_version )
  51:	{
  52:	        int acc,com ;
  53:	        fd_set readfds,readfds_save ;
  54:	        int i,n, maxfds, next_maxfds ;
  55:	
  56:	        acc = tcp_acc_port( portno, ip_version );
  57:	        if( acc<0 )
  58:	                exit( -1 );
  59:	        print_my_host_port( portno );
  60:	        tcp_sockaddr_print( acc );
  61:	
  62:	        FD_ZERO( &readfds_save );
  63:	        FD_SET( acc,&readfds_save );
  64:	        maxfds = next_maxfds = acc + 1;
  65:	        while( 1 )
  66:	        {
  67:	                readfds = readfds_save ;
  68:	                n = select( maxfds,&readfds,0,0,0 );
  69:	                if( n <= 0 )
  70:	                {
  71:	                        perror("select");
  72:	                        exit( 1 );
  73:	                }
  74:	                if( FD_ISSET(acc,&readfds) )
  75:	                {
  76:	                        FD_CLR( acc,&readfds );
  77:	                        printf("[%d] accepting incoming connections (fd==%d) ...\n",getpid(),acc );
  78:	                        if( (com = accept( acc,0,0 )) < 0 )
  79:	                        {
  80:	                                perror("accept");
  81:	                                exit( -1 );
  82:	                        }
  83:	                        FD_SET( com, &readfds_save );
  84:	                        if( com+1 > maxfds )
  85:	                        {
  86:	                                next_maxfds = com+1;
  87:	                        }
  88:	                        tcp_peeraddr_print( com );
  89:	                }
  90:	                for( i=0 ; i<maxfds ; i++ )
  91:	                {
  92:	                        if( FD_ISSET(i,&readfds) )
  93:	                        {
  94:	                                if( echo_receive_request_and_send_reply_once( i )<=0 )
  95:	                                {
  96:	                                        printf("[%d] connection (fd==%d) closed.\n",getpid(),i );
  97:	                                        close( i );
  98:	                                        FD_CLR( i,&readfds_save );
  99:	                                        if( maxfds == i+1 )
 100:	                                        {
 101:	                                                next_maxfds = find_maxfds( &readfds_save );
 102:	                                        }
 103:	                                }
 104:	                        }
 105:	                }
 106:	                maxfds = next_maxfds ;
 107:	        }
 108:	}
 109:	

tcp_acc_port() で、接続受付け用ポートに対応したソケットを 作り、print_my_host_port() (fork版 と同じ) で表示している。

ループに入る前に、fd_set 形の変数 readfds_save を初期化している。 fd_set は、ファイル記述子(file descriptor) の set (集合)を意味する。内 部的には、ビットの並びで実現されていることが多い。次のような操作がある。

FD_ZERO(&fds)
初期化する(集合を空にする)
FD_SET(fd,&fds)
fd を集合に加える(ビットを1にする)
FD_CLR(fd,&fds)
fd を集合から取り除く(ビットを0にする)
FD_ISSET(fd,&fds)
その fd が集合に含まれているかを調べる。
初期値は、tcp_acc_port() で作成した接続受付け用ポートに対応したソケッ トを入れる。

無限ループを含むのは、fork版と同じ である。

readfds = readfds_save は、select() の実行で壊されるので、保存してある。 「壊される」とは、プロセスからシステムへの方向に値を送るだけでなく、同 じ場所に、システムからプロセスの方向へ結果が返される(同じ場所に上書き される)ことを意味する。

select() は、引数で指定された集合が、入力可能かどうかを調べている。入 力可能でなければ、入力可能になるまで待つ。

第3引数以降を使えば、出力可能かを調べたり、無限に待つのではなくて、あ る指定された時間だけ待つこともできる。

もし、要求受付け用ポートに対応したソケット acc が入力可能ならば、 accept() すると止まらずに処理が進むことを意味する。処理とは、クライア ントとの間の通信用ポートに対応したソケットが作ることである。fork() 版 とは違い、この accept() の所で待つことはない。

accept() が成功したら、結果として返されたファイル記述子を、 readfds_save に加える。次のループから select() の監視の対象になる。

for 文で、残りのファイル記述子について調べる。一度に複数のファイル記 述子がセットされている可能性がある。

もしセットされているファイル記述子を見つけたら、 echo_receive_request_and_send_reply_once() を呼び出す。普通は、0 より大きい数が返ってくる。そして次のセットされている ファイル記述子について echo_receive_request_and_send_reply_once() の処理を続ける。

特殊な場合として、echo_receive_request_and_send_reply_once() は、クライ アントが接続を切った場合には、0 以下の値を返す。その場合は、close() で ファイル記述子を解放し、readfds_save からも FD_CLR() で取り除く。

select() の第1引数には、最大の「ファイル記述子+1」を与える。 初期値は、acc+1 である。 accept() の時に、増える可能性があり、その場合は増やす。 close() の時に減る可能性があり、その場合は減らす。

select版では、fork版とは異なり、ゾンビ・プロセスは発生しない。

 110:	int
 111:	find_maxfds( fd_set *fds )
 112:	{
 113:	        int i, maxfds ;
 114:	
 115:	        for( i=FD_SETSIZE; i>= 0; i-- )
 116:	        {
 117:	                if( FD_ISSET(i,fds) )
 118:	                {
 119:	                        return( i+1 );
 120:	                }
 121:	        }
 122:	        return( 0 );
 123:	}
 124:	

find_maxfds() は、select() の第1引数に与える最大の「ファイル記述子+ 1」を探す関数である。引数の fd_set を、FD_SETSIZE から 0 に向かって探 している。

 125:	#define BUFFERSIZE      1024
 126:	
 127:	int
 128:	echo_receive_request_and_send_reply_once( int com )
 129:	{
 130:	        char line[BUFFERSIZE] ;
 131:	        int rcount ;
 132:	        int wcount ;
 133:	
 134:	        if( (rcount=echo_receive_request_fd(line,BUFFERSIZE,com)) > 0 )
 135:	        {
 136:	                printf("[%d] read(%d,,) %d bytes, %s",getpid(),com,rcount,line );
 137:	                fflush( stdout );
 138:	                if( (wcount=echo_send_reply_fd(line,rcount,com))!= rcount )
 139:	                {
 140:	                        perror("write");
 141:	                        return( -1 );
 142:	                }
 143:	        }
 144:	        return( rcount );
 145:	}
 146:	
 147:	int
 148:	echo_receive_request_fd( char *line, ssize_t size, int com )
 149:	{
 150:	        return( readline(com,line,BUFFERSIZE) );
 151:	}
 152:	
 153:	int
 154:	echo_send_reply_fd( char *line, ssize_t count, int com )
 155:	{
 156:	        return( writen(com,line,count) );
 157:	}
 158:	
データを読込む時には、readline() を使っている。これは、fgets() と同様 に\n 記号が現れるまでを一区切りとして、読込むものである。引数は fgets() とは異なり、ファイル記述子である。readline() は、バッファサイ ズ以上は、読込まない。また、最後に文字列の終端の 0 を付ける。(read() システムコールでは、付けてくれない。)

このプログラムでは、write() システムコールの変りに writen() 関数(ソー ス・プログラムは同じファイルの下の方にある)を使っている。TCP/IP の通 信では、write(fd,buf,100) としても、100バイト送られずに、50 バイトしか 送られないことがある。残りの 50 バイトも送る必要があれば、ループして全 部送るようにする。システム・コール write() は、今は送る必要がない(後 で送ってもよい)場合、送らなくてもよい場合にも対応できるようになってい る。しかし、一般には送る方は全部送り終わるまでループして待った方がよい 場合が多い。writen() は、このような目的のための関数である。

echo_reply_once() は、readline() の結果(多くの場合は、読込んだバイト数) をそのまま返す。0 以下の数を返した時にもそのまま返す。

以下の関数は、fork() 版と同じである。

 161:	print_my_host_port( int portno )
 171:	tcp_sockaddr_print( int com )
 188:	tcp_peeraddr_print( int com )
 205:	sockaddr_print( struct sockaddr *addrp, socklen_t addr_len )
 222:	tcp_acc_port( int portno, int ip_version )
 304:	/* 
 305:	W.リチャード・スティーブンス著、篠田陽一訳:
 306:	"UNIXネットワークプログラミング第2版 Vol.1 ネットワークAPI:ソケットとXTI",
 307:	ピアソン・エデュケーション, 1999年. ISBN 4-98471-205-9
 308:	  3.9節 readn, writen, および readline 関数 (p.76)
 309:	
 310:	Richard Stevens: "UNIX Network Programming, Volume 1, Second Edition:
 311:	Networking APIs: Sockets and XTI", Prentice Hall, 1998.
 312:	ISBN 0-13-490012-X.  
 313:	    Section 3.9 readn, writen, and readline Functions (p.77)
 314:	
 315:	http://www.kohala.com/start/ (http://www.kohala.com/~rstevens/)
 316:	http://www.kohala.com/start/unpv12e/unpv12e.tar.gz
 317:	
 318:	*/
 319:	
 320:	/* include writen */
 321:	/*#include      "unp.h"*/
 322:	#include <errno.h>
 323:	
 324:	ssize_t                 /* Write "n" bytes to a descriptor. */
 325:	writen(int fd, const void *vptr, size_t n)
 326:	{
 327:	        size_t          nleft;
 328:	        ssize_t         nwritten;
 329:	        const char      *ptr;
 330:	
 331:	        ptr = vptr;
 332:	        nleft = n;
 333:	        while (nleft > 0) {
 334:	                if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
 335:	                        if (errno == EINTR)
 336:	                                nwritten = 0;           /* and call write() again */
 337:	                        else
 338:	                                return(-1);                     /* error */
 339:	                }
 340:	
 341:	                nleft -= nwritten;
 342:	                ptr   += nwritten;
 343:	        }
 344:	        return(n);
 345:	}
 346:	
 347:	/* include readline */
 348:	/*#include      "unp.h"*/
 349:	
 350:	ssize_t
 351:	readline(int fd, void *vptr, size_t maxlen)
 352:	{
 353:	        ssize_t n, rc;
 354:	        char    c, *ptr;
 355:	
 356:	        ptr = vptr;
 357:	        for (n = 1; n < maxlen; n++) {
 358:	again:
 359:	                if ( (rc = read(fd, &c, 1)) == 1) {
 360:	                        *ptr++ = c;
 361:	                        if (c == '\n')
 362:	                                break;  /* newline is stored, like fgets() */
 363:	                } else if (rc == 0) {
 364:	                        if (n == 1)
 365:	                                return(0);      /* EOF, no data read */
 366:	                        else
 367:	                                break;          /* EOF, some data was read */
 368:	                } else {
 369:	                        if (errno == EINTR)
 370:	                                goto again;
 371:	                        return(-1);             /* error, errno set by read() */
 372:	                }
 373:	        }
 374:	
 375:	        *ptr = 0;       /* null terminate like fgets() */
 376:	        return(n);
 377:	}
 378:	/* end readline */
readline() は、\n 記号が現れるまでを一区切りとして、読込む。fgets() と 同様に、最後に終端の 0 を付けてくれる。 このプログラムは、1バイトずつ読み込んでいるので、性能が悪い。上記の教 科書では、高速版やマルチスレッドで動作するプログラムも示してある。

実行例。

サーバ側。サーバは、終了しないので、最後に、^C を押して、割り 込みを掛けて終了させる。

注意:同じホストで複数人がポート番号 1231 を使うと動作しない。

$ cp ~yas/syspro/ipc/echo-server-select.c . [←]
$ make echo-server-select [←]
cc     echo-server-select.c   -o echo-server-select
$ ./echo-server-pthread [←]
Usage: ./echo-server-pthread portno {ipversion}
$ ./echo-server-select [←]
Usage: ./echo-server-select portno {ipversion}
$ ./echo-server-fork-fdopen 1231 [←]
run telnet aloe30.local 1231
[14407] accepting (fd==4) to [::]:1231
[14407] accepting incoming connections (acc==4) ...
[14407] connection (fd==5) from [2001:2f8:3a:1711::230:39]:53095
[14407] accepting incoming connections (acc==4) ...
[14408] received (fd==5) 5 bytes, [012
]
[14407] connection (fd==5) from [2001:2f8:3a:1711::230:40]:63432
[14407] accepting incoming connections (acc==4) ...
[14642] received (fd==5) 5 bytes, [abc
]
[14642] received (fd==5) 5 bytes, [def
]
[14408] received (fd==5) 5 bytes, [345
]
[14408] connection (fd==5) closed.
[14642] connection (fd==5) closed.
^C
$ []
クライアント側(その1)。
$ telnet aloe30 1231 [←]
Trying 2001:2f8:3a:1711::230:30...
Connected to aloe30.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> ^D
Connection closed.
$ []
クライアント側(その2)。
$ telnet aloe30 1231 [←]
Trying 2001:2f8:3a:1711::230:30...
Connected to aloe30.
Escape character is '^]'.
abc[←]
abc
def[←]
def
^]
telnet> ^D
Connection closed.
$ []

図1 複数のクライアントが接続した時(select())

図1 複数のクライアントが接続した時(select())

echo-server-selectに対するDoS攻撃

このプログラムでは、readline() を使っているので、クライアントから \nが 送られてこないと、サーバ全体の処理が停止してしまう。select() の結果、 本来ならブロックしないはずであるが、\n が来たらブロックしてしまう。

readline() の代りにread() を使えば、そのような問題は生じない。しかし、 行単位で何か処理を行うようなサーバを作るには、「クライアントごとに」、 少なくとも1行をためるためのバッファを設ける必要がある。

クライアントが意図的に '\n' を送らないことがある。これも、DoS 攻撃 (Denial of Service攻撃、サービス運用妨害攻撃)の一種である。 その他に、writen() に対して、クライアントが read() しないという攻撃も ある。

fork版では、クライアントご とにプロセス(スレッド)が存在するので、安全に fgets() を使うことが できる。ただし、プロセスをコピーする処理が重たいので、過剰な connect() による接続要求による DoS 攻撃には弱くなる。(fork() の処理ばかりして、 通常の業務ができなくなる。)

サーバ・プログラムを作成する時には、DoS 攻撃に強いものを作成するように 気をつける。


Last updated: 2022/06/21 11:42:38
Yasushi Shinjo / <yas@cs.tsukuba.ac.jp>