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

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

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

今日の重要な話

補足

strcat()とstrncpy()

strcat(), strncpy() を使わないように。 代わりに、snprintf(), strlcpy(), snprintf() を使う。 文字操作,文字列操作ライブラリ 参照。

fprintf() を使えば、snprintf() や strcpy() は多くの場合で不要になる。

画面に表示するプログラム(1)

main()
{
       printf("hello %s %d\n","world",100);
}
画面に表示するプログラム(2)
#include <stdio.h>
main()
{
       fprintf(stdout,"hello %s %d\n","world",100);
}
ネットワークに出力するプログラム
{
       fprintf(out,"hello %s %d\n","world",100);
}

名前とコメント

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

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

復習

echoサーバ

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

echo-server-nofork-fdopen.c

echo-server-nofork-fdopen.c のmain()

   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:	#include <unistd.h>     /* getpid(), gethostname() */
  17:	
  18:	extern  void echo_server( int portno, int ip_version );
  19:	extern  void echo_receive_request_and_send_reply( int com );
  20:	extern  int  echo_receive_request( char *line, size_t size, FILE *in );
  21:	extern  void echo_send_reply( char *line, FILE *out );
  22:	extern  void print_my_host_port( int portno );
  23:	extern  void tcp_peeraddr_print( int com );
  24:	extern  void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
  25:	extern  int  tcp_acc_port( int portno, int pf );
  26:	extern  int  fdopen_sock( int sock, FILE **inp, FILE **outp );
  27:	
  28:	int
  29:	main( int argc, char *argv[] )
  30:	{
  31:	        int portno, ip_version;
  32:	
  33:	        if( !(argc == 2 || argc==3) ) {
  34:	                fprintf(stderr,"Usage: %s portno {ipversion}\n",argv[0] );
  35:	                exit( 1 );
  36:	        }
  37:	        portno = strtol( argv[1],0,10 );
  38:	        if( argc == 3 )
  39:	                ip_version = strtol( argv[2],0,10 );
  40:	        else
  41:	                ip_version = 4; /* IPv4 by default */
  42:	        echo_server( portno, ip_version );
  43:	}
  44:	
main() は、引数として、ポート番号、IPのバージョン(4か6)を取る。 文字列で受け取ったポート番号を strtol() で int へ変換している。 IPのバージョンが省略されたら IPv4 とする。 最後に、echo_server() を呼ぶ。

echo-server-nofork-fdopen.c のecho_server()

echo_server() は、echoサーバのメインループである。

  45:	void
  46:	echo_server( int portno, int ip_version )
  47:	{
  48:	        int acc,com ;
  49:	
  50:	        acc = tcp_acc_port( portno, ip_version );
  51:	        if( acc<0 )
  52:	                exit( 1 );
  53:	        print_my_host_port( portno );
  54:	        while( 1 )
  55:	        {
  56:	                printf("[%d] accepting incoming connections (acc==%d) ...\n",
  57:	                       getpid(),acc );
  58:	                if( (com = accept( acc,0,0 )) < 0 )
  59:	                {
  60:	                        perror("accept");
  61:	                        exit( -1 );
  62:	                }
  63:	                tcp_peeraddr_print( com );
  64:	                echo_receive_request_and_send_reply( com );
  65:	        }
  66:	}
  67:	

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

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

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

tcp_peeraddr_print() を呼び出し、通信相手(peer)のIPアドレスとポート番号 を表示する。

最後に、echo_receive_request_and_send_reply() を呼び出し、その通信相手 についての処理を行う。

echo_receive_request_and_send_reply()

echo_receive_request_and_send_reply() は、1つの通信相手のクライアントに 対してechoサービスを提供する。

  68:	#define BUFFERSIZE      1024
  69:	
  70:	void
  71:	echo_receive_request_and_send_reply( int com )
  72:	{
  73:	        char line[BUFFERSIZE] ;
  74:	        int rcount ;
  75:	        int wcount ;
  76:	        FILE *in, *out ;
  77:	
  78:	        if( fdopen_sock(com,&in,&out) < 0 )
  79:	        {
  80:	                perror("fdopen");
  81:	                exit( 1 );  /* exit when no memory */
  82:	        }
  83:	        while( (rcount=echo_receive_request(line,BUFFERSIZE,in))>0 )
  84:	        {
  85:	                printf("[%d] received (fd==%d) %d bytes, [%s]\n",
  86:	                       getpid(),com,rcount,line );
  87:	                fflush( stdout );
  88:	                echo_send_reply( line,out );
  89:	        }
  90:	        if( rcount < 0 )
  91:	                perror("fgets");
  92:	        printf("[%d] connection (fd==%d) closed.\n",getpid(),com );
  93:	        fclose( in );
  94:	        fclose( out );
  95:	}
  96:	
