システムプログラム(第8週): ネットワーク・プログラミング/サーバ側(2)

                                       筑波大学 システム情報工学研究科 
                                       コンピュータサイエンス専攻, 電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

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

捕捉

ネットワーク・プログラミングの難しさ

ネットワークを超えて送られてくるデータは、(意図的に)間違っているこが ある。

HTTP のエラー

HTTP のエラーは、次のようになる。
% telnet www 80 [←]
Trying 130.158.86.207...
Connected to orchid-nwd.coins.tsukuba.ac.jp.
Escape character is '^]'.
GET /.. HTTP/1.0[←]
[←]
HTTP/1.1 400 Bad Request
Date: Mon, 15 Jun 2009 08:53:15 GMT
Server: Apache/2.0.63 (Unix) PHP/5.2.6
Content-Length: 323
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.0.63 (Unix) PHP/5.2.6 Server at www.coins.tsukuba.ac.jp Port 80</address>
</body></html>
Connection closed by foreign host.
% []
このうち、1行目、Content-Type:、空行、本文はしっかり返したい。

今日の重要な話

TCP/IP によるネットワーク・プログラミング

復習

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

複数のクライアントに対してサービスの同時に提供することが望ましい。

echo-server-nofork-fdopenへの複数のクライアントの接続

echo-server-nofork-fdopen.c は、1つのクライアントからの接続されると、そのクライアントに掛り切りに なって、他のクライアントにはサービスを提供できないという問題がある。

サーバ側

% cp ~yas/syspro/ipc/echo-server-nofork-fdopen.c . [←]
% make echo-server-nofork-fdopen [←]
cc     echo-server-nofork-fdopen.c   -o echo-server-nofork-fdopen
% ./echo-server-nofork-fdopen  [←]
Usage: ./echo-server-nofork-fdopen portno
% ./echo-server-nofork-fdopen 1231 [←]
run telnet azalea20 1231 
[710] connection (fd==4) from 130.158.86.40:49348
[710] received (fd==4) 5 bytes, [012
]
^C
% []
クライアント側(その1)。
% telnet azalea20 1231 [←]
Trying 130.158.86.40...
Connected to azalea20.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
[]
クライアント側(その2)。
% telnet azalea20 1231 [←]
Trying 130.158.86.40...
Connected to azalea20.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
[]
クライアント(その2)は、クライアント(その1)が終了するまでサービス を受けられない。

標準のechoサーバへの複数のクライアントの接続

標準の echo サーバ(ポート番号 7 で動作している) は、複数のクライアント から接続されれた場合、同時にサービスを提供することができる。

クライアント側(その1)。

% telnet azalea20 7 [←]
Trying 130.158.86.40...
Connected to azalea20.coins.tsukuba.ac.jp.
Escape character is '^]'.
012
012
345
345
^]
telnet> ^D
Connection closed.
% []
クライアント側(その2)。
% telnet azalea20 7 [←]
Trying 130.158.86.40...
Connected to azalea20.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc
abc
def
def
^]
telnet> ^D
Connection closed.
% []

複数のクライアントを同時に扱う方法

複数のクライアントに対してサービスの同時に提供するには次のような方法が ある。

fork()

fork() は、親プロセスをコピーすることにより、子プロセスを作成するシステ ム・コールである。 前半では、exec とともに 使う例を示した。 ネットワーク・プログラミングでは、exec を伴わない fork() が使われる ことがある。

3回forkするプログラム

このプログラムは、表面的には 3 回 fork() して画面に hello と表示するプ ログラムである。
   1:	/*
   2:	        fork-hello.c -- 画面に文字列を表示するプログラム
   3:	        ~yas/syspro/proc/fork-hello.c
   4:	        Start: 2001/05/13 23:19:01
   5:	*/
   6:	
   7:	#include <stdio.h>
   8:	
   9:	main()
  10:	{
  11:	        fork();
  12:	        fork();
  13:	        fork();
  14:	        printf("hello\n");
  15:	}
実行すると、画面には 8 回 hello と表示される。
% cp ~yas/syspro/proc/fork-hello.c . [←]
% make fork-hello [←]
cc     fork-hello.c   -o fork-hello
% ./fork-hello  [←]
hello
hello
hello
hello
hello
hello
hello
hello
% []
1回のfork() で、2倍に増える。3回のforkで、2 3 個に 増える。

echo-server-fork-fdopen.c

