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


電子・情報工学系/システム情報工学研究科CS専攻
新城 靖
<yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2005/No7.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2005/
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:	#define _USE_BSD        /* wait4() */
   8:	#include <stdio.h>
   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:	
  16:	extern  void echo_server( int portno );
  17:	extern  void echo_reply( int com );
  18:	extern  void print_my_host_port( int portno );
  19:	extern  void tcp_peeraddr_print( int com );
  20:	extern  void sockaddr_print( struct sockaddr *addrp, int addr_len );
  21:	extern  tcp_acc_port( int portno );
  22:	extern  int fdopen_sock( int sock, FILE **inp, FILE **outp );
  23:	
  24:	main( int argc, char *argv[] )
  25:	{
  26:	    int portno ;
  27:	        if( argc >= 3 )
  28:	        {
  29:	            fprintf( stdout,"Usage: %s [portno] \n",argv[0] );
  30:	            exit( -1 );
  31:	        }
  32:	        if( argc == 2 )
  33:	            portno = strtol( argv[1],0,10 );
  34:	        else
  35:	            portno = getuid();
  36:	        echo_server( portno );
  37:	}
  38:	
引数として、ポート番号を取る。ポート番号が与えられなければ、そのプロセ スの UID (User ID) から生成する。UID は、個人個人を識別するための番号 (16ビット程度、システムによっては 32 ビット)である。この課題では、(1台 のコンピュータでは)個人ごとに別々の UID を使う必要がある。上のように UID から生成する方法は、一般的ではない。(筑波大学情報学類のシステムで はうまく働く。)

  39:	void
  40:	echo_server( int portno )
  41:	{
  42:	    int acc,com ;
  43:	        acc = tcp_acc_port( portno );
  44:	        if( acc<0 )
  45:	            exit( -1 );
  46:	        print_my_host_port( portno );
  47:	        while( 1 )
  48:	        {
  49:	            if( (com = accept( acc,0,0 )) < 0 )
  50:	            {
  51:	                perror("accept");
  52:	                exit( -1 );
  53:	            }
  54:	            tcp_peeraddr_print( com );
  55:	            echo_reply( com );
  56:	        }
  57:	}
  58:	

tcp_acc_port() は、引数で与えられたポート番号を使って接続要求受付用ポー トを作成し、そのファイル記述子(ソケット)を返す。 このファイル記述子は、クライアント側とは異なり、そまままでは通信に 用いることはできない。 print_my_host_port() は、telnet で接続する時のヒントを表示する。

サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。

accept() は、接続要求を待つシステムコールである。クライアントから接続 が来るまで、システムコールを実行したまま止まっているように見える。接続 要求が届くと、TCP/IP通信路の開設され、通信用ポートのファイル記述子が返 される。このファイル記述子は、クライアント側と同様に 標準入出力(0,1,2)や open() システム・コールの結果と同じもので、 ファイルに対する write() システムコールや read() システムコールの第一引数とし て使うことができる。つまり、write() システムコールを使うと、ネットワー クに対してデータを送り出すことができ、read() システムコールを使うとネッ トワークからデータを受け取ることができる。最後に不要になったら close() で解放する。

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

  59:	#define BUFFERSIZE      1024
  60:	
  61:	void
  62:	echo_reply( int com )
  63:	{
  64:	    char line[BUFFERSIZE] ;
  65:	    int rcount ;
  66:	    int wcount ;
  67:	    FILE *in, *out ;
  68:	
  69:	        if( fdopen_sock(com,&in,&out) < 0 )
  70:	        {
  71:	            fprintf(stderr,"fdooen()\n");
  72:	            exit( 1 );
  73:	        }
  74:	        while( fgets(line,BUFFERSIZE,in) )
  75:	        {
  76:	            rcount = strlen( line );
  77:	            printf("[%d] received (fd==%d) %d bytes, [%s]\n",getpid(),com,rcount,line );
  78:	            fflush( stdout );
  79:	            fprintf(out,"%s",line );
  80:	        }
  81:	        printf("[%d] connection (fd==%d) closed.\n",getpid(),com );
  82:	        fclose( in );
  83:	        fclose( out );
  84:	}
  85:	
この echo_reply() は、特定のクライアント専用のecho サービスを提供する。 クライアントからの要求が続く限り、動作する。

