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

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

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

今日の重要な話

TA、教員、隣の人の活用

Google プログラミングの禁止。人(教員,TA,周りの席の人)に聞いた方が 100倍早い。時間の節約。

トップダウン的な理解

今後のシステムプログラムの例題では、例題のプログラムの一部を意図的に省 略してある。

例題のプログラムの利用方法

プログラムの先頭にファイル名が書いてある。cp コマンドでコピーできる。た とえば、~yas/syspro/ipc/echo-client-fdopen-one.c なら次のようにして コピーできる。
$ cp ~yas/syspro/ipc/echo-client-fdopen-one.c . [←]
$」は、プロンプトであり、実際には、「abelia50:~ s1412345$ 」のようになっている。コピーする時には、打たない。また、末尾 の「.」を打つこと。これはカレント・ワーキング・ディレクトリの意 味である。

Emacs Tips

行番号を削除するなら、Emacs の正規表現によるM-x replace-regexp が使える。 以下の例では、行の先頭(^)から始まる任意の文字(.)6個を空文字列に置き換え ている。

M-x replace-regexp[←]
Replace regexp (default 省略): ^......[←]
Replace regexp ^...... with: [←]
ただし、Web ページにあるプログラム(行番号があるもの)は、一部省略されて いるので、そのまま行番号を削除しただけでは動作しないことがある。 したがって、cpコマンド でコピーすべきである。

字下げが気に入らない時には、M-x indent-region を使うとよい。region を設 定するには、C-SPC (control + space) でマークを設定して、カーソルを移動 させる。タブ・キーも使えるが、1行ずつしかできない。

ことえりでバックスラッシュを入力できるようにすると良い。 また、 macOSで「入力ソース」を切り替るショートカットの変更MacOSX の Spotlight のショートカット・キーの解除 をすべきである。

カーソル移動には、M-<M-> も使 える。

Java

本日の課題では、Java言語でプログラムを作成してもよい。ただし、利用して もよいのは、Socket, ServerSocket, DatagramSocket 等の Socket API に近い レベルのAPI を提供するクラスのみである。高レベルのクラス、たとえば、 HttpURLConnection を用いてはならない。

TCP/IPの基本的な考え方

インターネット上のアプリケーションの多くは、TCP/IPという仕組みを用いて 通信を行っている。

ストリーム

TCP/IPは、信頼性のある(reliable)双方向のストリーム転送サービス (stream transport service)を提供する通信プロトコルである(図1)。ス トリームは、次のような性質がある転送サービスである。

Unixのパイプは、双方向ではなく単 方向であるが、同じストリームに分類される転送サービスを提供するものであ る。


図1(a) TCP/IPにより提供されるストリーム
図1(a) TCP/IPにより提供される双方向ストリーム


図1(b) Unixのパイプにより提供されるストリーム
図1(b) Unixのパイプにより提供される単方向ストリーム

なお、C言語のライブラリ関数である fopen(), fgets(), fputs() なども、 ストリームと呼ばれることがある。これは、もともとランダム・アクセス可能 で、メモリ中の配列と同じようにアクセスするすることもできるファイルを、 まるでプロセス間通信のストリームと同じように扱うことができることにも 関係している。

通信路の開設

TCP/IP では、プロセスとプロセスが、電話で会話をするように通信が行われ る。普通の電話で人間同士が話をするには、まず電話番号を指定して、話相手 に電話をとってもらわなければならない。TCP/IP においても同様である。 TCP/IPでは、電話を掛ける方をクライアント・プロセス、電話を待つ方をサー バ・プロセスと言いう。

TCP/IP では、回線を接続する段階では、クライアント・ プロセスとサーバ・プロセスは非対称である。一度通信路が確立された後は、 両方のプロセスは、TCP/IPのレベルでは、まったく対称的になる。

TCP/IPにおいてプロセス間に通信路を開設するには、IPアドレスとポート番 号が必要である。ポート番号は、同じIPアドレスを持つホスト(ネットワークに接続されたコンピュータ)上で動いている プロセスを区別するために使われる。

以下に、通信路が開設される手順を示す。

  1. サーバ・プロセスが、ポート番号を指定して、接続要求受付用ポートを作る(図3(a))。 (注意:要求受付用ポートでは、データの送受信はできない。)
  2. サーバ・プロセスが、クライアント・プロセスからの接続要求を待つ(図3(b))。
  3. クライアント・プロセスが通信用ポートを作り、それを使ってサーバ・プ ロセスが動いているホストのIPアドレスと、サーバ・プロセスが作った接続 要求受付用ポートのポート番号を使って、接続要求を行う(図3(c))。
  4. 接続要求が受け付けられると、サーバ側では、新たに通信用ポートが作ら、 サーバ・プロセスから利用可能になる(図3(d))。これは、特定のクライアント との通信のために使われる。
  5. 以後、クライアント・プロセスとサーバ・プロセスは、データを送受信す る(図3(e))。順番はどちらからが先でも良い。

図3(a) TCP/IP通信路の開設(1)。サーバは、tcp_acc_port()で接続要求受付用ポ-トを作る。

図3(a) TCP/IP通信路の開設(1)

図3(b) TCP/IP通信路の開設(2)。サーバは、accept()で接続要求を待つ。

図3(b) TCP/IP通信路の開設(2)

図3(c) TCP/IP通信路の開設(3)。クライアントは、tcp_connect()で,通信用ポ-トを作り,接続要求を行なう。

図3(c) TCP/IP通信路の開設(3)

図3(d) TCP/IP通信路の開設(4)。通信路が確立される。サーバ側で、通信用ポートが作られる。

図3(d) TCP/IP通信路の開設(4)

図3(e) TCP/IP通信路の開設(5)。クライアント、サーバとも write(),read() でデータの送受信。

図3(e) TCP/IP通信路の開設(5)

