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

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

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

今日の重要な話

復習

echoサーバ

TCP/IP のポート番号 7 (echo) では、受け取ったデータをそのまま返すサー ビスを提供している。以下は、これと同じような機能を提供するサーバである。

echo-server-nofork-fdopen.c

   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() で解放する。

tcp_peeraddr_print() は、通信相手の IP アドレスとポート番号を表示する関数である。

  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:	
print_my_host_port() は、telnet で接続する時のヒントを表示する。 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:	
 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:	
tcp_peeraddr_print() は、通信相手(peer)のアドレス(TCP/IPの場合、IPアド レスとポート番号)を表示する。通信相手のアドレスは、getpeername() システムコールで得られる。

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:	
tcp_acc_port() は、 通信路の開設 の仕事のうち、サーバ側で接続要求受付用ポートを作る関数である。 まず、クライアント側と同様に、ソケットを、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.
% []

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

複数のクライアントに対してサービスの同時に提供することが望ましい。 echo-server-nofork-fdopen.c は、1つのクライアントからの接続されると、そのクライアントに掛り切りに なって、他のクライアントにはサービスを提供できないという問題がある。 標準の echo サーバ(ポート番号 7 で動作している) は、複数のクライアント から接続されれた場合、同時にサービスを提供することができる。

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

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

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

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

snprintfによる文字列の操作

文字,文字列操作ライブラリ 復習しなさい。 fdopen_sock() を用いたプログラミ ングの場合、ネットワークに対する送信では、snprintf() は不用で、 fprintf() で十分な場合が多い。

Javaによるechoサーバ

練習問題

練習問題(701) ゾンビの効率的な消去

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

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

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

練習問題(702) SO_REUSEADDR

サーバを実行すると、次のようなエラーが出ることがある。
% ./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);
この効果を確かめなさい。

また、同じポート番号の再利用を制限している理由を考えなさい。

練習問題(703) DoS攻撃対策

echo-server-selec.cに含まれている \n を送ら ないことによるDoS攻撃に強くなるように、改良しなさい。

練習問題(704) poll() の利用

System V 系の Unix には select() システムコールがなく poll() システムコールだけが存在することがある。 echo-server-selec.cを poll() システムコールを使って書き換えなさい。

練習問題(705) spipe-last

クライアントから送られてくるデータを、コマンドの標準入力に接続して実行 するプログラムを作りなさい。たとえば、次のようなコマンドを実行すること を考える。

% ./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 で受け取ったデー タをパイプに書き込んでもよい。

練習問題(706) spipe-first

練習問題(705) spipe-last のクライアントを作成しなさい。 このクライアントは、引数で指定されたホスト名とポート番号のサーバに接続 する。そして、引数で指定されたコマンドを実行する。 たとえば、次のように実行された場合、spipe-first は、ls -l の結果を画面 に表示される代わりにサーバに送られるようにする。
% ./spipe-first hostname port ls -l [←]
spipe-first は、指定されたホスト名とポート番号でtcp_connect() を行う。 不要なポートを閉じ、標準出力を切り替えた後、指定されたコマンドを実行す る。すなわち、dup(), dup2(), close() などで、標準入出力を切り替えて、 execve() などで、プログラムを実行する。 コマンドを実行したらそのまま終了する。

この課題では、spipe-last と同様に、fork() を行わないことを推奨する。

練習問題(707) spipe-middle

練習問題(705) spipe-lastサーバと 練習問題(706) spipe-firstクライアントの 間に入るプロキシ spipe-middle を作成しなさい。 このプロキシは、次のような引数をとる。
% ./spipe-middle myport serverhost serverport sort +4 [←]
spipe-middle は、第2引数と第3引数で指定されたサーバにtcp_connect() で接 続要求を行う。 tcp_acc_port() により、第1引数で指定された ポート番号で接続受付用ポートを作成する。 spipe-first から接続要求を受け取ると、 不要なポートを閉じ、標準入出力を切り替える。そして、第4引数以降で指定さ れたコマンドを実行する。

練習問題(708) spipeでのクライアントとサーバの入れ替え

練習問題(705) spipe-last練習問題(706) spipe-firstで、 TCP/IPのクライアントとサーバを入れ替えてみなさい。 練習問題(707) spipe-middleも クライアントとサーバを入れ替えなさい。

練習問題(709) IPアドレスをホスト名に逆変換する