TCP/IP のポート番号 7 (echo) では、受け取ったデータをそのまま返すサー ビスを提供している。以下は、これと同じような機能を提供するサーバである。 複数の接続先(クライアント)の要求を同時に処理するために、クライアント ごとに fork() システム・コールで専用の子プロセスを作る。

echo-server-fork-fdopen.c のmain()

   1:	
   2:	/*
   3:	        echo-server-fork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork版)
   4:	        ~yas/syspro/ipc/echo-server-fork-fdopen.c
   5:	        Created on 2004/05/09 19:57:07
   6:	*/
   7:	#include <stdio.h>
   8:	#include <stdlib.h>     /* exit() */
   9:	#include <sys/types.h>  /* socket(), wait4() */
  10:	#include <sys/socket.h> /* socket() */
  11:	#include <netinet/in.h> /* struct sockaddr_in */
  12:	#include <sys/resource.h> /* wait4() */
  13:	#include <sys/wait.h>   /* wait4() */
  14:	#include <netdb.h>      /* getnameinfo() */
  15:	#include <string.h>     /* strlen() */
  16:	
  17:	extern  void echo_server( int portno );
  18:	extern  void echo_reply( int com );
  19:	extern  void delete_zombie(void);
  20:	extern  void print_my_host_port( int portno );
  21:	extern  void tcp_peeraddr_print( int com );
  22:	extern  void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
  23:	extern  tcp_acc_port( int portno );
  24:	extern  int fdopen_sock( int sock, FILE **inp, FILE **outp );
  25:	
  26:	main( int argc, char *argv[] )
  27:	{
  28:	    int portno ;
  29:	        if( argc != 2 )
  30:	        {
  31:	            fprintf( stdout,"Usage: %s portno\n",argv[0] );
  32:	            exit( -1 );
  33:	        }
  34:	        portno = strtol( argv[1],0,10 );
  35:	        echo_server( portno );
  36:	}
  37:	
main() は、 echo-server-nofork-fdopen.c と同じである。

echo-server-fork-fdopen.c のecho_server()

echo_server() は、echoサーバのメインループである。
  38:	void
  39:	echo_server( int portno )
  40:	{
  41:	    int acc,com ;
  42:	    pid_t child_pid ;
  43:	        acc = tcp_acc_port( portno );
  44:	        if( acc<0 )
  45:	            exit( -1 );
  46:	        print_my_host_port( portno );
  47:	        while( 1 )
  48:	        {
  49:	            delete_zombie();
  50:	            if( (com = accept( acc,0,0 )) < 0 )
  51:	            {
  52:	                perror("accept");
  53:	                exit( -1 );
  54:	            }
  55:	            tcp_peeraddr_print( com );
  56:	            if( (child_pid=fork()) > 0 ) /* parent */
  57:	            {
  58:	                close( com );
  59:	            }
  60:	            else if( child_pid == 0 ) /* child */
  61:	            {
  62:	                close( acc );
  63:	                echo_reply( com );
  64:	                exit( 0 );
  65:	            }
  66:	            else
  67:	            {
  68:	                perror("fork");
  69:	                exit( -1 );
  70:	            }  
  71:	        }
  72:	}
  73:	

delete_zombie() は、ゾンビ・プロセス(後述)を消去するものである。 (改善の余地がある。) echo サービスとしての処理は、fork() システムコールで分身を作り、子プロ セス側で行う。親プロセスは、すぐにaccept() に戻る。

getpid() は、自分自身の PID (Process ID (identifier)) を返すシステムコー ルである。PID (pid_t) は、16 ビット(システムによっては32ビット)の整数 である。実行結果で printf() の表示に、PID が表示されているが、同じではないことに 注意しなさい。

echo-server-fork-fdopen.c のdelete_zombie()

delete_zombie() は、終了した子プロセスの残骸を回収する関数である。
  74:	void
  75:	delete_zombie()
  76:	{
  77:	    pid_t pid ;
  78:	    while( (pid=wait4(-1,0,WNOHANG,0)) >0 )
  79:	    {
  80:	        printf("[%d] zombi %d deleted.\n",getpid(),pid );
  81:	        continue;
  82:	    }
  83:	}
  84:	
wait4() システムコールを使って、終了した子プロセスをwait してあげてい る。ただし、WNOHANG オプションを付けてあるので、終了した子プロセスがい なければ、待たずに返ってくる。

echo-server-fork-fdopen.c のその他の関数

