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

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

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

連絡

仮想計算機に関する集中講義のお知らせ
開設学類 情報学群 情報科学類開設
科目番号 GB3 7001
授業科目名 情報システム特別講義A
標準履修年次 3・4年次
単位数 1単位
担当教員 河野 健二(慶應義塾大学 理工学部 准教授)
授業概要 「オペレーティングシステムの仮想化技術」この講義では、仮想機械モニタ (VMM) の仕組みを理解することを目標に VMware Workstation や Xenなどで用いられている実装技術について解説する。1990 年代後半からの技術進展をなぞる形で仮想化技術の肝となる部分について解き明かして行く。
日程 7月6日(水) 10:00-18:00
7月7日(木) 10:00-18:00
教室 総合研究棟B棟0112室
履修申請 平成23年6月24日(金)までTWINSで申請すること。
備考 (世話人教員)システム情報工学研究科 新城 靖

捕捉

データ構造とアルゴリズム

データ構造とアルゴリズムは相似。 HTTP のメッセージの構造は、トップレベルでは構造体。構造体を処理するよう に手続きを並べたい。 「行」は、「文字の繰り返し+行末\r\n」。繰り返しの処理は、ループであるが、 実際には、「行」単位で処理をする関数を呼べば良い。 「行」単位の処理の関数の内部には、ループが1個含まれる。

HTTP の内容(content)は、バイトの並び。バイト単位のループでもプログラムは書けるが、 遅いので、ある程度の大きさの塊(buffer size単位)で処理するのが普通。 テキストだけ扱うならば、行の並びと仮定しても問題ない。

snprintf()

snprintf() は、前半 に取り上げた。 使い方は、printf() と同じ。 注意: snprintf() のバッファサイズは、strlen() 等で計算するのではなく、 データに依存しない値(定数等)にすること。データに依存する値になると、バッ ファ・オーバフローの危険性が残る。少なくとも、プログラムが難解になり、 人目でバッファ・オーバフローが起きないことがわからない。

strcpy(), strcat() は論外。

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

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

HTTP のエラー

HTTP のエラーは、次のようになる。
$ telnet www 80 [←]
Trying 130.158.86.1...
Connected to www.coins.tsukuba.ac.jp.
Escape character is '^]'.
GET /.. HTTP/1.0[←]
[←]
HTTP/1.1 400 Bad Request
Date: Mon, 07 Jun 2010 11:26:36 GMT
Server: Apache
Content-Length: 226
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>
</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 {ipversion}
$ ./echo-server-nofork-fdopen 1231 [←]
run telnet cosmos10(v6) 1231 
[9082] connection (fd==4) from 130.158.86.150:58120
[9082] received (fd==4) 5 bytes, [012
]
^C
$
クライアント側(その1)。
$ telnet cosmos10 1231 [←]
Trying 130.158.86.150...
Connected to cosmos10.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
クライアント側(その2)。
$ telnet cosmos10 1231 [←]
Trying 130.158.86.150...
Connected to cosmos10.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc[←]
[]
クライアント(その2)は、クライアント(その1)が終了するまでサービス を受けられない。

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

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

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

$ telnet cosmos10 7 [←]
Trying 130.158.86.150...
Connected to cosmos10.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> ^D
Connection closed.
$ []
クライアント側(その2)。
$ telnet cosmos10 7 [←]
Trying 130.158.86.150...
Connected to cosmos10.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc[←]
abc
123[←]
123
^]
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 個 に増える。親子関係は複雑。

fork-fork-forkの親子関係

図? fork()システム・コールによるプロセスのコピー

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, int ip_version );
  18:	extern  void delete_zombie();
  19:	extern  void echo_receive_request_and_send_reply( int com );
  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  int  tcp_acc_port( int portno, int pf );
  24:	extern  int fdopen_sock( int sock, FILE **inp, FILE **outp );
  25:	
  26:	main( int argc, char *argv[] )
  27:	{
  28:	    int portno, ip_version;
  29:	        if( !(argc == 2 || argc==3) ) {
  30:	            fprintf(stderr,"Usage: %s portno {ipversion}\n",argv[0] );
  31:	            exit( 1 );
  32:	        }
  33:	        portno = strtol( argv[1],0,10 );
  34:	        if( argc == 3 )
  35:	            ip_version = strtol( argv[2],0,10 );
  36:	        else
  37:	            ip_version = 4; /* IPv4 by default */
  38:	        echo_server( portno, ip_version );
  39:	}
  40:	
main() は、 echo-server-nofork-fdopen.c と同じである。

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

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

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

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

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

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

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