この echo_receive_request_and_send_reply() は、特定のクライアント専用の echo サービスを提供する。クライアントからの要求が続く限り、動作する。

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

この関数は、クライアントから届いた要求メッセージを echo_receive_request() で配列 line に受信している。受信したメッセージ は、'\0' で終端されている。 このプログラムは、echo サービスであるので、受信したメッセージをそのまま echo_send_reply() でクライアントに送信している。 この関数は、デバッグのために、端末に printf() で受信したメッセージを表 示している。fflush() は、printf() の内部のバッファ(stdoutのバッファ)に 溜っているデータを書き出すものである。

echo_receive_request()

  97:	int
  98:	echo_receive_request( char *line, size_t size, FILE *in )
  99:	{
 100:	        if( fgets( line,size,in ) )
 101:	        {
 102:	                return( strlen(line) );
 103:	        }
 104:	        else
 105:	        {
 106:	                if( ferror(in) )
 107:	                        return( -1 );
 108:	                else
 109:	                        return( 0 );
 110:	        }
 111:	}
 112:	

echo_receive_request() は、エコー・サービスで、要求メッセージを 受信する関数である。 このプログラムは、fgets() で1行読み出している。 うまく受信できれば、受信したメッセージの長さを返す。 エラーがおきれば、-1、eof なら 0 を返す。

echo_send_reply()

 113:	void
 114:	echo_send_reply( char *line, FILE *out )
 115:	{
 116:	        fprintf(out,"%s",line );
 117:	}
 118:	

echo_send_reply() は、エコー・サービスで、応答メッセージを 送信する関数である。 fprintf() を使って受信している。メッセージには行末の \n は含まれている ので、ここでは付加していない。

この fprintf() では、fdopen_sock() でバッファリングを抑止しているので fflush() は不要になっている。

echo-server-nofork-fdopenの実行例

サーバ側。 サーバ・プロセスは、自発的には終了しない。終了させるには、キーボードで ^C を押し、シグナルを発生させる。

注意:同じポート番号 1231 を同じコンピュータで複数人が同時に使おうとす るとbind でエラーが出ることがある。

$ ls -l echo-server-nofork-fdopen.c  [←]
ls: echo-server-nofork-fdopen.c: No such file or directory
                              # ファイルが存在しないことの確認
$ cp ~yas/syspro/ipc/echo-server-nofork-fdopen.c . [←]
$ ls -l echo-server-nofork-fdopen.c  [←]
-rw-r--r--  1 yas  prof  5246  5 28 15:13 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 crocus39.coins.tsukuba.ac.jp 1231
[57167] accepting incoming connections (acc==3) ...
[57167] connection (fd==4) from 130.158.86.250:62468
[57167] received (fd==4) 5 bytes, [abc
]
[57167] received (fd==4) 5 bytes, [def
]
[57167] received (fd==4) 5 bytes, [ghi
]
[57167] connection (fd==4) closed.
[57167] accepting incoming connections (acc==3) ...
^C
$ []
クライアント側
$ telnet crocus39.coins.tsukuba.ac.jp 1231 [←]
Trying 130.158.86.249...
Connected to crocus39.coins.tsukuba.ac.jp.
Escape character is '^]'.
abc[←]
abc
def[←]
def
ghi[←]
ghi
^][←]
telnet> quit[←]
Connection closed.
$ []

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

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

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

詳しくは、次回 説明する。

HTTP

先週 説明した HTTP を復習しなさい。

HTTPの要求(オプション付き)