上の tcp_peeraddr_print() は、IP アドレスを数字で表示していた。 これを、ホスト名で表示するようにしなさい。

練習問題(710) HTTPサーバの応答

コマンドラインからファイル名を受け取り、画面に対して HTTPの応答を返すプ ログラムを作成しなさい。
% ./http-response /index.html [←]
HTTP/1.0 200 OK
Content-Type: text/html

<HTML>
中略
</HTML>
% []
この課題では、セキュリティに気をつけなさい。必ず次の条件を満たすように しなさい。 次のようなプログラムを作成すればよい。
  1. main() の引数 argv[1] をファイル名とする。
  2. 1. のファイル名を検査し、".." や "<" が含まれていないことを確認す る。含まれていたら、"400 Bad Request" のエラーの処理を行う。
  3. 1. のファイル名と ~/public_html からファイル名を作成する。 snprinf() を用いる。(strcat() は用いないこと。)
  4. ファイルを fopen() で開く。成功すれば、次の処理を行う。
    1. "200 OK" の行を fprintf() で stdout に出力する。
    2. Content-Type: の行を fprintf() で stdout に出力する。
    3. 空行を fprintf() で stdout に出力する。
    4. ファイルの内容をを fread() し、stdout に対して fwrite() で出力する。 ファイルの末尾まで繰り返す。
    5. fclose() でファイルを閉じる。
  5. ファイルがなければ、"404 Not Found" のエラー処理を行う。
エラーが生じた時、Apache は、Content-Type: text/html で HTML を出力して いる。これを Web ブラウザや telnet コマンドで確認しなさい。そして、それ と類似の結果を stdout に出力しなさい。

なお、この課題は、HTTP の応答を画面(stdout)に出力するが、ネットワーク通 信を一切行わない。

練習問題(711) HTTPサーバの要求解析

標準入力から HTTP の GET 要求を受け取り、要求行からファイル名を取り出す プログラムを作成しなさい。
% 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
% []
このプログラムは、次のような動作を行う。
  1. stdin から fgets() で 1 行読み込む。行末の \r や \n や \r\n を削除する。
  2. 最初の行を保存する。
  3. 2行目以降、空行が出てくるまで読み捨てる。
  4. 空行が出てくれば、ループを抜け、保存してある最初の行を解析する。
クライアントは、不正な要求行を送ってくる可能性がある。 そのような場合には、エラーにしなさい。 たとえば、次のようなデータを送ってくる可能性がある。 このような場合も、エラーとして扱い、プログラムがクラッシュしたり、バッ ファ・オーバーフローを起こしたりすることがないようにしなさい。

なお、この課題は、HTTP の要求を標準入力(stdin)から読むが、ネットワーク 通信を一切行わない。

練習問題(712) HTTPサーバ

まず、 練習問題(710) を実施しなさい。 次に、 練習問題(711) を実施しなさい。 そして、その時作成した関数を用いて、HTTP サーバを作成しなさい。

~yas/syspro/ipc/http-server.c に、常に同じ内容を返すHTTP サーバがある。 これを出発点にしてもよい。

学類内の計算機からの接続要求だけを受け付けるようにすることが望ましい。 たとえば、クライアントのIPアドレスを getpeername() で調べ、 130.158.86.0-130.158.87.255 の範囲についてのみアクセスを許可するように することが望ましい(この機能は、必須ではない)。

このような不正なデータを受け取ったとしても、サーバは耐える必要がある。 終了したり、バッファ・オーバーフローによる侵入を許してはならない。

実際の WWW ブラウザ (Firefoxなど)で、動作を確認しなさい。telnet だけで は、きちんと HTTP のプロトコルに従っているか確認できないので、不十分で ある。

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

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

練習問題(714) CGIが実行可能なhttpサーバ

練習問題(712) で、CGI により外部のプログラムを実行することができるようにしなさい。拡 張子が .cgi のプログラムを CGI と見なし、プログラムを実行し、そ の結果をクライアントに返しなさい。

練習問題(715) fingerサーバ

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 コマンドと似たような 動きを実現してもよい。

練習問題(716) HTTP Proxy

HTTP proxy 作りなさい。次の機能を、1つ以上付けなさい。

練習問題(717) サーバ自由課題

その他、HTTPサーバ、または、 HTTP Proxy と同程度以上の複 雑さを持つサーバを作成しなさい。
Last updated: 2007/05/29 21:05:03
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>