このプログラムでは、 クライアント側 と同様に fdopen_sock() を使って、通信可能なファイル記 述子 com から2つの FILE * を作成している。1つは、入力用、1つは出力 用である。その結果、 高水準入出力ライブラリ を使って通信が行えるようになっている。fprintf() で出力用の FILE * に書 き込むと、ネットワークに対してデータが送り出される。入力用の FILE * に fgets() を行うと、ネットワークからデータを受け取ることができる。

クライアントからの要求は、fgets() で読込んでいる。それを、サーバ側の端 末に printf() で表示している。fflush() は、printf() の内部のバッファ (stdoutのバッファ)に溜っているデータを書き出すものである。

クライアントには、fprintf() で結果を送り返している。

  86:	void
  87:	print_my_host_port( int portno )
  88:	{
  89:	    char hostname[100] ;
  90:	        gethostname( hostname,sizeof(hostname) );
  91:	        hostname[99] = 0 ;
  92:	        printf("run telnet %s %d \n",hostname, portno );
  93:	}
  94:	
print_my_host_port() は、telnet で接続する時のヒントを表示する。 gethostname() システムコールで自分自身のホスト名を取り出している。(正 式には、gethostname() の結果とインターネット的なホスト名(IPアドレスと 対応している))が一致して異ないことがある。

  95:	void
  96:	tcp_peeraddr_print( int com )
  97:	{
  98:	    struct sockaddr_storage addr ;
  99:	    int addr_len ;
 100:	        addr_len = sizeof( addr );
 101:	        if( getpeername( com, (struct sockaddr *)&addr, &addr_len  )<0 )
 102:	        {
 103:	            perror("tcp_peeraddr_print");
 104:	            return;
 105:	        }
 106:	        printf("[%d] connection (fd==%d) from ",getpid(),com );
 107:	        sockaddr_print( (struct sockaddr *)&addr, addr_len );
 108:	        printf("\n");
 109:	}
 110:	
 111:	void
 112:	sockaddr_print( struct sockaddr *addrp, int addr_len )
 113:	{
 114:	    char host[BUFFERSIZE] ;
 115:	    char port[BUFFERSIZE] ;
 116:	        if( getnameinfo(addrp, addr_len, host, sizeof(host),
 117:	                        port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 )
 118:	            return;
 119:	        printf("%s:%s", host, port );
 120:	}
 121:	
tcp_peeraddr_print() は、通信相手(peer)のアドレス(TCP/IPの場合、IPアド レスとポート番号)を表示する。通信相手のアドレスは、getpeername() システムコールで得られる。

IP アドレスは、IPv4 では、32 ビット(int)であり、 ポート番号は、16 ビット(sort)である。 ここでは、getnameinfo() ライブラリ関数を用いてホスト名とポート番号の 文字列表現に変換している。この時、NUMERIC と指定しいるので、 IPv4 では、ドット「.」で区切られた10進数4つになる。

getnameinfo() が使われている部分では、以前は、gethostbyaddr() が使われ ていた。

 122:	tcp_acc_port( int portno )
 123:	{
 124:	    struct sockaddr_in addr ;
 125:	    int addr_len ;
 126:	    int s ;
 127:	
 128:	        if( (s = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
 129:	        {
 130:	            perror("socket");
 131:	            return( -1 );
 132:	        }
 133:	
 134:	        memset( &addr, 0, sizeof(addr) );
 135:	        addr.sin_family = AF_INET ;
 136:	        addr.sin_addr.s_addr = INADDR_ANY ;
 137:	        addr.sin_port = htons( portno );
 138:	
 139:	        if( bind(s,(struct sockaddr *)&addr,sizeof(addr)) < 0 )
 140:	        {
 141:	            perror("bind");
 142:	            fprintf(stderr,"port number %d is already used. wait a moment or kill another program.\n", portno );
 143:	            return( -1 );
 144:	        }
 145:	        if( listen( s, 5 ) < 0 )
 146:	        {
 147:	            perror("listen");
 148:	            close( s );
 149:	            return( -1 );
 150:	        }
 151:	        return( s );
 152:	}
 153:	
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 adonis9.coins.tsukuba.ac.jp 1231 
[6419] connection (fd==4) from 130.158.86.28:47329
[6419] received (fd==4) 5 bytes, [123
]
[6419] received (fd==4) 5 bytes, [456
]
[6419] received (fd==4) 5 bytes, [790
]
[6419] connection (fd==4) closed.
^C
% []
クライアント側
% telnet adonis9.coins.tsukuba.ac.jp 1231  [←]
Trying 130.158.86.29...
Connected to adonis9.coins.tsukuba.ac.jp.
Escape character is '^]'.
123[←]
123
456[←]
456
790[←]
790
^][←]
telnet> quit[←]
Connection closed.
% []

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

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

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

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

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

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

snprintf()

strcpy(), strcat() の代りに snprintf() が便利な場合がある。
   1:	/*
   2:	        string-snprintf.c -- snprintf を使った文字列のコピーと結合
   3:	        ~yas/syspro/string/string-snprintf.c
   4:	        Start: 2002/04/21 23:02:39
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr, snprintf() */
   8:	#include <stdlib.h> /* malloc() */
   9:	
  10:	main()
  11:	{
  12:	    char buf[100];
  13:	        if( snprintf(buf,100,"hello%s\n",",world") >=sizeof(buf) )
  14:	        {
  15:	            fprintf(stderr,"buffer overflow\n");
  16:	            exit( 1 );
  17:	        }
  18:	        printf("%s",buf );
  19:	}
  20:	
snprintf() は、第1引数に、バッファの番地、第2引数にバッファの長さ(最 後の0を含む)をとる。第3引数以降は、printf() と同じである。snprintf() では、けっしてバッファ・オーバーフローは起きない。起きそうになると、負 の数を返す。(成功すると、バイト数(最後の0は含まない)を返す。)

snprintf() は、%s 以外に %d や %c も使える。

注意:Linux (Glibc を使っている)では、snprintf() の仕様が変更された。 古い仕様(glibc 2.0.6以前)では、snprinf() は、エラーが起きると -1 を返 す。新しい仕様(glibc 2.1以降)では、snprintf() は、必要なバイト数、すな わち、バッファが無限大であるときに書き込まれる文字列の長さ(最後の0を除 く)を返す。これは、strlen() が返すものと同じである。従って、新しい snprintf() では、結果とバッファの大きさを比較して正確に書き込まれたか を検査する。(snprintf() は、決してバッファ・オーバーフローを起こすこと はないが、バッファが足りない時には意図していない結果が保存されているこ とになる。)

古い Unix や古い Linux では snprintf は sprintf を呼び出しているだけのことがあり、安全ではないことがある。 coins の環境は問題ない。

snprintf()によるstr*() の置き換え

strcpy(dst,src)
snprintf(dst,destsize,"%s",src)
strcat(dest,src)
snprintf(buf,bufsize,"%s%s",dest,src) (考え方の転換が必要)
fdopen_sock() を用いたプログラミ ングの場合、ネットワークに対する送信では、snprintf() は不用で、 fprintf() で十分な場合が多い。

echoサーバ(Java版)

   1:	/*
   2:	        EchoServerSingle.java -- 文字列を送受信するサーバ(TCP/IP, Java版, スレッドなし)
   3:	        ~yas/syspro/ipc/EchoServer.java
   4:	        Created on 2004/05/09 20:00:24
   5:	*/
   6:	
   7:	import java.net.*;
   8:	import java.io.*;
   9:	
  10:	class EchoServerSingle
  11:	{
  12:	    public static void main(String argv[]) throws IOException {
  13:	        if( argv.length != 1 )
  14:	        {
  15:	            System.err.println("Usage: % java EchoServer port");
  16:	            System.exit( -1 );
  17:	        }
  18:	        int portno = Integer.parseInt( argv[0] );
  19:	        echo_server( portno );
  20:	    }
引数として、ポート番号を取る。

  22:	    public static void echo_server( int portno ) throws IOException
  23:	    {
  24:	        ServerSocket acc = new ServerSocket( portno );
  25:	        print_my_host_port( portno );
  26:	        while( true )
  27:	        {
  28:	            Socket com = acc.accept();
  29:	            tcp_peeraddr_print( com );
  30:	            EchoServerWorker esw = new EchoServerWorker(com);
  31:	            esw.run();
  32:	        }
  33:	    }

ServerSocket() は、引数で与えられたポート番号を使って接続要求受付用ポー トを作成し、それに対応した ServerSocket のオブジェクトを返す。このオブ ジェクトは、Socket のオブジェクトとは異なり、そまままでは通信に用いる ことはできない。

print_my_host_port() は、telnet で接続する時のヒントを表示する。

サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。

accept() は、接続要求を待つメソッドである。クライアントから接続が来る まで、システムコールを実行したまま止まっているように見える。接続要求が 届くと、TCP/IP通信路の開設され、通信用ポートに対応したSocket クラスの オブジェクトが返される。このオブジェクトは、クライアント側と同様に getInputStream() や getOutputStream() により入出力可能なストリームを作 り出すことができる。

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

以後の仕事は、EchoServerWorker というクラスのオブジェクトを作成して行 わせている。コンストラクタでは、Socket クラスのオブジェクトを渡してい だけであり、実際の処理は何も行われない。実際の処理は、run() メソッドで 行われる。

  34:	    public static void print_my_host_port( int portno ) throws UnknownHostException
  35:	    {
  36:	        InetAddress ia = java.net.InetAddress.getLocalHost();
  37:	        String hostname = ia.getHostName();
  38:	        stdout.println("run telnet "+hostname+" "+portno );
  39:	    }
  40:	    public static void tcp_peeraddr_print( Socket com )
  41:	    {
  42:	        InetSocketAddress isa = (InetSocketAddress)com.getRemoteSocketAddress();
  43:	        InetAddress ia = isa.getAddress();
  44:	        String peerhostaddr = ia.getHostAddress();
  45:	        int peerportno = isa.getPort();
  46:	        stdout.println("connection (hash=="+com.hashCode()+") from "+peerhostaddr+":"+peerportno );
  47:	    }

print_my_host_port() と tcp_peeraddr_print() は、それぞれ同じ同名のC の関数と同じ働きをする。

   1:	/*
   2:	        EchoServerWorker.java -- 文字列を送受信するサーバ/ワーカ(TCP/IP, Java版)
   3:	        ~yas/syspro/ipc/EchoServerWorker.java
   4:	*/
   5:	
   6:	import java.net.*;
   7:	import java.io.*;
   8:	
   9:	public class EchoServerWorker implements Runnable
  10:	{
  11:	    Socket com ;
  12:	    EchoServerWorker( Socket com )
  13:	    {
  14:	        this.com = com ;
  15:	    }
  16:	    public void run()
  17:	    {
  18:	        try
  19:	        {
  20:	            BufferedReader in = new BufferedReader(
  21:	                new InputStreamReader( com.getInputStream() ));
  22:	            PrintStream out = new PrintStream( com.getOutputStream() );
  23:	            String line;
  24:	            while( (line = in.readLine())!= null )
  25:	            {
  26:	                stdout.println("received (hash=="+com.hashCode()+") "+
  27:	                               line.length()+" characters, ["+line+"]");
  28:	                out.println( line );
  29:	            }
  30:	            stdout.println("connection (hash=="+com.hashCode()+") closed.");
  31:	            in.close();
  32:	            out.close();
  33:	            com.close();
  34:	        }
  35:	        catch( IOException e )
  36:	        {
  37:	            stderr.println( e );
  38:	        }
  39:	    }
  40:	    static java.io.BufferedReader stdin = 
  41:	        new java.io.BufferedReader( new java.io.InputStreamReader(System.in) );
  42:	    static java.io.PrintStream stdout = System.out;
  43:	    static java.io.PrintStream stderr = System.err;     
  44:	}
echoサービスのクライアント と同様に、Socket から BufferedReader と PrintStream を生成している。 そして、readLine() や println() により通信を行っている。

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

わずかな修正で、スレッド対応になり、複数のクライアントに対してサービス を同時に提供できるようになる。
 % diff -c EchoServerSingle.java EchoServer.java
 *** EchoServerSingle.java       Sun May  9 20:01:50 2004
 --- EchoServer.java     Sun May  9 19:59:33 2004
 ***************
 *** 1,13 ****
   /*
 !       EchoServerSingle.java -- 文字列を送受信するサーバ(TCP/IP, Java版, スレッドなし)
	 ~yas/syspro/ipc/EchoServer.java
 !       Created on 2004/05/09 20:00:24
   */

   import java.net.*;
   import java.io.*;

 ! class EchoServerSingle
   {
       public static void main(String argv[]) throws IOException {
	 if( argv.length != 1 )
 --- 1,13 ----
   /*
 !       EchoServer.java -- 文字列を送受信するサーバ(TCP/IP, Java版)
	 ~yas/syspro/ipc/EchoServer.java
 !       Created on 2004/02/14 16:22:13
   */

   import java.net.*;
   import java.io.*;

 ! class EchoServer
   {
       public static void main(String argv[]) throws IOException {
	 if( argv.length != 1 )
 ***************
 *** 27,34 ****
	 {
	     Socket com = acc.accept();
	     tcp_peeraddr_print( com );
 !           EchoServerWorker esw = new EchoServerWorker(com);
 !           esw.run();
	 }
       }
       public static void print_my_host_port( int portno ) throws UnknownHostException
 --- 27,34 ----
	 {
	     Socket com = acc.accept();
	     tcp_peeraddr_print( com );
 !           Thread th = new Thread( new EchoServerWorker(com) );
 !           th.start();
	 }
       }
       public static void print_my_host_port( int portno ) throws UnknownHostException
 % 

練習問題

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

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

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

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

練習問題(48) 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);
この効果を確かめなさい。

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

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

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

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

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

練習問題(51) TCP/IPのストリームとpipe()(1)

pipe() の代りに、TCP/IP のストリームを使って、異なるホスト上のプロセス を接続して、フィルタの処理を行わせてみなさい。

プログラムを1つだけ作るのではなく、tcp_connect() をするものと tcp_acc_port() をするものの2つを作るとよい。tcp_acc_port() をする側で は、fork() をして、複数のクライアントに対応するのではなく、通 信要求受付用の socket() を close() して、1つの通信先に専念するものでよい。

この課題では、fork() は、しなくてもよい。パイプは、fork() でできた親子 関係のあるプロセス間で通信を行うものであるが、TCP/IPでは、親子関係がな いプロセス間で通信ができる。

TCP/IP なら本来双方向で使える。しかし、この練習問題だと単方向だけを使 う。こうすると、パイプと同じような使い方ができる。

練習問題(52) TCP/IPのストリームとpipe()(2)

練習問題(51) で、3つ以上のプロセスを接続できるようにしなさい。

プログラムを1つだけ作るのではなく、先頭用、中間用、末尾用の3つを作る とよい。中間用のものは、うまく作ると、何個でもはさみ込めるようになる。

練習問題(53) TCP/IPのストリームとpipe()(3)

練習問題(51) または 練習問題(52) で、外部のプログラムを実行できるようにしなさい。

ヒント:dup(), dup2(), close() などで、標準入出力を切り替えて、 execve() などで、プログラムを実行する。

たとえば、シェルに次のように打ち込むことを考える。

% command1 | command2 | command3 [←]
この時、シェルは、fork() しながら2つのパイプで3つのプロセスを結び、 execve() など command1, command2, command3 を実行する。この問題では、 パイプではなくTCP/IP 結ぶ。そして、close(), dup(), close() して、 execve() でcommand1, command2, command3 を実行する。

この課題では、tcp_accept() 側で fork() を行う必要はない。1回のコマン ドを実行したら終了するようにする。

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

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

練習問題(55) localhost

telnet で接続先として localhost (127.0.0.1) を指定するとどうなるか。 localhost は、どのホストからも自分自信を指す名前である。

練習問題(56) httpサーバ

http サーバを作成しなさい。これは、受け取ったファイル名のファイルを fopen() して、内容を fread() し、それを接続先に fwrite() で返すように するとよい。

この課題では、特にセキュリティに気をつけなさい。必ず次の条件を満たすよ うなサーバを作りなさい。

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

~yas/syspro/ipc/http-server.c に、常に同じ内容を返すhttp サーバがある。 これを出発点にしてもよい。その場合、次の手順で開発するとよい。

  1. requestline を解析し、ファイル名を取り出す関数を単体で(httpサーバ とは別に) 開発する。
  2. 与えられたファイル名から、HTTP の応答のヘッダを画面に表示する関数 を単体で開発する。
  3. 1. から 2. を http-server.c に1つずつ組み込む。
大きなプログラムは、小さな部品に分解して独立して開発することが大事であ る。

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

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

練習問題(56) で、複数のクライアントから同時に接続を受けるようにしなさい。 この時、クライアントの最大値を設定して、それ以上のクライアントからの要 求を制限するようにしなさい。

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

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

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

練習問題(60) HTTP Proxy

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

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

その他、httpサーバ、fingerサーバ、または、HTTP Proxy と同程度以上の複 雑さを持つサーバを作成しなさい。
Last updated: 2005/06/25 00:49:04
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>