HTTPの要求は、 最低次の2行である。
GET /syspro/index.html HTTP/1.0←↓
←↓
HTTPの要求は、要求行(request line)から始まり、最後は、空行で終わる。 実際の Web ブラウザは、次のように要求行と空行の間にオプションを含む要求 を送る。
GET /~syspro/index.html HTTP/1.1←↓
Host: www.coins.tsukuba.ac.jp←↓
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:30.0) Gecko/20100101 Firefox/30.0←↓
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8←↓
Accept-Language: ja,en-us;q=0.7,en;q=0.3←↓
Accept-Encoding: gzip, deflate←↓
Referer: http://www.coins.tsukuba.ac.jp/←↓
Connection: keep-alive←↓
←↓
空行は、オプションの終了を意味する。 オプションは全部無視しても、最低限の HTTP サーバを作成することはできる。 サーバとしては、送られてきた要求メッセージを全て受け取る必要はある。 (空行が現れるまで受け取り続ける必要がある。)

HTTP の応答としては、最低限次のような行が期待される。

HTTP/1.0 200 OK←↓
Content-Type: text/html←↓
←↓
<html>省略</html>↓
1行目は、 成功/失敗を示す行(status line) である。200 は、成功したことを意味する。 2行目は、HTTPの応答の型を意味する。この例では、内容は、HTML であることを意味する。 3行目は、空行であり、HTTP 応答のヘッダの終りを意味する。 空行以降は、内容である。この例では、HTML の内容が1行だけ含まれている。

実際には次のように、様々なヘッダが送られる。

HTTP/1.1 200 OK←↓
Date: Mon, 28 Jul 2018 06:09:05 GMT←↓Server: Apache←↓
Last-Modified: Wed, 23 Apr 2018 09:25:25 GMT←↓Etag: "2231aca-63f-4f7b24ffc0c10"←↓
Accept-Ranges: bytes←↓
Content-Length: 1599←↓
Connection: close←↓
Content-Type: text/html←↓
←↓
<html>省略</html>↓

http-server

以下のプログラムは、HTTP で要求を受け取り、 HTTPで常に一定の内容を返すサーバである。 echoサーバ と似せて作ってあるので、まずは、echoサーバを読んで理解することを奨める。

http-server.c

http-server.c のmain()

   1:	
   2:	/*
   3:	  http-server-.c -- 常に同じ内容を返す HTTP サーバ(forkなし版)
   4:	  ~yas/syspro/ipc/http-server.c
   5:	  Created on: 2002/06/03 00:48:43
   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:	#include <unistd.h>     /* getpid(), gethostname() */
  17:	
  18:	extern  void http_server( int portno, int ip_version );
  19:	extern  void http_receive_request_and_send_reply( int com );
  20:	extern  int  http_receive_request( FILE *in );
  21:	extern  void http_send_reply( FILE *out );
  22:	extern  void http_send_reply_bad_request( FILE *out );
  23:	extern  void print_my_host_port_http( int portno );
  24:	extern  char *chomp( char *str );
  25:	extern  void tcp_peeraddr_print( int com );
  26:	extern  void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
  27:	extern  int  tcp_acc_port( int portno, int ip_version );
  28:	extern  int  fdopen_sock( int sock, FILE **inp, FILE **outp );
  29:	
  30:	int
  31:	main( int argc, char *argv[] )
  32:	{
  33:	        int portno, ip_version;
  34:	
  35:	        if( !(argc == 2 || argc==3) ) {
  36:	                fprintf(stderr,"Usage: %s portno {ipversion}\n",argv[0] );
  37:	                exit( 1 );
  38:	        }
  39:	        portno = strtol( argv[1],0,10 );
  40:	        if( argc == 3 )
  41:	                ip_version = strtol( argv[2],0,10 );
  42:	        else
  43:	                ip_version = 4; /* IPv4 by default */
  44:	        http_server( portno,ip_version );
  45:	}
  46:	
main() は、引数として、ポート番号を取る。 文字列で受け取ったポート番号を strtol() で int へ変換している。 最後に、http_server() を呼ぶ。 echo-server-nofork-fdopen.cのmain()とほとんど同じ。

http-server.c のhttp_server()

