select()による複数のクライアントに対するサービスの同時提供

システム・プログラム

                                       電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

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

■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:	        Start: 1997/06/09 19:53:33
   6:	*/
   7:	#include <stdio.h>
   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:	
  15:	extern  void echo_server( int portno );
  16:	extern  void echo_reply_select( int com );
  17:	extern  int  echo_reply_once( int com );
  18:	extern  void print_my_host_port( int portno );
  19:	extern  void tcp_peeraddr_print( int com );
  20:	extern  void sockaddr_print( struct sockaddr *addrp, int addr_len );
  21:	extern  tcp_acc_port( int portno );
  22:	extern  ssize_t writen(int fd, const void *vptr, size_t n);
  23:	extern  ssize_t readline(int fd, void *vptr, size_t maxlen);
  24:	
  25:	main( int argc, char *argv[] )
  26:	{
  27:	    int portno ;
  28:	        if( argc >= 3 )
  29:	        {
  30:	            fprintf( stdout,"Usage: %s [portno] \n",argv[0] );
  31:	            exit( -1 );
  32:	        }
  33:	        if( argc == 2 )
  34:	            portno = strtol( argv[1],0,10 );
  35:	        else
  36:	            portno = getuid();
  37:	        echo_reply_select( portno );
  38:	}
  39:	

main() 関数の部分は、fork版 とほと んど同じである。

----------------------------------------------------------------------
  40:	void
  41:	echo_reply_select( int portno )
  42:	{
  43:	    int acc,com ;
  44:	    fd_set readfds,readfds_save ;
  45:	    int i,n ;
  46:	
  47:	        acc = tcp_acc_port( portno );
  48:	        if( acc<0 )
  49:	            exit( -1 );
  50:	        print_my_host_port( portno );
  51:	
  52:	        FD_ZERO( &readfds_save );
  53:	        FD_SET( acc,&readfds_save );
  54:	        while( 1 )
  55:	        {
  56:	            readfds = readfds_save ;
  57:	            n = select( FD_SETSIZE,&readfds,0,0,0 );
  58:	            if( n <= 0 )
  59:	            {
  60:	                perror("select");
  61:	                exit( 1 );
  62:	            }
  63:	            if( FD_ISSET(acc,&readfds) )
  64:	            {
  65:	                FD_CLR( acc,&readfds );
  66:	                if( (com = accept( acc,0,0 )) < 0 )
  67:	                {
  68:	                    perror("accept");
  69:	                    exit( -1 );
  70:	                }
  71:	                FD_SET( com, &readfds_save );
  72:	                tcp_peeraddr_print( com );
  73:	            }
  74:	            for( i=0 ; i<FD_SETSIZE ; i++ )
  75:	            {
  76:	                if( FD_ISSET(i,&readfds) )
  77:	                {
  78:	                    if( echo_reply_once( i )<=0 )
  79:	                    {
  80:	                        printf("[%d] connection (fd==%d) closed.\n",getpid(),i );
  81:	                        close( i );
  82:	                        FD_CLR( i,&readfds_save );
  83:	                    }
  84:	                }
  85:	            }
  86:	        }
  87:	}
  88:	
----------------------------------------------------------------------

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_reply_once() を呼 び出す。普通は、0 より大きい数が返ってくる。そして次のセットされている ファイル記述子について echo_reply_once() の処理を続ける。

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

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

  89:	#define BUFFERSIZE      1024
  90:	
  91:	int
  92:	echo_reply_once( int com )
  93:	{
  94:	    char line[BUFFERSIZE] ;
  95:	    int rcount ;
  96:	    int wcount ;
  97:	
  98:	        if( (rcount=readline(com,line,BUFFERSIZE)) > 0 )
  99:	        {
 100:	            printf("[%d] read(%d,,) %d bytes, %s",getpid(),com,rcount,line );
 101:	            fflush( stdout );
 102:	            if( (wcount=writen(com,line,rcount))!= rcount )
 103:	            {
 104:	                 perror("write");
 105:	                 exit( 1 );
 106:	            }
 107:	        }
 108:	        return( rcount );
 109:	}