TCP/IPにおける通信路開設において、クライアントは、サーバ側の接続要求受 付用ポートのポート番号を、事前に知っている必要がある。サーバは、普通、 ポート番号を固定する。いくつかの主要なサービスでは、利用すべきポート番 号が決められている。たとえば、HTTP ならば、80 を使う。

クライアント側の通信用ポートのポート番号は、通常は、オペレーティング・ システムにより自動的に割り当てられる。

クライアント・サーバ・モデル

 プロセスは、本来、いつでも自由にメッセージ(ネットワークを流れるデー タ)を送信でき、いつでも自由に、メッセージを受信できる。しかし、本当に自 由に送信と受信を行うと、プログラムが複雑になり、見通しが悪くなり、バグ も混入しやすくなる。 そこで、通信を行うプログラムを書く時に 意図的に制約を設定してプログラムを単純化して見通し のよいものにしようという考え方が生まれた。 たとえば、図?で、(a) と (b) を比較すると、プロセスの数とメッセージの 数は同じであるが、(b) の方がはるかに見通しがよい。

図?(a) 構造化されていないもの
図?(a) 構造化されていないもの

図?(b) 構造化されたもの
図?(b) 構造化されたもの

 クライアント・サーバ・モデルは、プロセス間通信を構造化したものであり、 最近の用語でいうと、デザイン・パターンの1つである。

プログラミングの歴 史の中で「構造化」という言葉は、まず、「制御構造」に対して使われた。構 造化プログラミングとは、goto文を、よい goto 文と悪い goto文に分け、よ い goto 文だけを使うようにしようとするものである。初期のプログラミング では、アセンブリ言語や貧弱な制御構造しか持たない Fortran が使われてい たが、その時は、jump 命令や goto 文が多用されていた。そのような jump 命令や goto 文にも、分かりやすいものとわかりにくいものがあった。そこで、 よい goto 文のパターンを整理して、それだけを使ってプログラムを書くのが よいとされた。そしてよい goto 文にはプログラミング言語のレベルでif、 while、continue、break、そして、手続き呼出し(call)とreturn という特別 な形式が割り当てられた。C言語や Pascal では、goto 文が残されたが、 Java などの最近の言語ではgoto 文が記述できなくなっている。

 プロセス間通信を構造化するという意味でのクライアント・サーバ・モデル では、まずプロセスをクライアントとサーバの2種類に分ける。

図? 通信のパタンからみたクライアントとサーバの定義
図? 通信のパタンからみたクライアントとサーバの定義

クライアントもサーバも、多くの場合、内部に1つループを持ち、次の動作を 繰り返す。
クライアント
先に(要求)メッセージを送信し、後で(応答)メッセージを受信する。
サーバ
先に(要求)メッセージを受信し、後で(応答)メッセージを送信する。

クライアント・サーバ・モデルに基づくプログラムには次のようなことを行う プロセスは存在しない。

注意:クライアントとサーバは、いろいろな意味で使われる。これらの意味は、 多くの場合、一致しているが、一致していないこともある。

クライアントのパタン

    connect(s);   // 接続要求。accept() と対応。

    send(s,message);        // 要求
    receive(s,message);     // 応答
    send(s,message);        // 要求
    receive(s,message);     // 応答
    ...                     // 必要回数繰り返す

    close(s);     // 接続の切断。
注意1:ここで、connect(), send(), receive() は、抽象的な意味。 具体的なシステム・コールの使い方を説明したものではない。

send() や receive() は、複数の具体的なシステム・コールと対応することが ある。たとえば、1回のシステム・コールでは送信できない場合、(ループして) 複数回のシステム・コールを用いることもある。

サーバのパタン

    make_port(a); // 受付端の登録。
    while( 1 )
    {
        s=accept(a);    // 実際の受付。connect() と対応。
        while( !eof(s) )
        {
            receive(s,message);     // 要求の受信
            send(s,message);        // 応答の送信
        }
        close(s);     // 接続の切断。
    }

情報科学類「システムプログラム」独自のクライアントを作成するためのAPI

TCP/IPのクライアント側のプログラムで大事な標準のシステムコールとライブ ラリ関数は、一般に、次の通りである。 この講義では、次の独自のAPIを用いる。
int tcp_connect( char *server, int portno ) [独自]
通信用ポートを作成し、 ホスト名 server のポート番号 (portno) へ TCPで通信路を開設する。 そのTCP/IPのストリームに対応したファイル記述子を返す。
int fdopen_sock( int sock, FILE **inp, FILE **outp ) [独自]
tcp_connect() で得られた、TCP/IPのファイル記述子を、2つの FILE *に変換する。2つのうち、1つは受信用、もう1つは送信用。
tcp_connect() と fdopen_sock() の働きにより、次の標準の関数を用いてネッ トワークのデータを送受信することができる。
int fprintf(FILE *out,char *fmt, ...) [標準]
out に結びついた TCP/IP のストリームに対してメッセージを送信する。
char *fgets(char *buf, int n, FILE *in ) [標準]
in に結びついた TCP/IP のストリームに対からメッセージを 1行受信する。
size_t fwrite(void *buf, size_t size, size_t nitems, FILE *out) [標準]
out に結びついた TCP/IP のストリームに対してbuf 番地にある長さ size バイトの構造体を nitems 個送信する。構造体ではなく、バイト単位で送 信したい場合には、size には 1 を指定し、nitems には、バッファの大きさを 指定する。
size_t fread(void *buf, size_t size, size_t nitems, FILE *in)) [標準]
in に結びついた TCP/IP のストリームから size バイトの長さの構造体 を nitems だけ受信する。構造体ではなく、バイト単位で受信したい場合には、 size には 1 を指定し、nitems には、バッファの大きさを指定する。
FILE *については、 前半第3回「11. ファイルアクセス」 も参照。

TCP/IP の汎用クライアント・プログラムとしての telnet コマンド

telnet コマンドとは