http_server() は、HTTPサーバのメインループである。

  47:	void
  48:	http_server( int portno, int ip_version )
  49:	{
  50:	        int acc,com ;
  51:	
  52:	        acc = tcp_acc_port( portno, ip_version );
  53:	        if( acc<0 )
  54:	                exit( -1 );
  55:	        print_my_host_port_http( portno );
  56:	        while( 1 )
  57:	        {
  58:	                printf("[%d] accepting incoming connections (fd==%d) ...\n",getpid(),acc );
  59:	                if( (com = accept( acc,0,0 )) < 0 )
  60:	                {
  61:	                        perror("accept");
  62:	                        exit( -1 );
  63:	                }
  64:	                tcp_peeraddr_print( com );
  65:	                http_receive_request_and_send_reply( com );
  66:	        }
  67:	}
  68:	

echo-server-nofork-fdopen.cのecho_server()とほとんど同じ。違いは、次の所だけ。

http-server.c の http_receive_request_and_send_reply()

http_receive_request_and_send_reply() は、 1つの通信相手のクライアントに対して HTTP により要求を受付け、 HTTPにより応答を返す。

  69:	#define BUFFERSIZE      1024
  70:	
  71:	void
  72:	http_receive_request_and_send_reply( int com )
  73:	{
  74:	        FILE *in, *out ;
  75:	
  76:	        if( fdopen_sock(com,&in,&out) < 0 )
  77:	        {
  78:	                perror("fdooen()");
  79:	                return;
  80:	        }
  81:	        if( http_receive_request( in ) )
  82:	        {
  83:	                http_send_reply( out );
  84:	        }
  85:	        else
  86:	        {
  87:	                http_send_reply_bad_request( out );
  88:	        }
  89:	        printf("[%d] Replied\n",getpid() );
  90:	        fclose( in );
  91:	        fclose( out );
  92:	        return;
  93:	}
  94:	
この関数では、 echo-server-nofork-fdopen.cのecho_receive_request_and_send_reply() と同様に、 fdopen_sock() を使って、通信可能なファイル記 述子 com から2つの FILE * を作成している。

http_receive_request() を呼び出し、HTTP で要求を受け取っている。 もしエラーがなければ、http_send_reply() を呼び出し、HTTP で OK の応答を送信する。 エラーが有れば、http_send_reply_bad_request() を呼び出し、エラーの 応答を送信する。

http-server.c の http_receive_request()

http_receive_request()は、HTTP の要求を解析し、画面に表示している。
  95:	int
  96:	http_receive_request( FILE *in )
  97:	{
  98:	        char requestline[BUFFERSIZE] ;
  99:	        char rheader[BUFFERSIZE] ;
 100:	        
 101:	        if( fgets(requestline,BUFFERSIZE,in) <= 0 )
 102:	        {
 103:	                printf("No request line.\n");
 104:	                return( 0 );
 105:	        }
 106:	        chomp( requestline ); /* remove \r\n */
 107:	        printf("requestline is [%s]\n",requestline );
 108:	        while( fgets(rheader,BUFFERSIZE,in) )
 109:	        {
 110:	                chomp( rheader ); /* remove \r\n */
 111:	                if( strcmp(rheader,"") == 0 )
 112:	                        break;
 113:	                printf("Ignored: %s\n",rheader );
 114:	        }
 115:	        if( strchr(requestline,'<') ||
 116:	            strstr(requestline,"..") )
 117:	        {
 118:	                printf("Dangerous request line found.\n");
 119:	                return( 0 );
 120:	        }
 121:	        return( 1 );
 122:	}
 123:	

http_receive_request() は、まず、 HTTPの要求の最初の行(request line) をfgets() で読み込む。次に、chomp() で行末の改行 \n\r を取り除く。

要求行の内容に対して簡単なエラーチェックをしている。次のような パタンが含まれるとエラーにしている。

最初の要求行に続き、オプションの行を読み出している。 オプションの最後は、空行で終わる。 空行が来るまで、オプションの行を読み出す。

このプログラムでは、オプションを一切使用していない。しかし、HTTP の要求 で送られてきた要求を全て読み出している。プロトコル上、必ず読み出す必要 がある。

http-server.c の http_send_reply()

