筑波大学 システム情報系 情報工学域 新城 靖 <yas@cs.tsukuba.ac.jp>
このページは、次の URL にあります。
https://www.coins.tsukuba.ac.jp/~syspro/2024/2024-07-17/echo-server-select.html
あるいは、次のページから手繰っていくこともできます。
https://www.coins.tsukuba.ac.jp/~syspro/2024/
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: */ 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 (集合)を意味する。内 部的には、ビットの並びで実現されていることが多い。次のような操作がある。
無限ループを含むのは、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())
readline() の代りにread() を使えば、そのような問題は生じない。しかし、 行単位で何か処理を行うようなサーバを作るには、「クライアントごとに」、 少なくとも1行をためるためのバッファを設ける必要がある。
クライアントが意図的に '\n' を送らないことがある。これも、DoS 攻撃 (Denial of Service攻撃、サービス運用妨害攻撃)の一種である。 その他に、writen() に対して、クライアントが read() しないという攻撃も ある。
fork版では、クライアントご とにプロセス(スレッド)が存在するので、安全に fgets() を使うことが できる。ただし、プロセスをコピーする処理が重たいので、過剰な connect() による接続要求による DoS 攻撃には弱くなる。(fork() の処理ばかりして、 通常の業務ができなくなる。)
サーバ・プログラムを作成する時には、DoS 攻撃に強いものを作成するように 気をつける。