telnet コマンドは、本来は、遠隔ログインのコマンドであり、 通常次のように使われる。
$ telnet hostname [←]
以後、ユーザ名とパスワードを打ち、そのホストへログインできる。そしてシェ ルにより対話的に利用できる。(coins では、telnet による遠隔ログインのサー ビスを提供していない。)

TCPの汎用クライアントとしてのtelnet

telnet コマンドに、次のようにポート番号(23のtelnetサービス以外)を与える ことで、文字列を送受信するようなプロトコルについては 汎用のTCPのクライアントとして使える。
$ telnet hostname portno [←]

端末、telnet、サーバ、要求、応答
図? TCPの汎用クライアントとしてのtelnet

telnetによるecho サーバへの接続

telnet コマンドは、^D (control+D) を打っても終了しない。 ^] を打つと、ローカルの telnet コマンドを制御することができる。 ここで、quit などのコマンドが使える。

以下の例は、echo サービス(ポート番号7番)を提供しているサーバに telnet コマンドをクライアントとして接続している。echo サービスは、送られて来た 文字列(最後に改行)をそのまま送り返すものである。

$ egrep '^echo[ ].*/tcp' /etc/services [←]
echo              7/tcp     # Echo
$ telnet abelia02.coins.tsukuba.ac.jp 7 [←]
Trying 130.158.86.2...
Connected to abelia02.coins.tsukuba.ac.jp.
Escape character is '^]'.
hello[←]
hello
exit[←]
exit
quit[←]
quit
^]
telnet> quit[←]
Connection closed.
$ []
注意:coins では、echo サービスを iMac で動作させている。 セキュリティ上の理由から、echo などの、システムプログラムの講義くらいで しか役に立たないようなサービスを停止することが、最近では一般的である。

2018/05/23。 echo サービスは、abelia04-abelia10 で動いている。 他のホストでは、動いていないかもしれない。

echoサービスのクライアント

TCP/IP のポート番号 7 では、送られてきた文字をそのまま返すサービスを提 供している。 実行例については、上の telnetによるecho サーバへの接続 を参照しなさい。

echo-client-fdopen-one.c

以下のプログラムは、echo サービスを利用するクライアントである。実行例 を先に示す。
$ cp ~yas/syspro/ipc/echo-client-fdopen-one.c . [←]
$ make echo-client-fdopen-one [←]
cc     echo-client-fdopen-one.c   -o echo-client-fdopen-one
$ ./echo-client-fdopen-one  [←]
Usage: ./echo-client-fdopen-one host port 'message'
$ ./echo-client-fdopen-one abelia02.coins.tsukuba.ac.jp 7 hello [←]
sent: 6 bytes [hello
]
received: 6 bytes [hello
]
$ ./echo-client-fdopen-one abelia02.coins.tsukuba.ac.jp 7 exit [←]
sent: 5 bytes [exit
]
received: 5 bytes [exit
]
$ ./echo-client-fdopen-one abelia02.coins.tsukuba.ac.jp 7 quit [←]
sent: 5 bytes [quit
]
received: 5 bytes [quit
]
$ []
このプログラムは、コマンドラインから3つの引数をとる。第1引数で指 定されたホスト上の、第2引数で指定されたポートで動作しているサーバに接 続する。そして、第3引数で与えられたメッセージをサーバへ送る。echo サービスのサーバは、同じ文字列を送り返して来る。このプログラムは、サー バから送り返されてきた文字列を受取り、結果を画面に表示する。 telnet コマンドとは異なり、文字列を1つしか送受信しない。

echo-client-fdopen-one.c のmain()

   1:	/*
   2:	  echo-client-fdopen-one.c -- 文字列を送受信するクライアント(TCP/IP版)
   3:	  ~yas/syspro/ipc/echo-client-fdopen-one.c
   4:	  Created on: 2009/06/01 21:13:38
   5:	*/
   6:	#include <stdio.h>
   7:	#include <stdlib.h>     /* exit() */
   8:	#include <string.h>     /* memset(), memcpy() */
   9:	#include <sys/types.h>  /* socket() */
  10:	#include <sys/socket.h> /* socket() */
  11:	#include <netinet/in.h> /* struct sockaddr_in */
  12:	#include <netdb.h>      /* getaddrinfo() */
  13:	#include <string.h>     /* strlen() */
  14:	#include <unistd.h>     /* close() */
  15:	
  16:	extern  int echo_client_one( char *server, int portno, char *message );
  17:	extern  int echo_send_request( FILE *out, char *message );
  18:	extern  int echo_receive_reply( FILE *in, char buf[], int size );
  19:	extern  int tcp_connect( char *server, int portno );
  20:	extern  int fdopen_sock( int sock, FILE **inp, FILE **outp );
  21:	
  22:	int
  23:	main( int argc, char *argv[] )
  24:	{
  25:	        char *server ;
  26:	        int portno ;
  27:	        char *message ;
  28:	        int err;
  29:	        if( argc != 4 )
  30:	        {
  31:	                fprintf( stderr,"Usage: %s host port 'message'\n",argv[0] );
  32:	                exit( -1 );
  33:	        }
  34:	        server  = argv[1] ;
  35:	        portno  = strtol( argv[2],0,10 );
  36:	        message = argv[3];
  37:	        err = echo_client_one( server, portno, message );
  38:	        return( err );
  39:	}
  40:	

main() 関数は、コマンドラインの引数を調べて、echo_client_one() を呼んで いる。第2引数のポート番号については、strtol() で、文字列として与えられ た数を、int に変換している。