データを読込む時には、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() 版と同じである。

 112:	print_my_host_port( int portno )
 121:	tcp_peeraddr_print( int com )
 137:	sockaddr_print( struct sockaddr *addrp, int addr_len )
 147:	tcp_acc_port( int portno )
 178:	/* 
 179:	W.リチャード・スティーブンス著、篠田陽一訳:
 180:	"UNIXネットワークプログラミング第2版 Vol.1 ネットワークAPI:ソケットとXTI",
 181:	ピアソン・エデュケーション, 1999年. ISBN 4-98471-205-9
 182:	  3.9節 readn, writen, および readline 関数 (p.76)
 183:	
 184:	Richard Stevens: "UNIX Network Programming, Volume 1, Second Edition:
 185:	Networking APIs: Sockets and XTI", Prentice Hall, 1998.
 186:	ISBN 0-13-490012-X.  
 187:	    Section 3.9 readn, writen, and readline Functions (p.77)
 188:	
 189:	http://www.kohala.com/start/ (http://www.kohala.com/~rstevens/)
 190:	http://www.kohala.com/start/unpv12e/unpv12e.tar.gz
 191:	
 192:	*/
 193:	
 194:	/* include writen */
 195:	/*#include      "unp.h"*/
 196:	#include <errno.h>
 197:	
 198:	ssize_t                 /* Write "n" bytes to a descriptor. */
 199:	writen(int fd, const void *vptr, size_t n)
 200:	{
 201:	        size_t          nleft;
 202:	        ssize_t         nwritten;
 203:	        const char      *ptr;
 204:	
 205:	        ptr = vptr;
 206:	        nleft = n;
 207:	        while (nleft > 0) {
 208:	                if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
 209:	                        if (errno == EINTR)
 210:	                                nwritten = 0;           /* and call write() again */
 211:	                        else
 212:	                                return(-1);                     /* error */
 213:	                }
 214:	
 215:	                nleft -= nwritten;
 216:	                ptr   += nwritten;
 217:	        }
 218:	        return(n);
 219:	}
 220:	
 221:	/* include readline */
 222:	/*#include      "unp.h"*/
 223:	
 224:	ssize_t
 225:	readline(int fd, void *vptr, size_t maxlen)
 226:	{
 227:	        ssize_t n, rc;
 228:	        char    c, *ptr;
 229:	
 230:	        ptr = vptr;
 231:	        for (n = 1; n < maxlen; n++) {
 232:	again:
 233:	                if ( (rc = read(fd, &c, 1)) == 1) {
 234:	                        *ptr++ = c;
 235:	                        if (c == '\n')
 236:	                                break;  /* newline is stored, like fgets() */
 237:	                } else if (rc == 0) {
 238:	                        if (n == 1)
 239:	                                return(0);      /* EOF, no data read */
 240:	                        else
 241:	                                break;          /* EOF, some data was read */
 242:	                } else {
 243:	                        if (errno == EINTR)
 244:	                                goto again;
 245:	                        return(-1);             /* error, errno set by read() */
 246:	                }
 247:	        }
 248:	
 249:	        *ptr = 0;       /* null terminate like fgets() */
 250:	        return(n);
 251:	}
 252:	/* end readline */
readline() は、\n 記号が現れるまでを一区切りとして、読込む。fgets() と 同様に、最後に終端の 0 を付けてくれる。 このプログラムは、1バイトずつ読み込んでいるので、性能が悪い。上記の教 科書では、高速版やマルチスレッドで動作するプログラムも示してある。

実行例。

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

注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。


----------------------------------------------------------------------
% ./echo-server-select 1231 [←]
run telnet adonis9.coins.tsukuba.ac.jp 1231 
[30747] connection (fd==4) from 130.158.86.71:34092
[30747] read(4,,) 5 bytes, 012
[30747] connection (fd==5) from 130.158.86.29:33988
[30747] read(5,,) 5 bytes, abc
[30747] read(5,,) 5 bytes, def
[30747] connection (fd==5) closed.
[30747] read(4,,) 5 bytes, 345
[30747] connection (fd==4) closed.
^C
% []
----------------------------------------------------------------------
クライアント側(その1)。
----------------------------------------------------------------------
% telnet adonis9.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------
クライアント側(その2)。
----------------------------------------------------------------------
% telnet adonis9.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc[←]
abc
def[←]
def
^]
telnet> quit[←]
Connection closed.
% []
----------------------------------------------------------------------

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

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

◆DoS攻撃

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

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

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

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

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


Last updated: 2004/05/09 23:15:30
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>