http_send_reply()は、HTTP の応答メッセージを File *out に出力している。
 124:	void
 125:	http_send_reply( FILE *out )
 126:	{
 127:	        fprintf(out,"HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n");
 128:	        fprintf(out,"<html><head></head><body>hello.</body></html>\n");
 129:	}
 130:	

http_send_reply() は、HTTPのヘッダと本体を出力している。 ヘッダは、status line に Content-Typeが続く。 空行以降が本体である。

本体では、HTMLの文書を出力している。

http-server.c の http_send_reply_bad_request()

http_send_reply_bad_request()は、 HTTP の応答メッセージ(エラー)を File *out に出力している。
 131:	void
 132:	http_send_reply_bad_request( FILE *out )
 133:	{
 134:	        fprintf(out,"HTTP/1.0 400 Bad Request\r\nContent-Type: text/html\r\n\r\n");
 135:	        fprintf(out,"<html><head></head><body>400 Bad Request</body></html>\n");
 136:	}
 137:	

http_send_reply_bad_request() は、HTTPのヘッダと本体を出力している。 ヘッダは、status line (エラー)に Content-Typeが続く。 空行以降が本体である。

本体では、HTMLの文書を出力している。

http-serverの実行例

サーバ側。 サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。

注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。

$ ls -l http-server.c [←]
ls: http-server.c: No such file or directory
$ cp ~yas/syspro/ipc/http-server.c . [←]
$ ls -l http-server.c [←]
-rw-r--r--  1 yas  prof  6330  7 28 15:44 http-server.c
$ make http-server [←]
cc     http-server.c   -o http-server
$ ./http-server [←]
Usage: ./http-server portno {ipversion}
$ ./http-server 1231 [←]
open http://crocus39.coins.tsukuba.ac.jp:1231/index.html
[57308] accepting incoming connections (fd==3) ...
[57308] connection (fd==4) from 130.158.86.247:52315
requestline is [GET /index.html HTTP/1.1]
Ignored: Host: crocus39.coins.tsukuba.ac.jp:1231
Ignored: User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0
Ignored: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Ignored: Accept-Language: ja,en-us;q=0.7,en;q=0.3
Ignored: Accept-Encoding: gzip, deflate
Ignored: Connection: keep-alive
[57308] Replied
[57308] accepting incoming connections (fd==3) ...
[57308] connection (fd==4) from 130.158.86.247:52316
requestline is [GET /favicon.ico HTTP/1.1]
Ignored: Host: crocus39.coins.tsukuba.ac.jp:1231
Ignored: User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0
Ignored: Accept: image/png,image/*;q=0.8,*/*;q=0.5
Ignored: Accept-Language: ja,en-us;q=0.7,en;q=0.3
Ignored: Accept-Encoding: gzip, deflate
Ignored: Connection: keep-alive
[57308] Replied
[57308] accepting incoming connections (fd==3) ...
[57308] connection (fd==4) from 130.158.86.247:52317
requestline is [GET /favicon.ico HTTP/1.1]
Ignored: Host: crocus39.coins.tsukuba.ac.jp:1231
Ignored: User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0
Ignored: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Ignored: Accept-Language: ja,en-us;q=0.7,en;q=0.3
Ignored: Accept-Encoding: gzip, deflate
Ignored: Connection: keep-alive
[57308] Replied
[57308] accepting incoming connections (fd==3) ...
^C
$ []
クライアント側
$ open http://crocus39.coins.tsukuba.ac.jp:1231/index.html [←]
Safari や Firefox で "http://crocus39.coins.tsukuba.ac.jp:1231/index.html" を開く。

echoサーバの細かい所

以下の内容は、飛ばしてよい。

print_my_host_port()

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

tcp_peeraddr_print()

tcp_peeraddr_print() は、通信相手(peer)のアドレス(TCP/IPの場合、IPアド レスとポート番号)を表示する。

sockaddr_print()

sockaddr_print() は、引数で与えられた struct sockaddr に含まれる IP アドレスとポート番号を画面に表示する関数である。

tcp_acc_port()

tcp_acc_port() は、 通信路の開設 の仕事のうち、サーバ側で接続要求受付用ポートを作る関数である。

fdopen_sock()