echo_clien_onet()

  41:	#define BUFFERSIZE      1024
  42:	
  43:	int
  44:	echo_client_one( char *server, int portno, char *message )
  45:	{
  46:	        int sock ;
  47:	        FILE *in, *out ;
  48:	        char rbuf[BUFFERSIZE];
  49:	        int res;
  50:	
  51:	        sock = tcp_connect( server, portno );
  52:	        if( sock<0 )
  53:	                return( 1 );
  54:	        if( fdopen_sock(sock,&in,&out) < 0 )
  55:	        {
  56:	                fprintf(stderr,"fdooen()\n");
  57:	                close( sock );
  58:	                return( 1 );
  59:	        }
  60:	        res = echo_send_request( out, message );
  61:	        if( res < 0 )
  62:	        {
  63:	                fprintf(stderr,"fprintf()\n");
  64:	                fclose( in );
  65:	                fclose( out );
  66:	                return( 1 );
  67:	        }
  68:	        printf("sent: %d bytes [%s\n]\n",res,message );
  69:	        res = echo_receive_reply( in, rbuf, BUFFERSIZE );
  70:	        if( res < 0 )
  71:	        {
  72:	                fprintf(stderr,"fprintf()\n");
  73:	                fclose( in );
  74:	                fclose( out );
  75:	                return( 1 );
  76:	        }
  77:	        printf("received: %d bytes [%s]\n", res, rbuf );
  78:	        fclose( in );
  79:	        fclose( out );
  80:	        return( 0 );
  81:	}
echo_client_one() では、tcp_connect() という関数を呼び出している。この結果、 サーバとの間に TCP/IP通信路の開設され、通信可能なファイル記述子 (ファイルディスクリプタ) が返さ れる。このファイル記述子は、標準入出力(0,1,2)や open() システム・コー ルの結果と同じもので、 write() システムコールや read() システムコールの第一引数とし て使うことができる。つまり、write() システムコールを使うと、ネットワー クに対してデータを送り出すことができ、read() システムコールを使うとネッ トワークからデータを受け取ることができる。最後に不要になったら close() で解放する。

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

echo_send_request() を呼び出して、要求メッセージを送信している。 echo_receive_reply() を呼び出して、応答メッセージを受信している。

echo_send_request()

  78:	int
  79:	echo_send_request( FILE *out, char *message )
  80:	{
  81:	        int res;
  82:	        res = fprintf( out, "%s\n", message ); /* send a request with '\n' */
  83:	        return( res );
  84:	}
  85:	

echo_send_request() は、エコー・サービスで、要求メッセージを 送信する関数である。 TCP/IP の通信では、行単位(最後に\n)でデータを送受信することが多い。 このプログラムでは、fprintf() で行末に改行(\n)を付加している。

echo_receive_reply()

  86:	int
  87:	echo_receive_reply( FILE *in, char buf[], int size )
  88:	{
  89:	        char *res;
  90:	        res = fgets( buf, size, in ); /* receive a reply message */
  91:	        if( res )
  92:	                return( strlen(buf) );
  93:	        else
  94:	                return( -1 );
  95:	}
  96:	

echo_receive_reply() は、エコー・サービスで、応答メッセージを 受信する関数である。 fgets() を使って、文字列のデータを行末「\n」まで受信している。

echo サービスでは、1行送り、1行受け取る。他のサービスでは、1行送っ て複数行受け取ったり、受け取る方では行の概念がなくなるもの(HTTPで画像 データを受け取る場合など)もある。その場合は、fprintf() や fgets() では なくて、fwrite() や fread() を使う必要がある。

HTTP

WWW (the World-Wide Web)では、TCP/IP の上にさらに HTTP (HyperText Transfer Protocol)と呼ばれるプロトコルを構築し、データの転送を行ってい る。Firefox や Lynx などのブラウザは、WWW サーバとの間に TCP/IP による 通信路を開設する。そして、クライアントは、必要なデータを得るための命令 を送る。これに対してサーバは、命令に応じた処理を行い結果を返す。この命 令の形式や結果の形式を定めたものが、HTTP である。HTTP 通信プロトコルを 受け付けるサーバを、HTTP サーバと呼ぶ。

HTTPの要求

HTTP の要求は、最初に要求の種類(メソッド)を含む行があり、以後、オプショ ンが続く。最後に、空行があり、要求の終わりを示す。 たとえば、次のような URL を持つデータをアクセスすることを考える。

http://www.coins.tsukuba.ac.jp:80/~syspro/

Firefox などのクライアントは、まずホスト名 www.coins.tsukuba.ac.jp とポート 番号 80 を使ってサーバとの間に TCP/IP の通信路を開設する。そして、クラ イアントは、開設した通信路を使って、サーバに次のような文字列を送る。

GET /~syspro/ HTTP/1.0←↓
Host: www.coins.tsukuba.ac.jp←↓
←↓

ここで、"GET" が命令の種類、"/~syspro/" は、GETの引数の、要求してい るデータを表わす URL (ファイル名)、"HTTP/1.0" は、使っているプロトコル のバージョンである。次の空行は、命令のヘッダ部分の終りを意味するもので あり、必要である。「←」は、キャリッジ・リターンのコード(0x0d,C言語で' \r')、「↓」は、ニューラインのコード(0x0a,C言語で'\n')である。HTTP の ヘッダでは、行末に「←↓」を付けるように規定されている。(サーバを構築 する場合には、「←」か「↓」のどちらか1つしかこない場合でもきちんと動 作することが求められている。)

"Host:" は、名前ベースの仮想ホストを使っている場合に必要となる。

HTTPの要求での Host: の役割

1つの httpd サーバで、複数のドメイン名の Web サーバを兼ねていることがあ る。この機能を、Apache httpd では、仮想ホスト( バーチャルホスト、 virtual host)と呼んでいる。

仮想ホストの実現方法には、2種類ある。

名前ベースの仮想ホストを使っているような Web サーバでは、HTTP の要求で、 Host: という行も送信する必要がある。Host: がないと、エラーの応答 "HTTP/1.0 302 Found" が返されることがある。

HTTPの応答

