システム・プログラム
電子・情報工学系
新城 靖
<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.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 (集合)を意味する。内 部的には、ビットの並びで実現されていることが多い。次のような操作がある。
無限ループを含むのは、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クライアント側(その1)。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 %
----------------------------------------------------------------------
---------------------------------------------------------------------- % telnet adonis9.coins.tsukuba.ac.jp 1231クライアント側(その2)。Trying 130.158.86.29... Connected to adonis9.coins.tsukuba.ac.jp. Escape character is '^]'. 012
012 345
345 ^] telnet> quit
Connection closed. %
----------------------------------------------------------------------
---------------------------------------------------------------------- % telnet adonis9.coins.tsukuba.ac.jp 1231Trying 130.158.86.29... Connected to adonis9.coins.tsukuba.ac.jp. Escape character is '^]'. abc
abc def
def ^] telnet> quit
Connection closed. %
----------------------------------------------------------------------
図4 複数のクライアントが接続した時(select())
readline() の代りにread() を使えば、そのような問題は生じない。しかし、 行単位で何か処理を行うようなサーバを作るには、「クライアントごとに」、 少なくとも1行をためるためのバッファを設ける必要がある。
クライアントが意図的に '\n' を送らないことがある。これも、DoS 攻撃 (Denial of Service攻撃、サービス運用妨害攻撃)の一種である。 その他に、writen() に対して、クライアントが read() しないという攻撃も ある。
echo-server-fork.c では、クライアントご とにプロセス(スレッド)が存在するので、安全に readline() を使うことが できる。ただし、プロセスをコピーする処理が重たいので、過剰な connect() による接続要求による DoS 攻撃には弱くなる。(fork() の処理ばかりして、 通常の業務ができなくなる。)
サーバ・プログラムを作成する時には、DoS 攻撃に強いものを作成するように 気をつける。