fdopen_sock() は、TCP/IP による通信を、fprintf(), fgets(), fread() 等で 行えるようにする関数である。 この関数は、 クライアント側 とまったく同じである。

http-serverの細かい所

以下の内容は、飛ばしてよい。

http-server.c の print_my_host_port_http()

http-server.cのprint_my_host_port_http()echo-server-nofork-fdopen.cのprint_my_host_port() と同様に、画面にサーバのホスト名 とポート番号を表示している。ただし、Web ブラウザでアクセスする時に便利 なように、URL の形をしている。

chomp()

http-server.cのchomp() は、行末の \n\r を削除する関数である。

http-server.c のtcp_peeraddr_print()

echo-server-nofork-fdopen.cのtcp_peeraddr_print()と同じ。

http-server.c のsockaddr_print()

echo-server-nofork-fdopen.cのsockaddr_print()と同じ。

http-server.c のtcp_acc_port()

echo-server-nofork-fdopen.cのtcp_acc_port()と同じ。

http-server.c のfdopen_sock()

echo-server-nofork-fdopen.cのfdopen_sock()と同じ。

Javaによるechoサーバ

JavaによるHTTPサーバ

練習問題

練習問題(701) echo-serverのコンパイルと実行

echo-server-nofork-fdopen.c を、コンパイルして実行しなさい。 次のクライアントでアクセスしなさい。

練習問題(702) http-serverのコンパイルと実行

http-server.c を、コンパイルして実行しなさい。 次のクライアントでアクセスしなさい。 画面に現れる次の部分に着目しなさい。

練習問題(703) string_split()とfree_string_vector()の利用

次のファイルに含まれている関数 string_split() と free_string_vector() を利用しなさい。 これらの関数の引数と結果は、次のようになっている。
int string_split( char *str, char del, int *countp, char ***vecp )
文字列 str を、区切り文字 del で分解する。 結果を、countp と vecp に返す。 ここで、count と vec は、 main() の引数の argc, argv と同じ形式になっている。
void free_string_vector( int count, char **vec )
string_split() で得た vec のメモリを解放する。
これらの関数は、次のようにして使う。
    int count;
    char **vec;
    int i ;
        str = "GET /index.html HTTP/1.0";
	if( string_split( str, ' ', &count, &vec ) < 0 )
	{
	    perror("string_split-malloc");
	    exit( 1 );
	}
	for( i=0 ; i< count; i++ )
	    printf("%d: %s\n", i, vec[i] );
	free_string_vector( count, vec );

これらの関数を使って、次のような文字列を解析し、count、 および、 vec[0], vec[1], ... vec[count-1] にどのような値が返されるのかを調べなさ い。ただし、区切り文字としては ' ' (空白) を用いるものとする。

  1. "GET /index.html HTTP/1.1"
  2. "GET /index.html"
  3. "GET /index.html /favicon.ico HTTP/1.0"
  4. "GETa /index.html HTTP/1.0"

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

標準入力から HTTP の GET 要求を受け取り、要求行からファイル名を取り出す プログラムを作成しなさい。次のファイルをコピーして、一部を修正すると良 い。
$ cp ~yas/syspro/ipc/http-request-analyze.c . [←]
$ emacs http-request-analyze.c [←]
$ cc http-request-analyze.c -o http-request-analyze [←]
$ ./http-request-analyze < ~syspro/public_html/htdocs/2018/2018-05-30/http-good-request.txt [←]
requestline is [GET /index.html HTTP/1.1]
Ignored: Host: www.coins.tsukuba.ac.jp
Ignored: User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:12.0) Gecko/20100101 Firefox/12.0
Ignored: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Ignored: Accept-Language: ja,en-us;q=0.7,en;q=0.3
Ignored: Accept-Encoding: gzip, deflate
Ignored: Connection: keep-alive
filename is [/index.html].
$ []
このプログラムは、関数 http_receive_request() で次のような動作を行う。
  1. 引数in (stdinが与えられる)から fgets() で 1 行読み込む。行末の \r や \n や \r\n を削除する。chomp()を使っている。
  2. 最初の行を変数 requestline に保存する。
  3. 2行目以降、空行が出てくるまで読み捨てる。
  4. 空行が出てくれば、ループを抜け、保存してある最初の行(requestline)を解析する。