GET による要求に対して、サーバは、クライアントへ次のような応答メッセー ジを送り返す。
HTTP/1.1 200 OK←↓
Date: Tue, 22 May 2018 13:01:22 GMT←↓
Server: Apache←↓
Last-Modified: Thu, 13 Apr 2018 08:17:10 GMT←↓
ETag: "23fea50-6de-54d07f3908fac"←↓
Accept-Ranges: bytes←↓←↓
Content-Length: 1758←↓
Connection: close←↓
Content-Type: text/html←↓
←↓  (空行)
本文(HTML)
応答のうち、最初の行が、状態行(status line)と呼ばれる、要求が成功した か失敗したかわ表わしている行である。"200" とは、成功したという意味であ る(表3参照)。2行目から最初の空行(「←↓」だけの行)までは、応答メッ セージのヘッダである。応答メッセージのヘッダには、データの型や、サーバ のバージョン、データが更新された日付と時刻、バイト数などが記録されてい る。

応答で、最初の空行(「←↓」だけの行)の次が、データの本体である。この 例では、HTMLで記述されたデータが返されている。サーバは、データ転送が完 了すると、TCP/IP の通信路を切断する。

クライアントは、受け取ったデータを整形して利用者に対して表示する。たと えば、インライン・イメージとして指定されたデータを続けてサーバに要求し て展開したり、フォントを変えたりして表示する。

telnetによるWWWサーバへのアクセス

以下の例は、telnet コマンドを用いて coins の Web サーバをアクセスした例 である。(注意:データは、更新されることがあるので、必ずしもこの通りの データが返されるとは限らない。)
$ telnet www.coins.tsukuba.ac.jp 80↓
Trying 130.158.87.1...
Connected to violet-nwa.coins.tsukuba.ac.jp.
Escape character is '^]'.
GET /~syspro/ HTTP/1.0↓
Host: www.coins.tsukuba.ac.jp↓
↓
HTTP/1.1 200 OK←↓
Date: Tue, 22 May 2018 10:33:35 GMT←↓
Server: Apache←↓
Last-Modified: Mon, 02 Apr 2018 13:59:57 GMT←↓
ETag: "332412e-71b-568de0146d295"←↓
Accept-Ranges: bytes←↓
Content-Length: 1819←↓
Connection: close←↓
Content-Type: text/html←↓
←↓
<HTML>↓
<HEAD>↓
<META HTTP-EQUIV="content-type" CONTENT="text/html;charset=iso-2022-jp">↓
<TITLE> システムプログラム↓
</TITLE>↓
<STYLE TYPE="text/css"><!--↓
@import url(coins-syspro.css);↓
--></STYLE>↓
</HEAD>↓
↓
<BODY>↓
↓
<H1><A ID="title">システムプログラム</A></H1>↓
↓
</P><P>↓
↓
<H2><A ID="2017/" HREF="2018/">■2018年</A></H2>↓
<H2><A ID="2017/" HREF="2017/">■2017年</A></H2>↓
<H2><A ID="2016/" HREF="2016/">■2016年</A></H2>↓
<H2><A ID="2015/" HREF="2015/">■2015年</A></H2>↓
...中略...
<HR>↓
Last updated: 2017/04/13 17:17:09↓
 <BR>↓
<ADDRESS> <A HREF="http://www.cs.tsukuba.ac.jp/~yas/">Yasushi Shinjo</A> / &lt;yas@is.tsukuba.ac.jp&gt; </ADDRESS>↓
</BODY>↓
</HTML>↓
Connection closed by foreign host.
$ []

telnet で接続した後に、HTTP の要求メッセージを3行(空行含む)送っている。 それに対して、HTTP/1.1 200 OK 以下が、HTTPの応答である。

HTTPのメソッド

表2に、よく使われるHTTP で定義されている命令(メソッド)の例を示す。こ れらの命令に対して、サーバは、表3に定義されたような応答を行う。

表2 HTTPで定義されている命令(methods)の例

命令		説明
--------------------------------------------------------------------
GET		情報を得る(ヘッダと本体の両方)
HEAD		情報のヘッダのみを得る
POST		新しく情報を作る

HTTPの応答

HTTPの応答では、次のようなコードがよく使われる。 200番台は、エラーが無いことを意味する。

表3 HTTPで定義されている状態コードの例

状態コード	説明
--------------------------------------------------------------------
200		OK(エラーなし)
301		要求されたデータが移動した
302		見つからない
303		別のページを見よ
304		ページは変更されていない
400		要求の形式にエラーがある
401		ページの閲覧が承認されななかった
403		アクセスが許されていない
404		要求されたデータが見つからない
500		サーバで内部エラーが起きた
501		メソッドが実装されていない

参考

細かい話

最初は読み飛ばして良い。

TCP/IP

TCP/IP は、TCP と IP という2つのプロトコルか らなる。TCP/IP 上の応用層と、IP データグラムを運ぶ物理層まで考えると、 全体としては 4 層のプロトコル・スタックからなる。

ソケットAPI

Unix 系のオペレーティング・システム上でTCP/IPを使うプログラムを開発する には、ソケットAPIを用いる。 システムプログラムの授業では、 tcp_connect()等のAPI を利用することを勧める。

DNS(Domain Name System)

DNS は、TCP/IP と共に使われる名前サービスであ る。 DNS は、ホスト名を IP アドレスに変換するサービスを提供する。 プログラムで利用するには、一般にはライブラリ関数 getaddrinfo() を使う。 システムプログラムの授業では、 tcp_connect()等のAPI を利用することを勧める。

tcp_connect()の詳細

tcp_connect() は、通信路の開 設の仕事のうち、クライアント側の仕事をする関数である。 システムプログラムの授業では、 利用方法 がわかれば十分である。 興味がある人は、 tcp_connect()の詳細 を見なさい。

fdopen_sock()の詳細

fdopen_sock() は、TCP/IP による通信を、fprintf(), fgets(), fread() 等で 行えるようにする関数である。 システムプログラムの授業では、 利用方法 がわかれば十分である。 興味がある人は、 fdopen_sock()の詳細 を見なさい。

marshaling/unmarshaling