他の関数は、 echo-server-nofork-fdopen.c と同じである。

 % diff -c echo-server-nofork-fdopen.c echo-server-fork-fdopen.c
 *** echo-server-nofork-fdopen.c	Mon Jun  8 18:46:58 2009
 --- echo-server-fork-fdopen.c	Mon Jun 15 18:06:02 2009
 ***************
 *** 1,8 ****

   /*
 ! 	echo-server-nofork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork無し版)
 ! 	~yas/syspro/ipc/echo-server-nofork-fdopen.c
 ! 	Created on 2004/05/09 19:08:47
   */
   #include <stdio.h>
   #include <stdlib.h>	/* exit() */
 --- 1,8 ----

   /*
 ! 	echo-server-fork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork版)
 ! 	~yas/syspro/ipc/echo-server-fork-fdopen.c
 ! 	Created on 2004/05/09 19:57:07
   */
   #include <stdio.h>
   #include <stdlib.h>	/* exit() */
 ***************
 *** 16,21 ****
 --- 16,22 ----

   extern	void echo_server( int portno );
   extern	void echo_reply( int com );
 + extern	void delete_zombie(void);
   extern	void print_my_host_port( int portno );
   extern	void tcp_peeraddr_print( int com );
   extern	void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
 ***************
 *** 38,59 ****
   echo_server( int portno )
   {
       int acc,com ;
	 acc = tcp_acc_port( portno );
	 if( acc<0 )
	     exit( -1 );
	 print_my_host_port( portno );
	 while( 1 )
	 {
	     if( (com = accept( acc,0,0 )) < 0 )
	     {
		 perror("accept");
		 exit( -1 );
	     }
	     tcp_peeraddr_print( com );
 ! 	    echo_reply( com );
	 }
   }

   #define	BUFFERSIZE	1024

   void
 --- 39,87 ----
   echo_server( int portno )
   {
       int acc,com ;
 +     pid_t child_pid ;
	 acc = tcp_acc_port( portno );
	 if( acc<0 )
	     exit( -1 );
	 print_my_host_port( portno );
	 while( 1 )
	 {
 + 	    delete_zombie();
	     if( (com = accept( acc,0,0 )) < 0 )
	     {
		 perror("accept");
		 exit( -1 );
	     }
	     tcp_peeraddr_print( com );
 ! 	    if( (child_pid=fork()) > 0 ) /* parent */
 ! 	    {
 ! 		close( com );
 ! 	    }
 ! 	    else if( child_pid == 0 ) /* child */
 ! 	    {
 ! 		close( acc );
 ! 		echo_reply( com );
 ! 		exit( 0 );
 ! 	    }
 ! 	    else
 ! 	    {
 ! 		perror("fork");
 ! 		exit( -1 );
 ! 	    }  
	 }
   }

 + void
 + delete_zombie()
 + {
 +     pid_t pid ;
 +     while( (pid=wait4(-1,0,WNOHANG,0)) >0 )
 +     {
 + 	printf("[%d] zombi %d deleted.\n",getpid(),pid );
 + 	continue;
 +     }
 + }
 + 
   #define	BUFFERSIZE	1024

   void
 % 

echo-server-fork-fdopenの実行例

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

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

% cp ~yas/syspro/ipc/echo-server-fork-fdopen.c . [←]
% make echo-server-fork-fdopen [←]
cc     echo-server-fork-fdopen.c   -o echo-server-fork-fdopen
% ./echo-server-fork-fdopen  [←]
Usage: ./echo-server-fork-fdopen portno
% ./echo-server-fork-fdopen 1231 [←]
run telnet azalea20 1231 
[908] connection (fd==4) from 130.158.86.40:49424
[910] received (fd==4) 5 bytes, [012
]
[908] connection (fd==4) from 130.158.86.40:49425
[913] received (fd==4) 5 bytes, [abc
]
[913] received (fd==4) 5 bytes, [def
]
[913] connection (fd==4) closed.
[910] received (fd==4) 5 bytes, [345
]
[910] connection (fd==4) closed.
^C
% []
クライアント側(その1)。
% telnet azalea20 1231 [←]
Trying 130.158.86.40...
Connected to azalea20.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> ^D
Connection closed.
% []
クライアント側(その2)。
% telnet azalea20 1231 [←]
Trying 130.158.86.40...
Connected to azalea20.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc[←]
abc
def[←]
def
^]
telnet> ^D
Connection closed.
% []

図解

図1 サーバが accept() で接続要求を待っている

図1 サーバが accept() で接続要求を待っている

図2 サーバが accept()している時にクライアントが connect()した所

図2 サーバが accept()している時にクライアントが connect()した所

図3 accept() の結果、通信用ポート com が作られる

図3 accept() の結果、通信用ポート com が作られる