requestlineの解析は、次のように行う。
  1. requestline に「<」や「..」が含まれていたら、エラー。
  2. requestline を string_split() 等で、' ' を区切り文字として分割する。 string_split() は、内部で malloc() を呼んでメモリを確保している。
  3. 要素数が 3 でなければエラー。
  4. 最初の要素が "GET" でなければエラー。
  5. 最後の要素が "HTTP/1.0" か "HTTP/1.1" でなければエラー。
  6. 2つ目の要素を filename にコピーする。
  7. string_split()を使っていた時には、 free_string_vector() を呼び、メモリを解放する。
  8. return 1 で、成功したことを返す。
注意: エラーを返す前に、確保したメモリを解放すること。 そうしなければ、メモリ・リークが生じる。 メモリは、string_split() でも確保されている。

クライアントは、不正な要求行を送ってくる可能性がある。そのような場合に は、エラーにしなさい。たとえば、次のようなデータを送ってくる可能性があ る。

このような場合も、エラーとして扱い、プログラムがクラッシュしたり、バッ ファ・オーバーフローを起こしたりすることがないようにしなさい。

なお、このプログラムは、HTTP の要求を標準入力(stdin)から読むが、ネット ワーク通信を一切行わない。レポートには、標準入力からデータを与え、プロ グラムが正しく動作していることを示しなさい。

練習問題(705) HTTPサーバの応答(.htmlのみ)

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

<HTML>
中略
</HTML>
$ []
このプログラムは、動作する関数が含まれている。残りの部分をうめて完成さ せない。全体としては、次のようなプログラムになる。
  1. main() は、引数 argv[1] を取る。これは、ファイル名の一部を表す。こ れと stdout を引数として http_send_reply() を呼ぶ。
  2. http_send_reply() は、まず、ファイル名の一部から拡張子を取得する。 拡張子が空なら、エラーメッセージを出力してリターンする。
  3. 拡張子が ".html" の時のみ処理を行う。その他の時は、エラーメッセー ジを出力してリターンする。
  4. main の引数として与えられたファイル名の一部、環境変数 HOME"/public_html/htdocs/" から、実際のファイル名を作成する。snprinf() を用 いると便利である。(strcat() は用いないこと。) 環境変数は、ライブラリ関数 getenv() を使うと入手できる。
    char *home;
         home = getenv("HOME");
         ...
    
    なお、"/public_html/htdocs/" 以外のディレクトリを用いてもよい。 たとえば、"." でもよい。
  5. 4. で作成したファイル名を用いて、fopen() でファイルを開く。失敗し た時には、ファイルが存在しないことを意味する。その時は、 http_send_reply_not_found() で、エラーメッセージを出力してリターンする。
  6. 成功すれば、ファイルが存在した時には、 成功/失敗を示す行(status line) として"HTTP/1.0 200 OK\r\n" を出力する。
  7. 続いて、"Content-Type: text/html" を出力する。
  8. 続いて、空行を出力する。
  9. 続いて、ファイルの内容を fread() で入力し、その内容を fwrite() で 出力する。ファイルの末尾まで繰り返す。この処理を、 fread() と fwrite() の使い方については、 前半の資料 を参考にしなさい。 なお、この課題では、対象がテキストだけなので、fgets() と fprintf() を使っ てもよい。バイナリを扱う時には、fgets() と fprintf() では不十分である。 fgetc() や fputc() でも動作するが、fread() と fwrite() よりも性能が低い。
  10. fopen() で開いたファイルを fclose() で閉じる。
なお、環境変数 HOME の変わりに「~」を使うことはできない。 「~」は、open() システム・コールや fopen() ライブラリ関数では使 えない。「~」 は、シェルや emacs 等のプログラムが独自にホーム・ ディレクトリを調べて置き換えている。

なお、この課題は、HTTP の応答を画面(stdout)に出力するが、ネットワーク通 信を一切行わない。 レポートには、コマンドラインからデータを与え、プログラムが正しく動作し ていることを示しなさい。

$ ./http-response-html /existing-file-name.html [←]
$ ./http-response-html /file-not-found.html [←]
$ ./http-response-html /bad-file-name.txt [←]