TCP/IPでプログラムを作成する場合、メモリ中のデータ項目とネットワーク上を流れる データを対応づける必要がある。これを、 marshaling/unmarshalingという。

システムプログラムの授業では、主に次のデータを扱うことにし、 marshaling/unmarshalingの問題を深くは取り扱わない。

2バイト以上の 整数 では、 ビット数 バイト・オーダ の問題がある。

echoサービスのクライアント(Java版)

Java 言語で記述した echo-client-fdopen-one.c である。

練習問題

練習問題(601) telnetコマンドによるWebサーバへのアクセス

telnetコマンドを使って、Web サーバに接続して HTML 等のテキストファイルを画面に表示しなさい。
$ telnet サーバ名 80 [←]
接続後、キーボードからHTTPに従い要求メッセージを打ち込む。
たとえば、以下の例は、coins のユーザ ~syspro の ホーム・ページ http://www.coins.tsukuba.ac.jp/~syspro/ を得るための要求を示す。
$ telnet www.coins.tsukuba.ac.jp 80 [←]
GET /~syspro/index.html HTTP/1.0[←]
Host: www.coins.tsukuba.ac.jp[←]
[←]
最初の行は、空白で区切られた3つの部分がなること、改行が2個あることに 注意しなさい。要求を打つと、問題がなければ画面には目的の HTML ファイル が表示される。

syspro のホーム・ページは、文字コードとして JIS (ISO-2022-JP) を用いている(2018年5月)。 自分で試す時には、 "kterm -fn a14 -fk k14" 等の JIS コードを表示できる端末を使うか、 Emacs shell で M-x set-buffer-process-coding-system で junet にすると良い。 あるいは、 ~syspro/index.html 以外で、英語や端末の文字コードと一致しているページを選ぶと よい。たとえば、自分のホームディレクトリ以下の ファイル(~/public_html/htdocs/file1.html) を 作成し、アクセスするには、GET に次のようなファイル名を与える。

GET /~ログイン名/file1.html HTTP/1.0←↓
Host: www.coins.tsukuba.ac.jp←↓
←↓

レポートには、次のものを含めなさい。

余裕があれば、次のことも行いなさい。

練習問題(602) wgetコマンド

wget は、URLを引数として取り、その資源をサーバから取得してファイルに保 存するコマンドである。wget コマンドを使ってみなさい。
$ wget  http://www.coins.tsukuba.ac.jp/~syspro/index.html [←]
詳しくは、man wget か、wget -h を実行しなさい。 次のオプションを使ってみなさい。
-d (--debug)
デバッグ。HTTPの要求と応答を画面に出力する。
--save-headers
応答メッセージも保存する。
-c (--continue)
中断したダウンロードを再開する。

練習問題(603) HTTP要求メッセージの作成

練習問題(601) で、キーボードからどのような文字列を打っ たのかを思い出しなさい。そして、それを printf() で画面に表示するプログ ラムを作成しなさい。

$ ./print-http-request-fixed [←]
GET /index.html HTTP/1.0[←]
Host: www.coins.tsukuba.ac.jp[←]
[←]
$ []

次のファイルをコピーして修正しても良い。

$ cp ~yas/syspro/ipc/print-http-request-fixed.c . [←]
$ cp ~yas/syspro/ipc/PrintHttpRequestFixed.java . [←]
注意1: この課題では、fgets() 等でキーボードから入力することはしない。出 力するデータは、プログラムの中に埋めこまれたデータを使いなさい。

注意2: この課題では、printf() ではなく、fprintf() を使いなさい。そうす ると、ネットワーク通信を行うプログラムに改変する時に簡単になる。

練習問題(604) HTTP要求メッセージの作成/引数付き

練習問題(603) で、どの ホストに対してどのようなファイルを 取るかを main の引数 で取れるようにしなさい。
$ ./print-http-request www.example.com /index.html [←]
GET /index.html HTTP/1.0[←]
Host: www.example.com[←]
[←]
$ ./print-http-request www.coins.tsukuba.ac.jp /~syspro/index.html [←]
GET /~syspro/index.html HTTP/1.0[←]
Host: www.coins.tsukuba.ac.jp[←]
[←]
$ []
次のファイルをコピーして修正しても良い。
$ cp ~yas/syspro/ipc/print-http-request.c . [←]
$ cp ~yas/syspro/ipc/PrintHttpRequest.java . [←]
注意1: この課題では、fgets() 等でキーボードから入力することはしない。出 力するデータは、main() の引数、または、プログラムの中に埋めこまれたデー タを使いなさい。

注意2: この課題では、printf() ではなく、fprintf() を使いなさい。そうす ると、ネットワーク通信を行うプログラムに改変する時に簡単になる。

練習問題(605) テキストを扱うHTTPクライアント

この課題を解く前に必ず 練習問題(601) , 練習問題(603) , および、 練習問題(604) を行いなさい。レポートには、 練習問題(605) の結果だけでなく、 練習問題(601) , 練習問題(603) , および、 練習問題(604) の結果も含めなさい。

HTTPサーバに HTML 等のテキスト・ファイルを要求し、その内容を画面に表示 するプログラムを作りなさい。このプログラムの名前を、wcat とする。

wcat コマンドは、次のように3つの引数を与えて利用するものとする。

$ ./wcat host port file [←]
(ここに、サーバから取得したテキストが表示される)
$ []
ここで、host は、ホスト名、port は、TCP/IP のポート番号、file は、得る べきファイル名である。これは、URL の文法で記述すると、次のようになる。

http://host:port/file

なお、wcat では、ポート番号の引数を省略しないものとする(省略可能なよ うに工夫してもよい)。HTTP プロトコルで用いられる標準のポート番号は、 80である。

