筑波大学 システム情報系 情報工学域
                                       新城 靖
                                       <yas@cs.tsukuba.ac.jp>
このページは、次の URL にあります。
	http://www.coins.tsukuba.ac.jp/~syspro/2015/2015-07-28
/echo-server-select.html
あるいは、次のページから手繰っていくこともできます。
	http://www.coins.tsukuba.ac.jp/~syspro/2015/
	http://www.coins.tsukuba.ac.jp/~yas/
echo-server-select.cでは、1つのプロセスで実行している。
   1:	
   2:	/*
   3:	  echo-server-select.c -- 受け取った文字列をそのまま返すサーバ(select版)
   4:	  ~yas/syspro/ipc/echo-server-select.c
   5:	  Created on 1997/06/09 19:53:33
   6:	*/
   7:	#include <stdio.h>
   8:	#include <stdlib.h>     /* exit() */
   9:	#include <sys/types.h>  /* socket(), time(), select() */
  10:	#include <sys/socket.h> /* socket() */
  11:	#include <netinet/in.h> /* struct sockaddr_in */
  12:	#include <sys/time.h>   /* select() */
  13:	#include <unistd.h>     /* select() */
  14:	#include <netdb.h>      /* getnameinfo() */
  15:	#include <string.h>     /* memset() */
  16:	
  17:	extern  void echo_server_select( int portno, int ip_version );
  18:	extern  void echo_reply_select( int com );
  19:	extern  int  find_maxfds( fd_set *fds );
  20:	extern  int  echo_receive_request_and_send_reply_once( int com );
  21:	extern  int  echo_receive_request_fd( char *line, ssize_t size, int com );
  22:	extern  int  echo_send_reply_fd( char *line, ssize_t count, int com );
  23:	
  24:	extern  void print_my_host_port( int portno );
  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 = 4; /* IPv4 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:	
  61:	        FD_ZERO( &readfds_save );
  62:	        FD_SET( acc,&readfds_save );
  63:	        maxfds = next_maxfds = acc + 1;
  64:	        while( 1 )
  65:	        {
  66:	                readfds = readfds_save ;
  67:	                n = select( maxfds,&readfds,0,0,0 );
  68:	                if( n <= 0 )
  69:	                {
  70:	                        perror("select");
  71:	                        exit( 1 );
  72:	                }
  73:	                if( FD_ISSET(acc,&readfds) )
  74:	                {
  75:	                        FD_CLR( acc,&readfds );
  76:	                        printf("[%d] accepting incoming connections (fd==%d) ...\n",getpid(),acc );
  77:	                        if( (com = accept( acc,0,0 )) < 0 )
  78:	                        {
  79:	                                perror("accept");
  80:	                                exit( -1 );
  81:	                        }
  82:	                        FD_SET( com, &readfds_save );
  83:	                        if( com+1 > maxfds )
  84:	                        {
  85:	                                next_maxfds = com+1;
  86:	                        }
  87:	                        tcp_peeraddr_print( com );
  88:	                }
  89:	                for( i=0 ; i<maxfds ; i++ )
  90:	                {
  91:	                        if( FD_ISSET(i,&readfds) )
  92:	                        {
  93:	                                if( echo_receive_request_and_send_reply_once( i )<=0 )
  94:	                                {
  95:	                                        printf("[%d] connection (fd==%d) closed.\n",getpid(),i );
  96:	                                        close( i );
  97:	                                        FD_CLR( i,&readfds_save );
  98:	                                        if( maxfds == i+1 )
  99:	                                        {
 100:	                                                next_maxfds = find_maxfds( &readfds_save );
 101:	                                        }
 102:	                                }
 103:	                        }
 104:	                }
 105:	                maxfds = next_maxfds ;
 106:	        }
 107:	}
 108:	
tcp_acc_port() で、接続受付け用ポートに対応したソケットを
作り、print_my_host_port() (fork版 と同じ)
で表示している。
ループに入る前に、fd_set 形の変数 readfds_save を初期化している。 fd_set は、ファイル記述子(file descriptor) の set (集合)を意味する。内 部的には、ビットの並びで実現されていることが多い。次のような操作がある。
無限ループを含むのは、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版とは異なり、ゾンビ・プロセスは発生しない。
 109:	int
 110:	find_maxfds( fd_set *fds )
 111:	{
 112:	        int i, maxfds ;
 113:	
 114:	        for( i=FD_SETSIZE; i>= 0; i-- )
 115:	        {
 116:	                if( FD_ISSET(i,fds) )
 117:	                {
 118:	                        return( i+1 );
 119:	                }
 120:	        }
 121:	        return( 0 );
 122:	}
 123:	
find_maxfds() は、select() の第1引数に与える最大の「ファイル記述子+ 1」を探す関数である。引数の fd_set を、FD_SETSIZE から 0 に向かって探 している。
 124:	#define BUFFERSIZE      1024
 125:	
 126:	int
 127:	echo_receive_request_and_send_reply_once( int com )
 128:	{
 129:	        char line[BUFFERSIZE] ;
 130:	        int rcount ;
 131:	        int wcount ;
 132:	
 133:	        if( (rcount=echo_receive_request_fd(line,BUFFERSIZE,com)) > 0 )
 134:	        {
 135:	                printf("[%d] read(%d,,) %d bytes, %s",getpid(),com,rcount,line );
 136:	                fflush( stdout );
 137:	                if( (wcount=echo_send_reply_fd(line,rcount,com))!= rcount )
 138:	                {
 139:	                        perror("write");
 140:	                        return( -1 );
 141:	                }
 142:	        }
 143:	        return( rcount );
 144:	}
 145:	
 146:	int
 147:	echo_receive_request_fd( char *line, ssize_t size, int com )
 148:	{
 149:	        return( readline(com,line,BUFFERSIZE) );
 150:	}
 151:	
 152:	int
 153:	echo_send_reply_fd( char *line, ssize_t count, int com )
 154:	{
 155:	        return( writen(com,line,count) );
 156:	}
 157:	
データを読込む時には、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() 版と同じである。
160: print_my_host_port( int portno ) 170: tcp_peeraddr_print( int com ) 187: sockaddr_print( struct sockaddr *addrp, socklen_t addr_len ) 204: tcp_acc_port( int portno, int ip_version )
 281:	/* 
 282:	W.リチャード・スティーブンス著、篠田陽一訳:
 283:	"UNIXネットワークプログラミング第2版 Vol.1 ネットワークAPI:ソケットとXTI",
 284:	ピアソン・エデュケーション, 1999年. ISBN 4-98471-205-9
 285:	  3.9節 readn, writen, および readline 関数 (p.76)
 286:	
 287:	Richard Stevens: "UNIX Network Programming, Volume 1, Second Edition:
 288:	Networking APIs: Sockets and XTI", Prentice Hall, 1998.
 289:	ISBN 0-13-490012-X.  
 290:	    Section 3.9 readn, writen, and readline Functions (p.77)
 291:	
 292:	http://www.kohala.com/start/ (http://www.kohala.com/~rstevens/)
 293:	http://www.kohala.com/start/unpv12e/unpv12e.tar.gz
 294:	
 295:	*/
 296:	
 297:	/* include writen */
 298:	/*#include      "unp.h"*/
 299:	#include <errno.h>
 300:	
 301:	ssize_t                 /* Write "n" bytes to a descriptor. */
 302:	writen(int fd, const void *vptr, size_t n)
 303:	{
 304:	        size_t          nleft;
 305:	        ssize_t         nwritten;
 306:	        const char      *ptr;
 307:	
 308:	        ptr = vptr;
 309:	        nleft = n;
 310:	        while (nleft > 0) {
 311:	                if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
 312:	                        if (errno == EINTR)
 313:	                                nwritten = 0;           /* and call write() again */
 314:	                        else
 315:	                                return(-1);                     /* error */
 316:	                }
 317:	
 318:	                nleft -= nwritten;
 319:	                ptr   += nwritten;
 320:	        }
 321:	        return(n);
 322:	}
 323:	
 324:	/* include readline */
 325:	/*#include      "unp.h"*/
 326:	
 327:	ssize_t
 328:	readline(int fd, void *vptr, size_t maxlen)
 329:	{
 330:	        ssize_t n, rc;
 331:	        char    c, *ptr;
 332:	
 333:	        ptr = vptr;
 334:	        for (n = 1; n < maxlen; n++) {
 335:	again:
 336:	                if ( (rc = read(fd, &c, 1)) == 1) {
 337:	                        *ptr++ = c;
 338:	                        if (c == '\n')
 339:	                                break;  /* newline is stored, like fgets() */
 340:	                } else if (rc == 0) {
 341:	                        if (n == 1)
 342:	                                return(0);      /* EOF, no data read */
 343:	                        else
 344:	                                break;          /* EOF, some data was read */
 345:	                } else {
 346:	                        if (errno == EINTR)
 347:	                                goto again;
 348:	                        return(-1);             /* error, errno set by read() */
 349:	                }
 350:	        }
 351:	
 352:	        *ptr = 0;       /* null terminate like fgets() */
 353:	        return(n);
 354:	}
 355:	/* 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-select  1231 
run telnet crocus39.coins.tsukuba.ac.jp 1231
[599] accepting incoming connections (fd==3) ...
[599] connection (fd==4) from 130.158.86.247:50504
[599] read(4,,) 5 bytes, 012
[599] accepting incoming connections (fd==3) ...
[599] connection (fd==5) from 130.158.86.248:50429
[599] read(5,,) 5 bytes, abc
[599] read(5,,) 5 bytes, def
[599] read(4,,) 5 bytes, 345
[599] connection (fd==4) closed.
[599] connection (fd==5) closed.
^C
$ 
クライアント側(その1)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231 
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
345
345
^]
telnet> ^D
Connection closed.
$ 
クライアント側(その2)。
$ telnet crocus39.coins.tsukuba.ac.jp 1231 
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
abc
def
def
^]
telnet> ^D
Connection closed.
$ 
図1 複数のクライアントが接続した時(select())
readline() の代りにread() を使えば、そのような問題は生じない。しかし、 行単位で何か処理を行うようなサーバを作るには、「クライアントごとに」、 少なくとも1行をためるためのバッファを設ける必要がある。
クライアントが意図的に '\n' を送らないことがある。これも、DoS 攻撃 (Denial of Service攻撃、サービス運用妨害攻撃)の一種である。 その他に、writen() に対して、クライアントが read() しないという攻撃も ある。
fork版では、クライアントご とにプロセス(スレッド)が存在するので、安全に fgets() を使うことが できる。ただし、プロセスをコピーする処理が重たいので、過剰な connect() による接続要求による DoS 攻撃には弱くなる。(fork() の処理ばかりして、 通常の業務ができなくなる。)
サーバ・プログラムを作成する時には、DoS 攻撃に強いものを作成するように 気をつける。