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

                                       筑波大学 システム情報系 情報工学域
                                       新城 靖
                                       <yas@cs.tsukuba.ac.jp>

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

捕捉

名前とコメント

例題をコピーする時の注意点

練習しないと、上達しない。

HTTPの要求での Host: の役割

1つの httpd サーバで、複数のドメイン名の Web サーバを兼ねていることがあ る。この機能を、Apache httpd では、仮想ホスト( バーチャルホスト、 virtual host)と呼んでいる。

仮想ホストの実現方法には、2種類ある。

名前ベースの仮想ホストを使っているような Web サーバでは、HTTP の要求で、 Host: という行も送信する必要がある。Host: がないと、エラーの応答 "HTTP/1.0 302 Found" が返されることがある。

例:

http://www.example.com/index.html
GET /index.html HTTP/1.0←↓
Host: www.example.com←↓
←↓

C言語のexternの働き

関数の型宣言については、extern を付けてもつけなくても同じ。
extern  void echo_server( int portno, int ip_version );
void echo_server( int portno, int ip_version );
変数については、次のような使い方をするのがよい。

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

データ構造とアルゴリズムは相似。 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 
[15980] accepting incoming connections (fd==3) ...
[15980] connection (fd==4) from 130.158.86.150:57562
[15980] 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:	            printf("[%d] accepting incoming connections (fd==%d) ...\n",getpid(),acc );
  54:	            if( (com = accept( acc,0,0 )) < 0 )
  55:	            {
  56:	                perror("accept");
  57:	                exit( -1 );
  58:	            }
  59:	            tcp_peeraddr_print( com );
  60:	            if( (child_pid=fork()) > 0 ) /* parent */
  61:	            {
  62:	                close( com );
  63:	            }
  64:	            else if( child_pid == 0 ) /* child */
  65:	            {
  66:	                close( acc );
  67:	                echo_receive_request_and_send_reply( com );
  68:	                exit( 0 );
  69:	            }
  70:	            else
  71:	            {
  72:	                perror("fork");
  73:	                exit( -1 );
  74:	            }  
  75:	        }
  76:	}
  77:	

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() は、終了した子プロセスの残骸を回収する関数である。
  78:	void
  79:	delete_zombie()
  80:	{
  81:	    pid_t pid ;
  82:	    while( (pid=wait4(-1,0,WNOHANG,0)) >0 )
  83:	    {
  84:	        printf("[%d] zombi %d deleted.\n",getpid(),pid );
  85:	        continue;
  86:	    }
  87:	}
  88:	
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	2012-05-28 15:12:48.000000000 +0900
 --- echo-server-fork-fdopen.c	2012-06-04 15:05:44.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,52 ****
 --- 42,55 ----
   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 );
	 print_my_host_port( portno );
	 while( 1 )
	 {
 + 	    delete_zombie();
	     printf("[%d] accepting incoming connections (fd==%d) ...\n",getpid(),acc );
	     if( (com = accept( acc,0,0 )) < 0 )
	     {
 ***************
 *** 54,63 ****
		 exit( -1 );
	     }
	     tcp_peeraddr_print( com );
 ! 	    echo_receive_request_and_send_reply( com );
	 }
   }

   #define	BUFFERSIZE	1024

   void
 --- 57,91 ----
		 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 
[17071] accepting incoming connections (fd==3) ...
[17071] connection (fd==4) from 130.158.86.150:57741
[17071] accepting incoming connections (fd==3) ...
[17073] received (fd==4) 5 bytes, [012
]
[17071] connection (fd==4) from 130.158.86.1:54330
[17071] accepting incoming connections (fd==3) ...
[17074] received (fd==4) 5 bytes, [abc
]
[17074] received (fd==4) 5 bytes, [def
]
[17073] received (fd==4) 5 bytes, [345
]
[17073] connection (fd==4) closed.
[17074] 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> quit[←]
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> quit[←]
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)と呼ばれる。

$ ./echo-server-fork-fdopen  1231 [←]
run telnet cosmos10(v6) 1231 
[17103] accepting incoming connections (fd==3) ...
[17103] connection (fd==4) from 130.158.86.150:57745
[17103] accepting incoming connections (fd==3) ...
[17111] received (fd==4) 5 bytes, [012
]
[17103] connection (fd==4) from 130.158.86.1:54390
[17103] accepting incoming connections (fd==3) ...
[17112] received (fd==4) 5 bytes, [abc
]
[17112] received (fd==4) 5 bytes, [def
]
[17112] connection (fd==4) closed.
ps コマンドで見ると、ゾンビの状態(STAT) は、Z と表示される。

$ ps uxw|egrep echo [←]
yas  17112   0.0  0.0        0      0 s003  Z+    3:21PM   0:00.00 (echo-server-fork)
yas  17111   0.0  0.0  2434840    192 s003  S+    3:21PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  17103   0.0  0.0  2434840    364 s003  S+    3:20PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  17145   0.0  0.0  2426928    328   p1  R+    3:22PM   0:00.00 egrep echo
$ []
echo-server-fork-fdopen.c では、 delete_zombie() でゾンビを消そうとはしている。しかし、accept() で止まっ ている状態なので、どこかのクライアントから接続要求が来ない限りは、 delete_zombie() が呼ばれないので、ゾンビとして残っている。

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

[17103] connection (fd==4) from 130.158.86.1:54408
[17103] zombi 17112 deleted.
[17103] accepting incoming connections (fd==3) ...
ゾンビが回収された後の ps コマンドの結果:
$ ps uxw|egrep echo [←]
yas  17152   0.0  0.0  2434840    184 s003  S+    3:23PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  17111   0.0  0.0  2434840    192 s003  S+    3:21PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  17103   0.0  0.0  2434840    364 s003  S+    3:20PM   0:00.00 ./echo-server-fork-fdopen 1231
yas  17157   0.0  0.0  2435120    452   p1  S+    3:25PM   0:00.00 egrep echo
$ []

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

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

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

練習問題

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

練習問題(706) 複数のクライアントから同時に接続を受けるようにしなさい。

実行結果として、サーバ側で、複数同時に受け付けていることを示すログをつ けなさい。複数接続できることを確認するには、telnet のように遅いクライア ントを用いるとよい。単に Web ブラウザで複数のページを開けても、通信その ものは短時間で終わるので、複数接続できることの確認にはならない。

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

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

注意: 上限を設定する目的は、攻撃等でサーバの CPU 時間が奪われることやプ ロセスが増えすぎることを防ぐことにある。プロセスの終了を busy wait で待っ てはならない。待つならば、accept() や wait() のように、CPU 時間を消費し ない形で待つようにしなさい。

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

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

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

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

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

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

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

練習問題(801) で、 練習問題(804) と同様に 割り込みを使って即座にゾンビを回収するようにしなさい。

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

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

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

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

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

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

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

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

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


Last updated: 2012/06/18 12:37:58
Yasushi Shinjo / <yas@cs.tsukuba.ac.jp>