プログラム全体の構造は、次のようになる。

  1. main() の引数を解析する。 (echo-client-fdopen-one.cのmain() および、 練習問題(604) を真似る。)
  2. ホスト名とポート番号を用いて、TCP/IPで通信路を作る。 (echo-client-fdopen-one.cのecho_clien_onet()で、 tcp_connect(), fdopen_sock() の使い方を参考にする。 tcp_connect(), fdopen_sock() のソースコードは、 echo-client-fdopen-one.cに 含まれているので、それを cp コマンドでコピーして使いなさい。 )
  3. HTTP で要求のヘッダを fprintf() で送信する。 送信内容は、練習問題(604) と同じで、 送信先が画面(stdout)ではなく、TCP/IP の通信路である。
  4. HTTP で応答のヘッダ、および、本文のテキストを fgets() で1行受信する。 受信したメッセージを画面(stdout)へ fputs() や fprintf() で出力する。 サーバからは、複数行送られてくるので、 これらの受信と表示を、eof (end of file) になるまで繰り返す。
読み込んだヘッダは、画面に表示してもよい。 空行に続いて、本体を受信する。テキストのみを扱う場合、ヘッダと同じ方法 で受信してもよい。受信した本文は、必ず画面(標準出力)に表示する。

プログラムをつくる時には、 行末の扱い(CR-LF)に注意しないさい。 行末は、\n ではなく \r\n となっていることがある。 空行も、"\n" ではなく "\r\n" となっていることがある。

練習問題(606) HTTP応答メッセージの解析(ヘッダの表示)

HTTPの応答メッセージを解析するプログラム http-response-header を作りな さい。 http-response-header は、引数として指定されたファイルに含まれるHTTPのヘッ ダのみを表示するプログラムである。
$ ./http-response-header file.txt [←]
(ここにヘッダ部分が表示される)
$ []
HTTPの応答のヘッダ部分は、複数行から構成される。ヘッダの終わりには 空行がある。従って、空行が来るまで、ループして行単位でヘッダを読み込む。

このプログラムを作成する時に用いるデータは、 wget コマンドを用いて作成す ることができる。

$ wget --save-headers URL -O file.txt [←]
注意:wget の -O は、大文字である。 小文字-o は、別の意味がある。 wget のバージョンによっては、--save-headers の代わりに-S が使えることもある。

練習問題(607) HTTP応答メッセージの解析(本体の表示)

練習問題(606) と同様に、HTTPの応答メッセー ジを解析するプログラムhttp-response-content を作りなさい。 http-response-content は、引数として指定されたファイルに含まれるHTTPの 本体のみを表示するプログラムである。
$ ./http-response-content file.txt [←]
(ここに本体部分が表示される)
$ []

練習問題(608) テキストを扱うHTTPクライアント(応答の解析)

練習問題(605) に対して、次の機能を付加しなさい。

練習問題(609) バイナリを扱うHTTPクライアント

練習問題(608) で、テキストだけでなくバイナリ・ データを受け取り、また、受け取ったデータをファイルに保存するプログラム を作成しなさい。このプログラムの名前を、wsave とする。 wcat コマンドは、次のように4つの引数を与えて利用するものとする。
$ ./wsave host port file localfile [←]
(画面には何も出力されない)
$ []
最初の3つの引数は、練習問題(605) のwcatと同じである。 最後の引数は、保存するローカル・ファイルである。

このプログラムでは、本体部分ではバイナリデータを扱う必要がある。 fdopen_sock() を使う場合、ヘッダについては、fgets() を使っ てデータを送受信してもよい。しかし、ヘッダが終わった後、本体部分では、 fread() を使う必要がある。

本体をファイルに保存する部分は、 前半第3回/ファイルアクセス(応用) のプログラムと似たものになると思われる。 ただし、コピー元は、ネットワークで、コピー先はファイルになる。 fread() を使う場合には、入力したバイト数(読み込んだ要素の数)を調べ、 そのバイト数の分を fwrite() 等でファイルに出力する。

HTTPの応答は、バッファ・サイズよりも大きくなる可能性がある。1回の fread() では受信できないことがある。そのため、すべてのデータを受信する まで、バッファ単位でループする必要がある。

また、fread() で文字列を読み込んだとしても、最後に 0 (NULL) で終端 されないので、注意しなさい。

レポートには、 練習問題(605) と同様の結果も含めなさい。

練習問題(610) NNTPPクライアントの作成

NNTPクライアント nncat を作成しなさい。

まず、telnet で、これらのサーバに接続しなさい。そして、それぞれのプロ トコルに従って、要求を打ち込み、どのような結果が返ってくるかを調べなさ い。

次に、telnet で行った要求の送信と結果の受信を行うようなプログラムを作り なさい。このとき、必要なパラメタ(ニュースグループ名や記事番号を含む)は、 main() の引数から取りなさい。 NNTP は、テキスト・ベースのプロトコルなので、全ての通信にfprintf() や fgets() を使ってもよい。

この課題では、fgets() でキーボードからデータを読み込むことはしてはなら ない。main() の引数で得られるパラメタ以外で、サーバに送るべきデータは、 プログラムの内部で fprintf() や snprintf() 等を用いて作成すること。 たとえば、"GROUP " や "ARTICLE" などは、fprintf() のフォーマット文字列 で指定する方法がある。キーボードや main() の引数として、本来プログラム で生成すべき文字列を与えてはならない。

この課題では、サーバからの応答を解析すること。エラーから生じた時には、 以後の処理を中止する機能を持つこと。

接続先として次のホストを使いなさい。

プログラムをつくる時には、 行末の扱い(CR-LF)にも注意しないさい

練習問題(611) SMTPクライアントの作成

練習問題(610) と同様に、 SMTP のクライアント smtpput を作成しなさい。

まず、telnet で、これらのサーバに接続しなさい。そして、それぞれのプロ トコルに従って、要求を打ち込み、どのような結果が返ってくるかを調べなさ い。

次に、telnet で行った要求の送信と結果の受信を行うようなプログラムを作り なさい。このとき、メールサーバ、送信元アドレス、送信先アドレスは、 main() の引数から、メール本文は、標準入力から入力しなさい。 SMTP は、テキスト・ベースのプロトコルなので、全ての通信にfprintf() や fgets() を使ってもよい。

