筑波大学 システム情報系 情報工学域
                                       新城 靖
                                       <yas@cs.tsukuba.ac.jp>
このページは、次の URL にあります。
	http://www.coins.tsukuba.ac.jp/~syspro/2013/2013-06-05
/index.html
あるいは、次のページから手繰っていくこともできます。
	http://www.coins.tsukuba.ac.jp/~syspro/2013/
	http://www.coins.tsukuba.ac.jp/~yas/
HTTP の内容(content)は、バイトの並び。バイト単位のループでもプログラムは書けるが、 遅いので、ある程度の大きさの塊(buffer size単位)で処理するのが普通。 テキストだけ扱うならば、行の並びと仮定しても問題ない。
$ 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:、空行、本文はしっかり返したい。
http_request_analyze( FILE *in, FILE *out ) {
    char *buf;
        buf = malloc( BUFSIZE );
	...
	/* free(buf) がない */
	return( 1 ); 
}
http_request_analyze( FILE *in, FILE *out ) {
    char *buf;
        buf = malloc( BUFSIZE );
	...
	if( ... ) {
	    ...
	    /* free(buf) がない */
	    return( 0 );
	}
	...
	free( buf );
	return( 1 ); 
}
http_request_analyze( FILE *in, FILE *out ) {
    FILE *f;
        f = fopen(...);
	...
	/* fclose( f ) がない */
	return( 1 ); 
}
http_request_analyze( FILE *in, FILE *out ) {
    FILE *f;
        f = fopen(...);
	...
	if( ... ) {
	    ...
	    /* fclose( f ) がない */
	    return( 0 );
	}
	...
	fclose( f );
	return( 1 ); 
}
以上のプログラムは、間違い。真似しないように。
http_request_analyze( FILE *in, FILE *out ) {
    auto char buf[BUFSIZE];
        ...
	return( 1 ); 
}
auto と書かなくても OK 。
http_request_analyze( FILE *in, FILE *out ) {
    char buf[BUFSIZE];
        ...
	return( 1 ); 
}
FREAD(3)                 BSD Library Functions Manual                 FREAD(3)
NAME
     fread, fwrite -- binary stream input/output
LIBRARY
     Standard C Library (libc, -lc)
SYNOPSIS
     #include <stdio.h>
     size_t
     fread(void *restrict ptr, size_t size, size_t nitems,
         FILE *restrict stream);
型を合わせるために、ポインタ変数を使わなくてもよい。
    char *buf;
        buf = malloc( BUFSIZE );
	...
	fread( buf, 1, BUFSIZE, in );
以下でも OK。C言語で配列変数の名前を書くと、先頭要素のポインタの意味に
なる。
    char buf[BUFSIZE];
    	...
	fread( buf, 1, BUFSIZE, in );
以下でも同じ。
    char buf[BUFSIZE];
    	...
	fread( &buf[0], 1, BUFSIZE, in );
    char buf[BUFSIZE];
    	...
	fread( &buf, 1, BUFSIZE, in ); /* 普通は使わない。*/
サーバ側
$ 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)が終了するまでサービス
を受けられない。
クライアント側(その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.
$ 
   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()システム・コールによるプロセスのコピー
   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
と同じである。
  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 が表示されているが、同じではないことに 注意しなさい。
  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 オプションを付けてあるので、終了した子プロセスがい
なければ、待たずに返ってくる。
+」は、追加された行、
「-」は、削除された行、
「!」は、修正された行を意味する。
 $ 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
サーバ側。 サーバは、終了しないので、最後に、^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() で接続要求を待っている
図2 サーバが accept()している時にクライアントが connect()した所
図3 accept() の結果、通信用ポート com が作られる
図4 fork() して、親子に別れる。
図5 親は、com を close()、子は acc を close() する
図6 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。
図7 サーバが別のクライアントから接続要求を受け付ける
図8 accept() の結果、通信用ポート com が作られる
図9 fork() して、親子に別れる。
図10 親は、com を close()、子は acc を close() する
図11 親は、再び accept() で待ち、子は、特定のクライアントに対して read()/write() する。
$ ./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
$ 
実行結果として、サーバ側で、複数同時に受け付けていることを示すログをつ けなさい。複数接続できることを確認するには、telnet のように遅いクライア ントを用いるとよい。単に Web ブラウザで複数のページを開けても、通信その ものは短時間で終わるので、複数接続できることの確認にはならない。
注意: 上限を設定する目的は、攻撃等でサーバの CPU 時間が奪われることやプ ロセスが増えすぎることを防ぐことにある。プロセスの終了を busy wait で待っ てはならない。待つならば、accept() や wait() のように、CPU 時間を消費し ない形で待つようにしなさい。
Keep-Alive に対応しなさい。
ヒント:子プロセスの終了を、ソフトウェア割り込み(signal, SIGCHLD)で知 る方法もある。複数の子プロセスが終了しても、割り込みは1回しか起こらな いことがあることに注意しなさい。
ソフトウェア割込みを使うと、accept() システムコールがエラーで戻って来 るシステムもある。その場合、エラー番号が EINTR なら単純に終了しないで、 再び accept() に向うべきである。
ヒント: IPv4 と IPv6 専用のプロセスやスレッドを作成する。
ヒント: IPv4 の acc と IPv6 の acc を2つ作成し、 両方同時に監視する。
なお同時に扱えるクライアント数は、IPv4 と IPv6 で個別に数えても 統合して数えてもどちらでも良い。たとえば、最大2と設定した場合、 個別に数えて IPv4 で 2, IPv6 で 2、合計 4 としてもよい。