他の関数は、 echo-server-nofork-fdopen.c と同じである。 次のように diff コマンドで調べると、ほとんど違いがないことがわかる。 diff コマンドの結果、「+」は、追加された行、 「-」は、削除された行、 「!」は、修正された行を意味する。

 $ diff -c echo-server-nofork-fdopen.c echo-server-fork-fdopen.c 
 *** echo-server-nofork-fdopen.c 2010-06-07 20:55:04.000000000 +0900
 --- echo-server-fork-fdopen.c   2010-06-07 20:55:10.000000000 +0900
 ***************
 *** 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() */
 ***************
 *** 15,20 ****
 --- 15,21 ----
   #include <string.h>   /* strlen() */

   extern        void echo_server( int portno, int ip_version );
 + extern        void delete_zombie();
   extern        void echo_receive_request_and_send_reply( int com );
   extern        void print_my_host_port( int portno );
   extern        void tcp_peeraddr_print( int com );
 ***************
 *** 41,46 ****
 --- 42,48 ----
   echo_server( int portno, int ip_version )
   {
       int acc,com ;
 +     pid_t child_pid ;
	 acc = tcp_acc_port( portno, ip_version );
	 if( acc<0 )
	     exit( -1 );
 ***************
 *** 54,63 ****
		 exit( -1 );
	     }
	     tcp_peeraddr_print( com );
 !           echo_receive_request_and_send_reply( com );
	 }
   }

   #define       BUFFERSIZE      1024

   void
 --- 56,90 ----
		 exit( -1 );
	     }
	     tcp_peeraddr_print( com );
 !           if( (child_pid=fork()) > 0 ) /* parent */
 !           {
 !               close( com );
 !           }
 !           else if( child_pid == 0 ) /* child */
 !           {
 !               close( acc );
 !               echo_receive_request_and_send_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 {ipversion}
$ ./echo-server-fork-fdopen 1231 [←]
run telnet cosmos10(v6) 1231 
[9388] connection (fd==4) from 130.158.86.150:58141
[9390] received (fd==4) 5 bytes, [012
]
[9388] connection (fd==4) from 130.158.86.150:58142
[9392] received (fd==4) 5 bytes, [abc
]
[9392] received (fd==4) 5 bytes, [def
]
[9392] connection (fd==4) closed.
[9390] received (fd==4) 5 bytes, [345
]
[9390] connection (fd==4) closed.
^C
$ []
クライアント側(その1)。
$ telnet cosmos10 1231 [←]
Trying 130.158.86.150...
Connected to cosmos10.coins.tsukuba.ac.jp.
Escape character is '^]'.
012[←]
012
345[←]
345
^]
telnet> ^D
Connection closed.
$ []
クライアント側(その2)。
$ telnet cosmos10 1231 [←]
Trying 130.158.86.150...
Connected to cosmos10.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 uxw|egrep echo [←]
yas  10120   0.0  0.0  2434760    352 s001  S+    9:18PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  10520   0.0  0.0  2435040    400 s000  R+    9:21PM   0:00.00 egrep echo
yas  10193   0.0  0.0        0      0 s001  Z+    9:18PM   0:00.00 (echo-server-fork)
yas  10156   0.0  0.0  2434760    192 s001  S+    9:18PM   0:00.00 ./echo-server-fork-fdopen 1231
$ []
echo-server-fork-fdopen.c では、 delete_zombie() でゾンビを消そうとはしている。しかし、accept() で止まっ ている状態なので、どこかのクライアントから接続要求が来ない限りは、 delete_zombie() が呼ばれないので、ゾンビとして残っている。

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

$ ./echo-server-fork-fdopen 1231 [←]
run telnet cosmos10(v6) 1231 
[10120] connection (fd==4) from 130.158.86.151:62127
[10156] received (fd==4) 5 bytes, [012
]
[10120] connection (fd==4) from 130.158.86.151:62128
[10193] received (fd==4) 5 bytes, [abc
]
[10193] received (fd==4) 5 bytes, [def
]
[10193] connection (fd==4) closed.
[10120] connection (fd==4) from 130.158.86.151:62130
[10120] zombi 10193 deleted.
ゾンビが回収された後の ps コマンドの結果:
$ ps uxw|egrep echo [←]
yas  10655   0.0  0.0  2434760    184 s001  S+    9:23PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  10156   0.0  0.0  2434760    192 s001  S+    9:18PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  10120   0.0  0.0  2434760    352 s001  S+    9:18PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  10713   0.0  0.0  2425524     92 s000  R+    9:23PM   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) と同様に 割り込みを使って即座にゾンビを回収するようにしなさい。

練習問題(805) IPv4とIPv6の両方に対応可能なechoサーバ

echo-server-fork-fdopen.cecho-server-pthread.c は、1度には、IPv4 か IPv6 かいずれかしか実行できない。 これを、IPv4 と IPv6 の両方に対応するように変更しなさい。

ヒント: IPv4 と IPv6 専用のプロセスやスレッドを作成する。

練習問題(806) IPv4とIPv6の両方に対応可能なechoサーバ(select)

echo-server-select.c は、1度には、IPv4 か IPv6 かいずれかしか実行できない。 これを、IPv4 と IPv6 の両方に対応するように変更しなさい。

ヒント: IPv4 の acc と IPv6 の acc を2つ作成し、 両方同時に監視する。

練習問題(807) IPv4とIPv6の両方に対応可能な HTTP サーバ

練習問題(801) で、IPv4 と IPv6 の両方に対応するようにしなさい。

なお同時に扱えるクライアント数は、IPv4 と IPv6 で個別に数えても 統合して数えてもどちらでも良い。たとえば、最大2と設定した場合、 個別に数えて IPv4 で 2, IPv6 で 2、合計 4 としてもよい。


Last updated: 2011/06/15 12:15:07
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>