練習問題(706) HTTPサーバ(.htmlのみ)

  1. 練習問題(702) を行いなさ い。
  2. 練習問題(704) を行いなさい。 7回目の課題で、 練習問題(704) を選択した人は、8回目の課題としては何も記述しなくて もよい。7回目の課題で、 練習問題(704) を選択しなかった人は、 練習問題(704) の結果もレポートに含めなさい。
  3. 練習問題(705) を行いなさ い。レポートには、その結果も含めなさい。
  4. 練習問題(702) 練習問題(704) 、および、 練習問題(705) の プログラムを組み合わせて、.html の ファイルのみを受け付ける HTTP サーバを作成しなさい。
作成したプログラムが動作することを、実際の WWW ブラウザ (Firefoxなど)を 用いて確認しなさい。telnet だけでは、きちんと HTTP のプロトコルに従って いるか確認できないので、不十分である。 レポートには、実際の WWW ブラウザを用いたことを示す結果(サーバ側の表示)を 含めなさい。

補足: クライアントから不正な要求が送られたとしても、 exit() でサーバを 終了しないようにしなさい。

練習問題(707) HTTPサーバ(テキストのみ)

練習問題(706) で、.html を含め て次のような拡張子を持つファイルを扱えるようにしなさい。 この表にないものを加えても良い。
Content-Type: 拡張子
text/html .html
text/plain .txt
text/plain .text

練習問題(708) HTTPサーバ(バイナリ)

練習問題(706) で、.html を含め て次のような拡張子を持つファイルを扱えるようにしなさい。 この表にないものを加えても良い。
Content-Type: 拡張子
text/html .html
text/plain .txt
text/plain .text
image/gif .gif
image/jpeg .jpeg
image/png .png

練習問題(709) IPアドレスによるアクセス制御が可能なHTTPサーバ

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

練習問題(710) ドメイン名によるアクセス制御が可能なHTTPサーバ

練習問題(709) で、ドメイン名 coins.tsukuba.ac.jp を持つホストからのアクセスだけを許す ようにしなさい。

練習問題(711) 名前ベースの仮想ホストに対応したHTTPサーバ

練習問題(708) を、名前ベースの仮想ホストに対応させなさい。HTTP の要求でオプションであ る Host: を読み、それに応じて異なるディレクトリの内容を返すようにしなさ い。

Firefox 等で試す時には、ホスト名として、次のようなものを利用する。

練習問題(712) カレンダを表示するHTTPサーバ

要求された月のカレンダーを表示するような HTTP サーバを作成しなさい。 要求の与え方とては、次のような方法が考えられる。
GET /?month=6&year=2018 HTTP/1.0[←][←]
カレンダの出力については、cal コマンドを実行した結果を用いても良い。こ れに対して、HTTP で結果を返す。Content-Type: としては、text/plain に するか、text/html で <PRE></PRE>を使う方法が 考えられる。その他の手法でもよい。

このサーバを実行するための HTML の form を用意しなさい。

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

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

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

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

練習問題(714) 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 で受け取ったデー タをパイプに書き込んでもよい。

練習問題(715) spipe-first

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

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

練習問題(716) spipe-middle

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

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

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

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

練習問題(719) HTTP Proxy

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

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

その他、HTTPサーバ、または、 HTTP Proxy と同程度以上の複 雑さを持つサーバを作成しなさい。

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

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

練習問題(722) utmpの内容を表示するHTTPサーバ

utmpの内容を表示するような HTTP サーバを作成しなさい。 HTTPにより要求を受け取り、結果をHTMLの<TABLE>で表現して HTTPで返しなさい。

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

練習問題(723) ディレクトリの内容を表示するHTTPサーバ

opendir() ライブラリ関数を用いて、ディレクトリの内容を表示するような HTTP サーバを作成しなさい。HTTPにより要求を受け取り、結果をHTMLで表現し てHTTPの応答として返しなさい。ディレクトリの内部に子供のディレクトリが 含まれていた場合、それをハイパーリンクとして表現して返し、その子供のディ レクトリについても表示できるようにしなさい。

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


Last updated: 2018/05/30 21:21:04
Yasushi Shinjo / <yas@cs.tsukuba.ac.jp>