図4 fork() して、親子に別れる。

図4 fork() して、親子に別れる。

図5 親は、com を close()、子は acc を close() する

図5 親は、com を close()、子は acc を close() する

図6 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。

図6 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。

図7 サーバが別のクライアントから接続要求を受け付ける

図7 サーバが別のクライアントから接続要求を受け付ける

図8 accept() の結果、通信用ポート com が作られる

図8 accept() の結果、通信用ポート com が作られる

図9 fork() して、親子に別れる。

図9 fork() して、親子に別れる。

図10 親は、com を close()、子は acc を close() する

図10 親は、com を close()、子は acc を close() する

図11 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。

図11 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。

ゾンビ・プロセス

exit(2) システム・コールで終了したり、ソフトウェア割り込み(kill(2))で 強制終了させれたプロセスは、親プロセスが wait() するまで、形だけのこっ ている。このようなプロセスは、ゾンビ(Zombie)と呼ばれる。

ps コマンドで見ると、ゾンビの状態(STAT) は、Z と表示される。

% ps auxw | egrep echo [←]
yas        908   0.0 -0.0    36644    340  p2  S+    7:20PM   0:00.01 ./echo-server-fork-fdopen 1231
yas        913   0.0 -0.0        0      0  p2  Z+    1Jan70   0:00.00 (echo-server-fork)
yas        918   0.0 -0.0     8784      8  p0  R+    7:24PM   0:00.00 egrep echo
yas        910   0.0 -0.0        0      0  p2  Z+    1Jan70   0:00.00 (echo-server-fork)
% []
echo-server-fork-fdopen.c では、 delete_zombie() でゾンビを消そうとはしている。しかし、accept() で止まっ ている状態なので、どこかのクライアントから接続要求が来ない限りは、 delete_zombie() が呼ばれないので、ゾンビとして残っている。

この状態で接続要求が来ると、ゾンビが回収される。

% ./echo-server-fork-fdopen 1231 [←]
run telnet azalea20 1231 
[908] connection (fd==4) from 130.158.86.40:49424
[910] received (fd==4) 5 bytes, [012
]
[908] connection (fd==4) from 130.158.86.40:49425
[913] received (fd==4) 5 bytes, [abc
]
[913] received (fd==4) 5 bytes, [def
]
[913] connection (fd==4) closed.
[910] received (fd==4) 5 bytes, [345
]
[910] connection (fd==4) closed.
[908] connection (fd==4) from 130.158.86.40:49432
[908] zombi 913 deleted.
[908] zombi 910 deleted.
ゾンビが回収された後の ps コマンドの結果:
% ps auxw | egrep echo [←]
yas        908   0.0 -0.0    36644    352  p2  S+    7:20PM   0:00.01 ./echo-server-fork-fdopen 1231
yas        920   0.0 -0.0    36724    152  p2  S+    7:26PM   0:00.00 ./echo-server-fork-fdopen 1231
yas        922   0.0 -0.0     8784      8  p0  R+    7:26PM   0:00.00 egrep echo
% []

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

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

練習問題

練習問題(801) 複数のクライアントから要求を受け付けるHTTPサーバ

練習問題(706) 複数のクライアントから同時に接続を受けるようにしなさい。 この時、同時に扱えるクライアントの最大数を設定できるようにしなさい。た とえば最大2と設定した場合、2つの接続まで同時に処理を行うが、3つめが 来た場合、接続を切るか他の接続が閉じられるまで処理を遅延する。

練習問題(802) Keep Alive 対応の HTTPサーバ

練習問題(801) で、 HTTP の Keep-Alive に対応しなさい。

練習問題(803) ゾンビの即座な回収

echo-server-fork-fdopen.c では、accept() している途中で、子プロセスが終了するとゾンビになってし まう。子プロセスが終了したら直ちにゾンビを回収するようにしなさい。

ヒント:子プロセスの終了を、ソフトウェア割り込み(signal, SIGCHLD)で知 る方法もある。複数の子プロセスが終了しても、割り込みは1回しか起こらな いことがあることに注意しなさい。

ソフトウェア割込みを使うと、accept() システムコールがエラーで戻って来 るシステムもある。その場合、エラー番号が EINTR なら単純に終了しないで、 再び accept() に向うべきである。

練習問題(804) ゾンビの即座な回収ができる HTTPサーバ

練習問題(801) で、 練習問題(803) と同様に 割り込みを使って即座にゾンビを回収するようにしなさい。
Last updated: 2009/06/17 09:55:47
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>