筑波大学 システム情報工学研究科 
                                       コンピュータサイエンス専攻, 電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
	http://www.coins.tsukuba.ac.jp/~syspro/2008/No7.html
あるいは、次のページから手繰っていくこともできます。
	http://www.coins.tsukuba.ac.jp/~syspro/2008/
	http://www.coins.tsukuba.ac.jp/~yas/
   1:	
   2:	/*
   3:	        echo-server-nofork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork無し版)
   4:	        ~yas/syspro/ipc/echo-server-nofork-fdopen.c
   5:	        Created on 2004/05/09 19:08:47
   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 print_my_host_port( int portno );
  20:	extern  void tcp_peeraddr_print( int com );
  21:	extern  void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
  22:	extern  tcp_acc_port( int portno );
  23:	extern  int fdopen_sock( int sock, FILE **inp, FILE **outp );
  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_server( portno );
  38:	}
  39:	
引数として、ポート番号を取る。ポート番号が与えられなければ、そのプロセ
スの UID (User ID) から生成する。UID は、個人個人を識別するための番号
(16ビット程度、システムによっては 32 ビット)である。この課題では、(1台
のコンピュータでは)個人ごとに別々の UID を使う必要がある。上のように 
UID から生成する方法は、一般的ではない。(筑波大学情報学類のシステムで
はうまく働く。)
  40:	void
  41:	echo_server( int portno )
  42:	{
  43:	    int acc,com ;
  44:	        acc = tcp_acc_port( portno );
  45:	        if( acc<0 )
  46:	            exit( -1 );
  47:	        print_my_host_port( portno );
  48:	        while( 1 )
  49:	        {
  50:	            if( (com = accept( acc,0,0 )) < 0 )
  51:	            {
  52:	                perror("accept");
  53:	                exit( -1 );
  54:	            }
  55:	            tcp_peeraddr_print( com );
  56:	            echo_reply( com );
  57:	        }
  58:	}
  59:	
tcp_acc_port() は、引数で与えられたポート番号を使って接続要求受付用ポー トを作成し、そのファイル記述子(ソケット)を返す。 このファイル記述子は、クライアント側とは異なり、そまままでは通信に 用いることはできない。 print_my_host_port() は、telnet で接続する時のヒントを表示する。
サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。
accept() は、接続要求を待つシステムコールである。クライアントから接続 が来るまで、システムコールを実行したまま止まっているように見える。接続 要求が届くと、TCP/IP通信路の開設され、通信用ポートのファイル記述子が返 される。このファイル記述子は、クライアント側と同様に 標準入出力(0,1,2)や open() システム・コールの結果と同じもので、 ファイルに対する write() システムコールや read() システムコールの第一引数とし て使うことができる。つまり、write() システムコールを使うと、ネットワー クに対してデータを送り出すことができ、read() システムコールを使うとネッ トワークからデータを受け取ることができる。最後に不要になったら close() で解放する。
  60:	#define BUFFERSIZE      1024
  61:	
  62:	void
  63:	echo_reply( int com )
  64:	{
  65:	    char line[BUFFERSIZE] ;
  66:	    int rcount ;
  67:	    int wcount ;
  68:	    FILE *in, *out ;
  69:	
  70:	        if( fdopen_sock(com,&in,&out) < 0 )
  71:	        {
  72:	            fprintf(stderr,"fdooen()\n");
  73:	            exit( 1 );
  74:	        }
  75:	        while( fgets(line,BUFFERSIZE,in) )
  76:	        {
  77:	            rcount = strlen( line );
  78:	            printf("[%d] received (fd==%d) %d bytes, [%s]\n",getpid(),com,rcount,line );
  79:	            fflush( stdout );
  80:	            fprintf(out,"%s",line );
  81:	        }
  82:	        printf("[%d] connection (fd==%d) closed.\n",getpid(),com );
  83:	        fclose( in );
  84:	        fclose( out );
  85:	}
  86:	
この echo_reply() は、特定のクライアント専用のecho サービスを提供する。
クライアントからの要求が続く限り、動作する。
このプログラムでは、 クライアント側 と同様に fdopen_sock() を使って、通信可能なファイル記 述子 com から2つの FILE * を作成している。1つは、入力用、1つは出力 用である。その結果、 高水準入出力ライブラリ を使って通信が行えるようになっている。fprintf() で出力用の FILE * に書 き込むと、ネットワークに対してデータが送り出される。入力用の FILE * に fgets() を行うと、ネットワークからデータを受け取ることができる。
クライアントからの要求は、fgets() で読込んでいる。それを、サーバ側の端 末に printf() で表示している。fflush() は、printf() の内部のバッファ (stdoutのバッファ)に溜っているデータを書き出すものである。
クライアントには、fprintf() で結果を送り返している。
  87:	void
  88:	print_my_host_port( int portno )
  89:	{
  90:	    char hostname[100] ;
  91:	        gethostname( hostname,sizeof(hostname) );
  92:	        hostname[99] = 0 ;
  93:	        printf("run telnet %s %d \n",hostname, portno );
  94:	}
  95:	
gethostname() システムコールで自分自身のホスト名を取り出している。(正
式には、gethostname() の結果とインターネット的なホスト名(IPアドレスと
対応している))が一致して異ないことがある。
  96:	void
  97:	tcp_peeraddr_print( int com )
  98:	{
  99:	    struct sockaddr_storage addr ;
 100:	    socklen_t addr_len ; /* MacOSX: __uint32_t */
 101:	        addr_len = sizeof( addr );
 102:	        if( getpeername( com, (struct sockaddr *)&addr, &addr_len  )<0 )
 103:	        {
 104:	            perror("tcp_peeraddr_print");
 105:	            return;
 106:	        }
 107:	        printf("[%d] connection (fd==%d) from ",getpid(),com );
 108:	        sockaddr_print( (struct sockaddr *)&addr, addr_len );
 109:	        printf("\n");
 110:	}
 111:	