この課題では、fgets() でキーボードからデータを読み込むことはしてはなら ない。main() の引数で得られるパラメタ以外で、サーバに送るべきデータは、 プログラムの内部で fprintf() や snprintf() 等を用いて作成すること。 たとえば、"MAIL FROM:" や "RCPT TO:" などは、fprintf() のフォーマット文 字列で指定する方法がある。キーボードや main() の引数として、本来プログ ラムで生成すべき文字列を与えてはならない。

この課題では、必ずサーバからの応答を解析すること。エラーから生じた時に は、以後の処理を中止する機能を持つこと。

接続先として次のホストを使いなさい。

プログラムをつくる時には、 行末の扱い(CR-LF)にも注意しないさい

練習問題(612) 並列wsave

練習問題(609) で、 wget コマンドと同様に複数の URL を引 数に取り、複数のファイルを並列にコピーするものを作成しなさい。ただし、 並列度としては、最大n(1<n<=3)とし、それ以上の URL が与えられ た時でも、その並列度の範囲内で同時にコピーするようにしなさい。

ヒント:最大nまで、fork() して、それぞれ子プロセスで1個のファイルを コピーする。子供が終了したら、次の子供を fork() する。

実行速度に差があるので、複数のプロセスに均等にURL をばらまく方法では最 速にはならない。

練習問題(613) 中断・再開可能なwsave

練習問題(609) で、 (大きな)ファイルをコピーする時、中 断する機能をつけなさい。中断とは、 HTTP 1.1で定義されている Range: の機能を 使い、一度プログラムを終了しても、次に再開した時に続きを要 求する機能である。

プログラムを終了した後でもう一度実行したときに続きを行う行うことができ るものだけをこの課題を満たしたと認める。プログラムを終了しないものは、 この課題では中断とは認めない。 この課題ではシグナルでプロセスを中断することは行わない。

wget コマンドは、-c オプションを指定すると、中断した続きから再開する。

練習問題(614) Time Protocol のクライアント

RFC868 に定義されている Time Protocol のクライアン ト(TCP) を作成しなさい。そして、date コマンドと類似の結果を表示しなさ い。
$ ./time-client host 37 [←]
Sun May 26 20:54:23 JST 2018
$ []

このプログラムでは、TCP/IP でサーバに接続した後、何も送らずにサーバか ら4バイトの数を読み込む。その4バイトの数は、ネットワーク・バイト・オー ダになっているので、ntohl() で、ホストのバイト・オーダに変換する。この 値に、ある値で補正して、Unix で使われているtime_t に変換する。最後に、 strftime() や localtime() でカレンダーの形式に変換する。

RFC868 Time Protocol では、値は、1900年1月1日 0:00 (GMT) を基準にした 秒数を返す。time() システムコールや gettimeofday() システムコールでは、 1970 年を基準にしている。strftime() や localtime() を使う前に、差分を補正する必要がある。

この課題では、ポート番号 37 の time を使いなさい。ポート番号 13 の daytime を使ってはならない。

接続先のホストとしては、次のどれかを使いなさい。

練習問題(615) サービス名によるポート番号

上の echo-client-fdopen-one.c では、ポート番号を数で 与えている。これを、サービス名によりポート番号が指定できるように書き換 えなさい。

練習問題(616) netstatコマンド

netstat コマンドを使うと、その時利用されている結合(connection)を表示す ることができる。このコマンドを使って、ネットワークの利用状況を表示しな さい。

次のようなオプションがよく使わせる。

-a (all)
全て。接続を待っているサーバのソケットも表示する。
-n (number)
ネットワークのアドレスを番号で表示する。

練習問題(617) lsofコマンド

lsof コマンドは、プロセスが開いているファイルを表示するコマンドであ る。-i オプションを付けると、ファイルではなくネットワーク (Internet) の 利用状況が表示される。lsof -i を利用して、ネットワークの利用状況を表示 しなさい。

練習問題(618) WebブラウザによるHTTPの表示

Web ブラウザには、開発者用の道具として、HTTP で送受信しているデータを表 示する機能が利用可能になっていることがある。このような機能を利用してみ なさい。ブラウザによっては、機能拡張モジュール(add-on) をインストールす る必要がある。

練習問題(619) opensslコマンドによるWebサーバへのアクセス

openssl コマンドを使ってSSL 付きの Web サーバ (URL が https: で始まって いる)に接続して HTML 等のテキストファイルを画面に表示しなさい。 https: では、ポート番号としては標準で 443 が使われる。
$ openssl s_client -connect サーバ名:ポート番号 -quiet [←]
接続後、キーボードからHTTPに従い要求メッセージを打ち込む。
たとえば、以下の例は、coins のトップページの https://www.coins.tsukuba.ac.jp/ を得るための要求を示す。
$ openssl s_client -connect www.coins.tsukuba.ac.jp:443 -quiet
depth=1 C = JP, L = Academe2, O = National Institute of Informatics, OU = UPKI, OU = NII Open Domain CA - G2
verify error:num=20:unable to get local issuer certificate
verify return:0
GET / HTTP/1.0[←]
Host: www.coins.tsukuba.ac.jp[←]
[←]
HTTP/1.1 200 OK
Date: Thu, 24 Jul 2018 06:51:09 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
X-Pingback: https://www.coins.tsukuba.ac.jp/wp/xmlrpc.php
Link: <https://www.coins.tsukuba.ac.jp/>; rel=shortlink
Connection: close
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html lang="ja">



<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge, chrome=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>筑波大学 情報学群 情報科学類 | 大切な仲間と最先端のカリキュラムを</title>
・・・中略・・・
</body>
</html>

$ [←]

Last updated: 2018/05/22 22:24:51
Yasushi Shinjo / <yas@cs.tsukuba.ac.jp>