通信相手のアドレス(IPアドレスとポート番号)は、getpeername() システムコー
ルで得られる。得たアドレスを、sockaddr_print() に渡して表示している。
 112:	void
 113:	sockaddr_print( struct sockaddr *addrp, socklen_t addr_len )
 114:	{
 115:	    char host[BUFFERSIZE] ;
 116:	    char port[BUFFERSIZE] ;
 117:	        if( getnameinfo(addrp, addr_len, host, sizeof(host),
 118:	                        port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 )
 119:	            return;
 120:	        printf("%s:%s", host, port );
 121:	}
 122:	
IP アドレスは、IPv4 では、32 ビット(int)であり、
ポート番号は、16 ビット(sort)である。
ここでは、getnameinfo() ライブラリ関数を用いてホスト名とポート番号の
文字列表現に変換している。この時、NUMERIC と指定しいるので、
IPv4 では、ドット「.」で区切られた10進数4つになる。
getnameinfo() が使われている部分では、以前は、gethostbyaddr() が使われ ていた。
 123:	tcp_acc_port( int portno )
 124:	{
 125:	    struct sockaddr_in addr ;
 126:	    int addr_len ;
 127:	    int s ;
 128:	
 129:	        if( (s = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
 130:	        {
 131:	            perror("socket");
 132:	            return( -1 );
 133:	        }
 134:	
 135:	        memset( &addr, 0, sizeof(addr) );
 136:	        addr.sin_family = AF_INET ;
 137:	        addr.sin_addr.s_addr = INADDR_ANY ;
 138:	        addr.sin_port = htons( portno );
 139:	
 140:	        if( bind(s,(struct sockaddr *)&addr,sizeof(addr)) < 0 )
 141:	        {
 142:	            perror("bind");
 143:	            fprintf(stderr,"port number %d is already used. wait a moment or kill another program.\n", portno );
 144:	            return( -1 );
 145:	        }
 146:	        if( listen( s, 5 ) < 0 )
 147:	        {
 148:	            perror("listen");
 149:	            close( s );
 150:	            return( -1 );
 151:	        }
 152:	        return( s );
 153:	}
 154:	
まず、クライアント側と同様に、ソケットを、socket() システムコールで作成している。
PF_INET と SOCK_STREAMの組み合わせ
なので、TCP を使うことを意味する。
socket() の引数で、PF_INET の変りに、AF_INET と書いてもよい。ここでは、 Protocol を選んでいるので、PF_ が正しいが、実際には、PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われいる。
ソケットが作成できたら、bind() システムコールで、サーバ側で利用するア ドレス(IPアドレスとポート番号)を設定する。IP アドレスは、IPv4 では普通、 INADDR_ANY を指定する。複数の IP アドレスがある時には、どれに要求が来 ても受け付ける。特定の IP アドレスを指定すると、そのアドレスに来た要求 だけを受け付けるようになる。
ポート番号は、引数で与えられたものを、htons() でネットワーク・バイトオー ダに変換して与える。
次に、listen() システムコールにより、要求受け付けを開始する。第2引数 は、最大何個のクライアントを接続要求待ちで待たせるか(待ち行列の長さ) を指定する。 重たいサーバを設計する時には、キューの長さを調節する。Apache (WWW サー バ) などでは、500 程度になっていることがある。
注意:このプログラムには 複数のクライアントに対してサービスを同時に提供できない という問題がある。
bind() する addr を、getaddrinfo() で調べる流儀(IPv6風)もある。この場 合、getaddrinfo()の第一引数には、NULL を入れ、hints.ai_flags には AI_PASSIVE を設定する。
サーバ側。 サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。
注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。
% 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 1231 
run telnet azalea20.coins.tsukuba.ac.jp 1231 
[17311] connection (fd==4) from 130.158.86.40:55931
[17311] received (fd==4) 5 bytes, [123
]
[17311] received (fd==4) 5 bytes, [456
]
[17311] received (fd==4) 5 bytes, [789
]
[17311] connection (fd==4) closed.
^C
% 
クライアント側
% telnet azalea20.coins.tsukuba.ac.jp 1231  
Trying 130.158.86.40...
Connected to azalea20.coins.tsukuba.ac.jp.
Escape character is '^]'.
123
123
456
456
789
789
^]
telnet> quit
Connection closed.
% 
複数のクライアントに対してサービスの同時に提供するには次のような方法が ある。
ヒント:子プロセスの終了を、ソフトウェア割り込み(signal, SIGCHLD)で知 る方法もある。複数の子プロセスが終了しても、割り込みは1回しか起こらな いことがあることに注意しなさい。
ソフトウェア割込みを使うと、accept() システムコールがエラーで戻って来 るシステムもある。その場合、エラー番号が EINTR なら単純に終了しないで、 再び accept() に向うべきである。
% ./echo-server-fork 1231 
bind: Address already in use
port number 1231 is already used. wait a moment or kill another program.
% 
このエラーは、実際に同じポート番号を別のプロセスが使っている時にも出る
が、既に以前に使っていたプロセスが終了している時にも出ることがある。
後者の場合、次のオプションを付けると、この状態が緩和されることがある。
setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, 0, 0);この効果を確かめなさい。
また、同じポート番号の再利用を制限している理由を考えなさい。
クライアントから送られてくるデータを、コマンドの標準入力に接続して実行 するプログラムを作りなさい。たとえば、次のようなコマンドを実行すること を考える。
% ./spipe-last 1231 head -5 
<クライアントから送られてきた文字列のうち、先頭の5行だけがサーバの画面に表示される>
% 
spipe-last は、指定されたポート番号で tcp_acc_port() を行う。
クライアントからの接続要求を受け付けた後、不要なポートを閉じ、標準出力を切
り替えて、コマンドラインから指定されたコマンドを実行する。
すなわち、dup(), dup2(), close() などで、標準出力を切り替えて、
execve() などで、プログラムを実行する。
コマンドを実行したらそのまま終了する。
クライアントとしては、telnet を用いてもよい。 または、以下の spipe-first を用いることもできる。
この課題では、fork() を行わないことを推奨する。 TCP/IP なら本来双方向で使えるが、この練習問題では単方向だけを 使うことで、パイプの機能を代替する。
また、fork() とパイプを使う実現方法もある。コマンドを実行するする時に fork() して実行し、その標準入力をパイプにして、TCP/IP で受け取ったデー タをパイプに書き込んでもよい。
% ./spipe-first hostname port ls -l 
spipe-first は、指定されたホスト名とポート番号でtcp_connect() を行う。
不要なポートを閉じ、標準出力を切り替えた後、指定されたコマンドを実行す
る。すなわち、dup(), dup2(), close() などで、標準入出力を切り替えて、
execve() などで、プログラムを実行する。
コマンドを実行したらそのまま終了する。
この課題では、spipe-last と同様に、fork() を行わないことを推奨する。
% ./spipe-middle myport serverhost serverport sort +4 
spipe-middle は、第2引数と第3引数で指定されたサーバにtcp_connect() で接
続要求を行う。
tcp_acc_port() により、第1引数で指定された
ポート番号で接続受付用ポートを作成する。
spipe-first から接続要求を受け取ると、
不要なポートを閉じ、標準入出力を切り替える。そして、第4引数以降で指定さ
れたコマンドを実行する。
上の tcp_peeraddr_print() は、IP アドレスを数字で表示していた。 これを、ホスト名で表示するようにしなさい。
% ./http-response /index.html 
HTTP/1.0 200 OK
Content-Type: text/html
<HTML>
中略
</HTML>
% 
この課題では、セキュリティに気をつけなさい。必ず次の条件を満たすように
しなさい。
~/public_html)以下のファ
イルしかアクセスできないようにすること。それには、ファイルを開く
(open(), fopen())時に、頭に環境変数 HOME の値と "/public_html" を付加
する方法がある。(他の方法もある。)なお、~ は、open() システム・
コールや fopen() ライブラリ関数では使えない。~ は、シェルや 
emacs が独自にホーム・ディレクトリを調べて置き換えている。
.. が含まれていたら、アクセスを拒否すること。
.. は、1つ上のディレクトリを意味する。.. を許せば、
~/public_html 以外の場所、たとえば、
../Mail で、電子メールが盗まれる危険性がある。
| Content-Type: | 拡張子 | 
|---|---|
| text/html | .html | 
| text/plain | .txt | 
| image/gif | .gif | 
| image/jpeg | .jpeg | 
| image/png | .png | 
なお、この課題は、HTTP の応答を画面(stdout)に出力するが、ネットワーク通 信を一切行わない。
% cat > reqest.data 
GET /index.html HTTP/1.0
Accept: image/png,*/*;q=0.5
Accept-Language: ja,en;q=0.7,en-us;q=0.3
^D
% ./http-request-analyze < reqest.data 
/index.html
% 
このプログラムは、次のような動作を行う。
なお、この課題は、HTTP の要求を標準入力(stdin)から読むが、ネットワーク 通信を一切行わない。
この課題を行う場合、次のファイルに含まれる関数を利用してもよい。
~yas/syspro/string/string-split.c
int string_split( char *str, char del, int *countp, char ***vecp  )
int count;
char **vec;
    string_split( str,' ',&count, &vec );
ここで、count と vec は、
main() の引数の argc, argv と同じ形式になっている。
void free_string_vector( int count, char **vec )
string_split() で得た vec のメモリを解放する。
まず、 練習問題(710) を実施しなさい。 次に、 練習問題(711) を実施しなさい。 そして、その時作成した関数を用いて、HTTP サーバを作成しなさい。 この課題を行う場合、次のファイルに含まれる関数を利用してもよい。
~yas/syspro/ipc/http-server.c
学類内の計算機からの接続要求だけを受け付けるようにすることが望ましい。 たとえば、クライアントのIPアドレスを getpeername() で調べ、 130.158.86.0-130.158.87.255 の範囲についてのみアクセスを許可するように することが望ましい(この機能は、必須ではない)。
このような不正なデータを受け取ったとしても、サーバは耐える必要がある。 終了したり、バッファ・オーバーフローによる侵入を許してはならない。
実際の WWW ブラウザ (Firefoxなど)で、動作を確認しなさい。telnet だけで は、きちんと HTTP のプロトコルに従っているか確認できないので、不十分で ある。
.cgi のプログラムを CGI と見なし、プログラムを実行し、そ
の結果をクライアントに返しなさい。
finger サーバを作成しなさい。これは、受け取った文字列を引数にして、 finger コマンドを実行するとよい。そして、実行結果を、接続先に返すよう にするとよい。
finger コマンドのプロセスと finger サーバの間は、パイプで接続しなさい。 今までの例題で利用した方法を組み合わせることで実現することができるはず である(pipe(),fork(),dup(),close(),execve()など)。popen() ライブラリ 関数を利用してもよい。
finger コマンドのプロセスと finger サーバ間をパイプで結ぶ代わりに、 finger コマンドを exec する前に、配管工事をして、標準出力(1)をTCP/IPの ストリームに接続する方法もある。
受け取った文字列をそのまま popen() や system() などに渡すのは危険である。 というのも、これらの引数は、シェル(/bin/sh)が解釈するからである。 popen() や system() を使う場合には、 finger コ マンドが受付けるのに相応しいもの( isalpha() や isdigit() の並び)かどう かを検査しなさい。もし、シェルが解釈する特殊な文字列(|;<> など)が含まれていると、意図しないプログラムが実行させられることがある。
fork() や execve() を用いて直接 /usr/bin/finger を実行する方法はシェル を経由しないので、比較的安全である。
getpwnam() ライブラリ関数や utmp ファイルを用いて、finger コマンドと似たような 動きを実現してもよい。
HTTP proxy 作りなさい。次の機能を、1つ以上付けなさい。