Previous Up Next

 6  モダンなプロセス間通信: ソケット

パイプを使った通信にはいくつか制限があります。まず、パイプを使うとローカルのマシンとしか通信できません。名前付きパイプではやり取りをするプロセスが同じマシンで実行される必要があり、名前無しパイプでは二つのプロセスは親子関係になくてはいけません。次に、パイプでは クライアント-サーバ モデルという便利な通信モデルを使うことが難しいです。このモデルではちょうど一つの サーバ プログラムだけが共有リソースにアクセスできます。そして他の クライアント はサーバにアクセスすることで共有リソースにアクセスします。サーバが共有リソースへのアクセスの制御と管理を行うということです。例えば x-window システムでは共有リソースとはスクリーンとキーボード、そしてマウスです。

クライアント-サーバモデルをパイプで実装するのは難しいです。一番の困難はクライアントとサーバの間の接続を確立する部分です。名前無しパイプではこれは不可能です。サーバとクライアントに共通の親が必要であり、その親は任意の数のパイプを前もって保持する必要があるからです。名前付きパイプを使えばサーバがクライアントからの接続を読むことは不可能ではありません。しかしこの場合サーバが受け取るリクエストにはクライアントがサーバとやり取りするのに使う他の名前付きパイプの名前も含まれるので、複数のクライアントからの接続リクエストをどう排他制御するかが問題となります。

ソケットはこれらの問題を解決するために一般化されたパイプです。ソケットの説明のために、まずクライアント-サーバモデルを図 3 に示します。

Figure 3 — クライアント-サーバモデル
  1. サーバ U がソケット s をポート p に作成し、接続を待つ (1)。クライアントはポート番号 p を知っている。
  2. クライアント A がソケットを作成し、ポート p に接続する (2)。サーバ上では、システムがクライアント A とプライベートにやり取りするための新しいソケットを確保する (3)。この例ではサーバは補助サーバ V をフォークし、A との接続を閉じる (4)。 閉じられた接続は破線で示されている。以降はこの子プロセス VA との通信を行う (5)。
  3. サーバは新しいクライアント B を受け付けることができる。新しい接続が作成され、クローンしたプロセス W によって通信が行われる (6)。以降も同様となる。
  4. サーバはソケット s に結び付けられたファイルディスクリプタを閉じることでサービスを終了できる。システムはポート p を開放して再利用する。このポートは例えば他のサービスをインストールするのに使われる。

上記のモデルでは、サーバ U とクライアント A はプライベートな接続 (3) を確立することで他のクライアントの影響を受けずにやり取りを行います。このことから、この通信方式は コネクション型通信 と呼ばれます。通信が短ければサーバが (フォークせずに) 直接リクエストを処理することもできます。しかしこうするとサーバが接続 (3) を処理するのを待つため、あるいは複数の接続が多重化によって管理されているために、クライアントはサーバが利用可能になるまで待たなければいけません。

ソケットを使うと コネクションレス型通信 を利用することもできます。この方式ではサーバはクライアントとプライベートな接続を確立することなく直接通信します。コネクション型通信と比べるとこの方式が使われることは少ないです。 6.10 節で少しコメントしますが、この章では主にコネクション型通信を扱います。

6.1  ソケット

ソケットはパイプの拡張であり、bsd 4.2 で初めて実装されました。現在ではネットワークに接続するすべての Unix がソケットの機能を持ちます。クライアント-サーバモデルに沿った通信を行うための専用のシステムコールが存在します。このシステムコールによって、プロセス間のローカル通信およびリモート通信を (ほぼ) 透過的に行うことができます。

ソケットの 通信ドメイン は通信できるプロセス (およびそのアドレスのフォーマット) を制限します。異なる通信ドメインが利用可能です。例えば:

ソケットの 通信方式 は通信が信頼できるか (データの消失や重複があるか)、そしてデータの送受信の方法 (バイトストリーム、パケット列 — バイトの小さなブロック) を示します。通信方式はデータの送受信に使われるプロトコルを規定します。いくつかの通信方式が利用可能です。三つの通信方式をその特徴と共に示します:

方式信頼性データ表現
ストリーム信頼できるバイトストリーム
データグラム信頼できないパケット
セグメント化されたパケット信頼できるパケット

“ストリーム” 方式はパイプを使った通信にとても良く似ており、一番良く使われます。例えば構造化されていないバイトシーケンスの転送 (例えば rsh) などに使われます。“セグメント化されたパケット” 方式はデータをパケットとして転送します。すべての書き込みはパケットごとに区切られ、すべての読み込みは多くとも一つのパケットしか読み込めません。この方式はメッセージ指向の通信に適しています。“データグラム” 方式はイーサネットネットワークのハードウェアの特徴に一番近いです。データはパケットで転送されますが、それぞれのパッケットが目的地に届く保証はありません。ネットワークのリソースという観点では、この方式が一番無駄が少ないです。この方式は致命的な重要性を持たないデータの転送を行うプログラム (例えば biff) によって使われます。またデータの消失を手動で管理することでネットワークのパフォーマンスを向上させることもできます。

6.2  ソケットの作成

システムコール socket は新しいソケットを作成します:

val socket : socket_domain -> socket_type -> int -> file_descr

返り値は新しいソケットを表すファイルディスクリプタです。初期状態ではこのディスクリプタは “切断” 状態であり、 readwrite を受け付ける準備が整っていません。

第一引数は socket_domain 型の値で、通信ドメインを指定します:

PF_UNIXユニックスドメイン
PF_INETインターネットドメイン

第二引数は socket_type 型の値で、通信方式を指定します:

SOCK_STREAM信頼できるバイトストリーム
SOCK_DGRAM信頼できないパケット
SOCK_RAWネットワークの下層への直接のアクセス
SOCK_SEQPACKET信頼できるパケット

第三引数は通信で使うプロトコルです。通信ドメインに対するデフォルトのプロトコル (例えば SOCK_DGRAM に対しては udpSOCK_STREAM に対しては tcp など) を選択する 0 が通常使われます。第三引数に他の値を設定すると特殊なプロトコルを使うことができます。例えば ping コマンドは icmp (Internet Control Message Protocol) を使って、自動的に送信者に送り返すパケットを送ります。特殊なプロトコルに対する引数の値は /etc/protocols ファイルまたは nis (Network Information Service) データベース の protocols テーブルが存在すればそこにあります。システムコール getprotobyname はプロトコルに関する情報をポータブルな形で返します。

val getprotobyname : string -> protocol_entry

引数はプロトコルの名前で、返り値は protocol_entry 型のレコードです。このレコードの p_proto フィールドがプロトコルの番号を表します。

6.3  アドレス

ソケットに関する操作はソケットアドレスを利用するものがあります。ソケットアドレスはヴァリアント型 sockaddr によって表されます:

type sockaddr = | ADDR_UNIX of string | ADDR_INET of inet_addr * int

ADDR_UNIX f は Unix ドメインにおけるアドレスです。 f は対応するファイルのマシンのファイルシステムにおける名前です。 ADDR_INET (a,p) はインターネットドメインにおけるアドレスです。 a がマシンのインターネットアドレスで、p がそのマシンのポート番号です。

インターネットアドレスは抽象型 inet_addr を表します。次の二つの関数は 128.93.8.2 のような形をした文字列と inet_addr 型の値の間の変換を行います。

val inet_addr_of_string : string -> inet_addr val string_of_inet_addr : inet_addr -> string

インターネットアドレスを調べるもう一つの方法はホストの名前を使って /etc/hosts にあるテーブル、nis データベース、あるいはドメインネームサーバから引くことです。システムコール gethostbyname がこれを行います。現代的なマシンではドメインネームサーバへの問い合わせが最初に行われ、/etc/hosts はフォールバックとしてしか使われませんが、一般的にはこの順番はマシンの設定によります。

val gethostbyname : string -> host_entry

引数はホストの名前で、返り値は host_entry 型のレコードです。このレコードの h_addr_list フィールドが目的のマシンのインターネットアドレスを表します (同じマシンが複数のネットワークに違うアドレスで接続することは可能です)。

最もよく使われるサービスが使うポート番号は /etc/services のテーブルにリストされており、getservbyname 関数でポ−タブルに取得できます。

val getservbyname : string -> string -> service_entry

第一引数はサービスの名前 (ftp サーバには "ftp"、E メールには "smtp"、ニュースサーバには "nntp"talkntalk にはコマンドと同じ文字列、など) です。第二引数はプロトコルの名前です。プロトコルの名前にはサービスの通信方式がストリーム方式の場合は "tcp" が、データグラム方式の場合は"udp" が通常使われます。getservbyname の返り値は service_entry 型の値で、s_port フィールドがサービスが用いるポート番号を表します。

pauillac.inria.frftp サーバのアドレスを入手するには以下のようにします:

ADDR_INET((gethostbyname "pauillac.inria.fr").h_addr_list.(0), (getservbyname "ftp" "tcp").s_port)
* * *

6.4  サーバへの接続

システムコール connect はソケットに関連づいたサーバとの通信を確立します。

val connect : file_descr -> sockaddr -> unit

第一引数はソケットのディスクリプタで、第二引数はサーバのアドレスです。

接続が確立されると、ソケットのディスクリプタに対する write はサーバにデータを送信し、read はサーバからのデータを受信します。ソケットは入出力の操作に対してパイプのように振る舞います。まず、 read はデータが利用可能でない場合にはブロックし、要求よりも少ないバイト数を返すことがあります。次に、サーバが接続を閉じると read0 を返し、 write は読んだプロセスに sigpipe シグナルを送ります。

connect 関数はソケットをシステムによって選ばれたローカルアドレスにバインドします。このアドレスを手動で選んだほうが良い場合もあります。そのような場合は connect 関数を呼ぶ前に bind 関数 (6.7 節を参照) を呼ぶことでアドレスを選ぶことができます。

Unix コマンド netstat は現在のマシン上の接続と状況をリストします。

6.5  ソケットの切断

ソケットを切断する方法は二つあります。最初の方法はソケット close を呼んで入出力用の接続が閉じることです。しかしこれでは大雑把すぎることがあります。例えばファイルの終端を伝えるためにクライアントからサーバの接続を閉じつつもサーバからのデータを受け取るために反対側の接続は保ちたい場合があります。システムコール shutdown を使うと接続の一部を切断できます。

val shutdown : file_descr -> shutdown_command -> unit

第一引数は閉じるソケットのディスクリプタで、第二引数は shutdown_command 型の値で、どちらの方向の節毒を閉じるのかを指定します。

SHUTDOWN_RECEIVE読み込み用のソケットを閉じる。接続のもう一端における writesigpipe シグナルを呼び出しプロセスに送るようになる。
SHUTDOWN_SEND書き込み用のソケットを閉じる。接続のもう一端における read はEOF を返すようになる。
SHUTDOWN_ALL読み込みと書き込み用のソケットを両方閉じる。close と違い、閉じられたソケットのディスクリプタは開放されない。

ソケットの切断には closeshutdown のどちらを使っても多少の時間がかかることがあることに注意してください。

6.6  完全な例: ユニバーサルクライアント

client host porthost マシンの port ポートへの接続を確立するような clientコマンドを作成します。さらにこのコマンドは標準入力からのデータを作成したソケットから送信し、受信したデータを標準出力へ書き込みます。例えば、以下のコマンド

echo -e 'GET /~remy/ HTTP/1.0\r\n\r\n' | ./client pauillac.inria.fr 80

pauillac.inria.fr80 版ポートに接続し、ウェブページ /~remy/ に対する http リクエストを送ります。

client が行うのはクライアントとの接続を確立する部分だけであり、特定のプロトコルの実装は client を呼んだプログラムに任されます。この意味で、 client コマンドは “ユニバーサルな” クライアントアプリケーションです。

ライブラリ関数 Misc.retransmit fdin fdout はディスクリプタ fdin から読んだデータを fdout に書き込みます。入力ディスクリプタの終端に達するとディスクリプタを閉じずに関数は終了します。retransmit はシグナルで中断されることがあることに注意してください。

let retransmit fdin fdout = let buffer_size = 4096 in let buffer = String.create buffer_size in let rec copy () = match read fdin buffer 0 buffer_size with | 0 -> () | n -> ignore (write fdout buffer 0 n); copy () in copy ();;

本当の問題はここからです。

1 open Sys;; 2 open Unix;; 3 4 let client () = 5 if Array.length Sys.argv < 3 then begin 6 prerr_endline "Usage: client <host> <port>"; 7 exit 2; 8 end; 9 let server_name = Sys.argv.(1) 10 and port_number = int_of_string Sys.argv.(2) in 11 let server_addr = 12 try (gethostbyname server_name).h_addr_list.(0) 13 with Not_found -> 14 prerr_endline (server_name ^ ": Host not found"); 15 exit 2 in 16 let sock = socket PF_INET SOCK_STREAM 0 in 17 connect sock (ADDR_INET(server_addr, port_number)); 18 match fork () with 19 | 0 -> 20 Misc.retransmit stdin sock; 21 shutdown sock SHUTDOWN_SEND; 22 exit 0 23 | _ -> 24 Misc.retransmit sock stdout; 25 close stdout; 26 wait ();; 27 28 handle_unix_error client ();;

接続するインターネットアドレスを決めることから処理が始まります。アドレスはホストネームまたは数字の形を使うことができます。gethostbyname がどちらの場合にも対応しているためです。それからインターネットドメインでストリームタイプのソケットをデフォルトプロトコルで作成し、決定したアドレスのマシンに接続します。

プロセスは fork を使ってクローンします。子プロセスは標準入力からのデータをソケットにコピーします。標準入力の終端に達すると、子プロセスは送信方向の接続を閉じて終了します。親プロセスはソケットから呼んだデータを標準出力にコピーします。ファイルの終端に達すると標準出力を閉じ、子プロセスの終了を待ってから終了します。

このプログラムにおいて、接続が切れる原因は以下の三つです:

6.7  サービスの確立

前の節でクライアントがサーバに接続する方法を見たので、今度はサーバーがクライアントにサービスを提供する方法について見ていきます。まずソケットに特定のアドレスを関連付けネットワークから到達可能にします。システムコール bind がこれを行います。

val bind : file_descr -> sockaddr -> unit

第一引数はソケットのディスクリプタで、第二引数はバインドするアドレスです。インターネットアドレスとして定数 inet_addr_any を使うと、マシンが持つすべてのインターネットアドレス (複数のサブネットワーク内におけるアドレス) をバインドすることができます。

システムコール listen を使ってソケットが接続を受け入れられる状態にします:

val listen : file_descr -> int -> unit

第一引数はソケットのディスクリプタで、第二引数はサーバーがビジー状態のときに保持できるリクエストの数です(数十から大きなサーバーでは数百程度になります) 。接続を待っているクライアントが第二引数で指定した値よりも大きくなった場合、それ以降の接続リクエストは失敗します。

最後に、ソケットのディスクリプタに対するへの接続リクエストはシステムコール accept を通じて受け入れられます。

val accept : file_descr -> file_descr * sockaddr

accept の呼び出しが返った場合、引数として与えられたソケットは影響を受けないので続けて接続リクエストを受け入れることができます。返り値の第一要素はクライアントに接続した新しいディスクリプタです。サーバがこのディスクリプタに書き込んだデータは、クライアントが connect に渡したディスクリプタから読み込むことができます。またクライアントが connect に渡したディスクリプタに書き込んだデータは、accept の返り値の第一要素のディスクリプタから読み込むことができます。

accept の返り値の第一要素はクライアントのアドレスです。接続したクライアントが接続する権限を持っているかを確認したり (例えば x サーバはこの処理を行います。 xhost で新しい許可されたユーザを追加できます)、サーバーからクライアントへの二つめの接続を作成する (ftp はすべてのファイル転送リクエストに対してこの処理を行います) ことに使うことができます。

tcp サーバの一般的な構造は以下のようになっています:

let install_tcp_server_socket addr = let s = socket PF_INET SOCK_STREAM 0 in try bind s addr; listen s 10; s with z -> close s; raise z;;

ライブラリ関数 Misc.install_tcp_server addrbindlisten を使ってストリームタイプでインターネットドメインのソケットをデフォルトプロトコルで作成し、アドレス addr からの接続を受けられるように準備します。この関数はライブラリ関数なので、エラーの場合にはソケットを閉じます。

let tcp_server treat_connection addr = ignore (signal sigpipe Signal_ignore); let server_sock = install_tcp_server_socket addr in while true do let client = restart_on_EINTR accept server_sock in treat_connection server_sock client done;;

ライブラリ関数 Misc.tcp_serverMisc.install_tcp_server でソケットを作成した後無限ループに入ります。ループでは accept を使って接続要求を待ち、受け入れた接続は treat_connection 関数を使って処理します。treat_connection はライブラリ関数なことから、accept が途中で終了した場合にはやり直します。また予期しない接続の切断が起こったときにサーバプログラムを終了させるのではなく、EPIPE 例外が treat_connection によって捕捉されるようにsigpipe シグナルを無効化しておきます。接続を閉じるときにディスクリプタ client を閉じるのは例外の有無にかかわらず treat_connection の責任です。

treat_connection 関数はアドレスの他にサーバのディスクリプタも受け取りますが、これは treat_connection 関数が forkdouble_fork をした場合でもサーバを閉じられるようにするためです。

以下のアプリケーション特有の service 関数を考えます。この service 関数はクライアントと通信を行ってから接続を閉じます。

let service (client_sock, client_addr) = (* ディスクリプタが client_sock のクライアントと処理を行う *) (* 処理が終わったならば: *) close client_sock;;

この場合サーバは接続を順番に処理することができます。次の Misc モジュールにあるライブラリ関数はこのパターンに対処します:

let sequential_treatment server service client = service client

sequential_treatment 関数を使った場合サーバがあるクライアントを処理している間は他の接続リクエストを処理することができないので、この関数が使えるのは service 関数が決まった短い時間で終わるサービスのときに限られます。

ほとんどのサーバプログラムは accept が返った直後に fork を呼ぶことでサービスの実行を子プロセスに移譲します。子プロセスが接続を処理し、親プロセスはもう一度 accept を行います。Misc モジュールの以下の関数がこの操作を行います。

let fork_treatment server service (client_sock, _ as client) = let treat () = match fork () with | 0 -> close server; service client; exit 0 | k -> () in try_finalize treat () close client_sock;;

親プロセスがディスクリプタ client_socket を閉じることが重要であることに注意してください。ここで閉じることを忘れると子プロセスが client_socket を閉じても接続を終了しなくなり、すぐに親プロセスのディスクリプタが枯渇します。このディスクリプタはフォークが失敗したときにも閉じられます。エラーが致命的でなければサーバの実行が続くためです。

同様に、子プロセスは接続リクエストを受け取った server ディスクリプタをすぐに閉じます。一つ目の理由はこのディスクリプタが必要がないことです。二つ目の理由は子プロセスが終了する前にサーバが新しい接続の受け付けを終了する場合があることです。ml+exit 0+ の呼び出しは子プロセスがサービスの実行を終えた後に終了するためおよびサーバループを実行し始めないことを確実にするためにあるので重要です。

今までのライブラリ関数では子プロセスがいずれゾンビプロセスになり回収する必要が生じるというのを無視してきました。この問題の解決方法は二つ考えられます。一つ目のシンプルな方法はダブルフォーク ( ?? ページ参照)を使って孫プロセスに接続を処理させる方法です。この方法では Misc モジュールの以下のライブラリ関数を使います:

let double_fork_treatment server service (client_descr, _ as client) = let treat () = match fork () with | 0 -> if fork () <> 0 then exit 0; close server; service client; exit 0 | k -> ignore (restart_on_EINTR (waitpid []) k) in try_finalize treat () close client_descr;;

しかしこの方法でサーバプロセスが孫プロセスを管理する方法が一切無くなってしまいます。サービスを終了するときにサーバプロセスとサービスを実行している複数のプロセス全てが終了するように、サービスを同じプロセスグループで管理するのが望ましいです。この理由からサーバはフォークを行いながらも子プロセスを回収できるようにしておくことが多いです。例えば sigchld シグナルのハンドラを使うことができます (?? ページの Misc.free_children 関数を参照)。

6.8  ソケットの設定

ソケットには設定できる内部変数がたくさんあります。例えば転送バッファのサイズや転送の最小サイズ、接続を閉じるときの動作などです。

これらの変数は異なった型を持ちます。そのため OCamlには型の数だけgetsockopt 関数と setsockopt 関数があります。getsockopt 関数とその仲間の詳細なリストは OCamlのドキュメントを、getsockoptsetsockopt の厳密な意味は posix のリファレンスをそれぞれ参照してください。

この例で紹介する二つの変数はストリームタイプでインターネットドメインのソケットにしか適用することができません。

tcp プロトコルではソケットの切断にやり取りが必要なために少し時間がかかります。通常は close の呼び出しはすぐに返って切断に必要なやり取りはシステムが行います。次のコードを実行すると sock に対する close がブロックするようになります。この例では全てのデータが送られるか 5 秒が経過するまでブロックします。

setsockopt_optint sock SO_LINGER (Some 5);;

SO_REUSEADDR オプションを使うとローカルアドレスに対するソケットを閉じた直後に システムコール bind で再利用することが可能になります (前の接続のデータを受け取ってしまう可能性はあります)。このオプションを使うとサーバを停止してすぐに再起動できるのでテスト用途にとても便利です。

setsockopt sock SO_REUSEADDR;;
* * *

6.9  完全な例: ユニバーサルなサーバ

次の server コマンドを作成します:

./server port cmd arg1 ... argn

このコマンドはポート port からの接続リクエストを受け取り、それぞれの接続について cmd を引数 arg1 ... argn として、標準入出力をソケットの接続として実行します。例えば以下のコマンド:

./server 8500 grep foo

をマシン pomerol で実行し、別のマシンでユニバーサルクライアント (6.6 ページ参照) を以下のように実行します:

./client pomerol 8500 < /etc/passwd

クライアントは以下のコマンドを実行したときと同じ出力を表示します:

grep foo < /etc/passwd

grep がローカルのマシンではなく pomerol で実行される点が異なります。

server コマンドは多くのサーバが行うコードをまとめていて、特定のサービスやコミニケーションプロトコルの実装は起動されるプログラム cmd に任されています。この意味で service コマンドは ユニバーサルな サーバです。

1 open Sys;; 2 open Unix;; 3 4 let server () = 5 if Array.length Sys.argv < 2 then begin 6 prerr_endline "Usage: client <port> <command> [arg1 ... argn]"; 7 exit 2; 8 end; 9 let port = int_of_string Sys.argv.(1) in 10 let args = Array.sub Sys.argv 2 (Array.length Sys.argv - 2) in 11 let host = (gethostbyname(gethostname ())).h_addr_list.(0) in 12 let addr = ADDR_INET (host, port) in 13 let treat sock (client_sock, client_addr as client) = 14 (* ログを出力 *) 15 begin match client_addr with 16 | ADDR_INET(caller, _) -> 17 prerr_endline ("Connection from " ^ string_of_inet_addr caller); 18 | ADDR_UNIX _ -> 19 prerr_endline "Connection from the Unix domain (???)"; 20 end; 21 (* 接続を処理 *) 22 let service (s, _) = 23 dup2 s stdin; dup2 s stdout; dup2 s stderr; close s; 24 execvp args.(0) args 25 in 26 Misc.double_fork_treatment sock service client in 27 Misc.tcp_server treat addr;; 28 29 handle_unix_error server ();;

tcp_server に渡されるアドレスにはプログラムを実行しているマシンのインターネットアドレスが含まれます。このアドレスを取得するには普通 gethostname を使います(11 行目)。しかし一般的には複数のアドレスが一つのマシンを指していることがあります。例えばマシン pauillac のアドレスが 128.93.11.35 だったとしても、ローカルならば (すでに pauillac の中にいるならば)このマシンには 127.0.0.1 を使ってアクセスすることができます。定数インターネットアドレス inet_addr_any を使えば、マシンを指す全てのアドレスにサービスを行うことができます。

サービスは “ダブルフォーク” で処理されます。孫プロセスによって実行される service は標準入力と標準出力、そして標準エラー出力をソケットにリダイレクトしてからコマンドを実行します。サービスの処理をブロックしながら順番に行うことはできないことに注意してください。

クライアントからの接続は server の親プロセスからの介入なしに閉じられます。以下の三つのケースが考えられます。

注意点

サーバを書くのはクライアントを書くよりも労力が要ります。クライアントは接続するサーバのことを普通知っているのに対し、サーバはクライアントのことを何も知らないからです。とくにサーバがパブリックな場合、クライアントは “敵” である可能性があります。そのため例外的なケースに対する防御をしなければいけません。

典型的な攻撃は接続を開けた後リクエストを送ることなく開いたままにするというものです。接続を受け入れた後サーバはクライアントが接続している間ずっとブロックします。攻撃者は何もしない接続を大量に開くことでサービスを飽和させることができます。そのためサーバは同時接続数を制限してリソースを枯渇を防ぐと共に、長い間何もしていない接続を終了させる必要があります。

サーバがフォークを行わず、接続を来た順番に処理する場合、このブロッキングの問題が生じることになります。処理している接続は何もしていないにもかかわらず他の接続に応答することができなくなります。順番に処理するサーバに対する解決法として通信の多重化がありますが、これは実装するのが難しくなります。並列サーバはよりエレガントですが、それでもタイムアウトは必要になります。タイムアウトには例えばアラームを使うことができます(4.2 節参照)。

6.10  コネクションレス型通信

SOCK_STREAM タイプの接続のほとんどで使われる tcp プロトコルではコネクション型通信だけを使うことができます。逆に、SOCK_DGRAM タイプの接続のほとんどで使われる udp プロトコルでは常にコネクションレス型通信が使われ、二つのマシンの間には確立された接続がありません。このタイプのソケットではデータの転送はシステムコール recvfromsendto を使って行われます。

val recvfrom : file_descr -> string -> int -> int -> msg_flag list -> int * sockaddr val sendto : file_descr -> string -> int -> int -> msg_flag list -> sockaddr -> int

転送されたデータのサイズを返すという点で、インタフェースは readwrite に似ています。recvfrom は読み込んだバイト数の他にデータを送っているマシンのアドレスも返します。

SOCK_DGRAM タイプのソケットに connect を呼ぶと疑似接続が取得できます。この疑似接続は本当は存在せず、 connect の引数に渡されたアドレスがソケットによって記憶され、データの送受信で使われるようになるというだけです (他のアドレスからのデータは無視されます)。connect を複数回読んでアドレスを変更したり、0 などの無効なアドレスを渡して疑似接続を切断することは可能です。反対にストリームタイプのソケットにこのようなことを行うとエラーが出ます。

6.11  低レベル入出力

システムコール recvsend はそれぞれ readwrite に対応しますが、ソケットのディスクリプタ専用です。

val recv : file_descr -> bytes -> int -> int -> msg_flag list -> int val send : file_descr -> bytes -> int -> int -> msg_flag list -> int

recvsend のインタフェースは readwrite に似ていますが、msg_flag 型のフラグのリストが引数に追加されています。この型の値と意味を次に示します:

MSG_OOBアウトオブバンドデータを処理する。
MSG_DONTROUTEデフォルトのルーティングテーブルを使用しない。
MSG_PEEKデータを読むことなく確認だけ行う。

これらのプリミティブ (原始的な関数) はコネクション型通信通信でも readwrite の代わりに利用できるほか、疑似通信モードでも recvfromsendto の代わりに利用できます。

6.12  高レベルプリミティブ

ユニバーサルクライアント/サーバの例はよく利用されるので、Unix モジュールにはネットワークサービスを確立したり利用するための高レベルな関数があります。

val open_connection : sockaddr -> in_channel * out_channel val shutdown_connection : Pervasives.in_channel -> unit

open_connection は引数のアドレスに対する接続を開き、そのソケットに対する入出力用の Pervasives チャンネルの組を作成します。返り値のチャンネルに対する入出力はサーバとの通信となりますが、チャンネルはバッファされるために、リクエストを本当に送られたことを保証するにはチャンネルをフラッシュする必要があります。クライアントはチャンネルを閉じる (この操作はソケットも閉じます) ことで任意のタイミングで接続を閉じることができるほか、 shutdown_connection を使って “きれいに” 接続を閉じることもできます。サーバが接続を閉じた場合、クライアントは入力チャンネルから EOF を受け取ります。

establish_server 関数でサービスを確立することができます。

val establish_server : (in_channel -> out_channel -> unit) -> sockaddr -> unit

establish_server f addr はアドレス addr にサービスを確立し、リクエストを関数 f で処理します。コネクションがあるたびにサーバは新しいソケットを作成してフォークします。子プロセスはクライアントと通信に利用する入出力用の Pervasives チャンネルを作成し fに渡します。f が返ると子プロセスはソケットを閉じて終了します。クライアントが接続をきれいに閉じたならば、子プロセスは入力チャンネルに EOF を受け取ります。そうでなくクライアントが接続を突然閉じた場合には f が書き込みを行おうとしたときに sigpipe を受け取ります。いずれの場合でもこのとき親プロセスは他のリクエストを処理しているはずです。establish_server はエラー (サービスの確立時の OCamlランタイムやシステムのエラーなど) の場合を除いて返りません。

6.13  プロトコルの例

クライアントとサーバの間でやり取りされるデータは単純なプロトコル (rsh, rlogin, …) ではクライアントからサーバへ、およびサーバからクライアントへの二つのバイト列として自然に表現できます。その他のプロトコルではやり取りされるデータはもっと複雑であり、バイト列からデータへ、あるいはデータからバイト列へのデコード/エンコードが必要になります。クライアントとサーバはリクエストの形式と接続で交わされるレスポンスについて取り決めた転送プロトコルについて合意しておく必要があります。Unix コマンドが利用するプロトコルの多くは “rfc” (request for comments) と呼ばれるドキュメントに記載されています。これらのドキュメントは議論のための提案として始まりましたが、時が経ちユーザがこのプロトコルに慣れるにしたがって標準となりました2

“バイナリ” プロトコル

バイナリプロトコルの多くはデータをメモリ内の表現にできるだけ近いコンパクトな形式で転送します。エンコード/デコードに必要な処理を最小化しネットワーク帯域を節約するためです。このタイプのプロトコルの典型的な例は x サーバと x アプリケーションの間の通信で利用される x-window プロトコルと nfs プロトコル (rfc 1094) です。

バイナリプロトコルはデータを次のようにエンコードすることが多いです。整数と浮動小数点数には同じ 1, 2, 4, あるいは 8 バイトのバイナリ表現が使われます。文字列には長さを示す整数とその後に続く内容で表現されます。構造化されたオブジェクト (タプル、レコード) はフィールドを順番通りに並べて表現されます。可変長のオブジェクト (配列、リスト) は長さを表す整数とそれに続く要素で表現されます。転送されるデータの型が正確に分かっているならば、受け取ったプロセスは簡単にメモリ上に復元できます。そうでなく異なるタイプのデータがソケットでやり取りされた場合、データのエンコードはタイプを表すデータ先頭の整数を読み取ってから行われます。

x ライブラリの XFillPolygon 関数は 多角形を塗りつぶす関数であり、以下の形のメッセージを送信します:

* * *

バイナリプロトコルでは接続するマシンのアーキテクチャに注意する必要があります。例えば複数のバイトからなる整数は ビッグエンディアン のマシンでは最上位バイトを先に (メモリの若いアドレスに) 格納しますが、 リトルエンディアンのマシンでは最下位バイトが先になります。16 ビット整数 12345 = 48 × 256 + 57はビッグエンディアンのマシンではアドレスが n の場所に 48 が、 n+1 の場所に 57 が格納されますが、リトルエンディアンのマシンではアドレスが n の場所に 57 が、 n+1 の場所に 48 が格納されます。このためプロトコルは複数のバイトからなる整数を転送する場合にどちらの方法を使うのかを指定する必要があります。あるいは転送されるメッセージのヘッダにどちらの方法を使うか書くことでどちらも使えるようにすることもできます。

OCamlシステムにはデータ構造をエンコード/デコードする (文献によっては マーシャリング とか シリアライゼーション とか ピックリング などと呼ばれる処理です) ための関数が二つあります。これらの関数を使うと OCamlの値とバイト列の間の変換を行うことができます。

val output_value : out_channel -> 'a -> unit val input_value : in_channel -> 'a

output_valuesinput_values は値をディスクに保存してあとで読み出せるようにするための関数ですが、任意の値をパイプやソケットへ送ることにも利用できます。関数を除く全ての OCamlの値を処理でき、値の間の共有や循環を保存し、エンディアンの異なるマシン間でも利用できます。これ以上の情報は Marshal モジュールで確認できます。

セマンティクス上は input_value の型が間違っていることを指摘しておきます。全ての型 'a に対して input_value の返り値の型が'a になることはないので、この関数の型は一般的すぎます。input_value の返り値は明確な型であり、全ての可能な型ではありませんが、これをコンパイル時にチェックすることはできません。返り値の型は実行時に入力チャンネルから読み込むデータに依存しているからです。input_value に正しく型検査を行うにはダイナミックオブジェクトと呼ばれる ML 言語の拡張が必要になります。ダイナミックオブジェクトでは値が型と組にされ、実行時の型検査が可能になります。詳細は [15] を参照してください。

x-ウィンドウプロトコルが OCamlで書かれていたならば、サーバへのクエストのためのヴァリアント型 request とサーバからのレスポンスのためのヴァリアント型 reply が定義されていたでしょう。

type request = | FillPolyReq of (int * int) array * drawable * graphic_context * poly_shape * coord_mode | GetAtomNameReq of atom | ... and reply = | GetAtomNameReply of string | ...

サーバの主な処理はリクエストをデコードして返事を書くループとなるはずです:

(* 接続リクエストをディスクリプタ s に受け取る *) let requests = in_channel_of_descr s and replies = out_channel_of_descr s in try while true do match input_value requests with ... | FillPoly(vertices, drawable, gc, shape, mode) -> fill_poly vertices drawable gc shape mode | GetAtomNameReq atom -> output_value replies (GetAtomNameReply(get_atom_name atom)) | ... done with End_of_file -> (* 接続の終了 *)

アプリケーションとリンクされる x ライブラリの関数は次のような構造になるでしょう:

(* まずサーバとの接続を確立してディスクリプタを s とする *) ... let requests = out_channel_of_descr s and replies = in_channel_of_descr s;; let fill_poly vertices drawable gc shape mode = output_value requests (FillPolyReq(vertices, drawable, gc, shape, mode));; let get_atom_name atom = output_value requests (GetAtomNameReq atom); match input_value replies with | GetAtomNameReply name -> name | _ -> fatal_protocol_error "get_atom_name";;
* * *
遠隔手続き呼び出し

バイナリプロトコルのもう一つの使用例は遠隔手続き呼び出しです。遠隔手続き呼び出しとは、マシン A のユーザがマシン B で関数を f を実行するというものです。関数を直接呼び出すことは当然できません。ですが、マシン B に接続して関数を実行し、結果をマシン A に送り返すという作業を呼び出しごとにプログラムすることは可能です。

Figure 4 — 遠隔手続き呼び出し

遠隔手続き呼び出しは一般的な状況であるために、これを行う rpc サービスが存在します (図 4)。rpc サーバはマシン A と B の両方で実行され、以下のように動作します。まずマシン A 上のユーザがマシン A 上の rpc サーバに遠隔地のマシン B 上における関数の実行をリクエストします。次にマシン A 上の rpc サーバはリクエストをマシン B 上の rpc サーバにリレーし、マシン B は f を実行して結果をマシン A 上の rpc サーバに送り返します。最後にマシン A 上の rpc サーバが結果をユーザに渡して終わりです。

マシン B 上で実行される他の関数に対する遠隔手続き呼び出しが、マシン A 上の同じ rpc サーバで処理されることになります。マシン A と B にインストールされた rpc サービスの間で接続処理は共有され、ユーザからはこれらの遠隔手続き呼び出しの処理が単純な関数呼び出しに見えます (図の破線)。

“テキスト” プロトコル

プロトコルの効率が重要でないネットワークサービスでは “テキスト” プロトコルがよく利用されます。 “テキスト” プロトコルは実際のところ小さなコマンド言語です。リクエストは複数のコマンドであり、最初の語がリクエストのタイプを、残りの語がコマンドの引数を表します。応答も一つ以上のテキストからなり、大抵は応答の種類を表す数字コードから始まります。“テキスト” プロトコルの例をいくつか示します:

名前説明目的
smtp (Simple Mail Transfer Protocol)rfc 821 電子メール
ftp (File Transfer Protocol)rfc 959 ファイル転送
nntp (Network News Transfer Protocol)rfc 977 ニュースの閲覧
http/1.0 (HyperText Transfer Protocol)rfc 1945 ウェブの巡回
http/1.1 (HyperText Transfer Protocol)rfc 2068 ウェブの巡回

これらのプロトコルの大きな利点はクライアントとサーバ間のやり取りが人間にも読めることです。例えば telnet コマンドを使ってサーバと直接対話することができます。host を接続するホストの名前、 service をサーバで実行されたサービスの名前 (httpsmtpnntp など) として telnet host service を起動すると、クライアントとしてのリクエストを打ち込ことができ、それに対するサーバからの応答は標準出力に出力されます。これによってプロトコルは理解しやすくなりますが、コーディングとリクエストと応答のデコードはバイナリプロトコルよりも複雑になり、メッセージはより大きく、したがって非効率になる傾向にあります。

シェルから smtp サーバにメールを送ったときの対話ログの例を示します。>> から始まる行はクライアントからサーバへ向かうテキストであり、<< から始まる行はサーバからクライアントへ向かうテキストです。

telnet margaux smtp Trying 128.93.8.2 ... Connected to margaux.inria.fr. Escape character is '^]'. << 220 margaux.inria.fr Sendmail 5.64+/AFUU-3 ready at Wed, 15 Apr 92 17:40:59 >> HELO pomerol.inria.fr << 250 Hello pomerol.inria.fr, pleased to meet you >> MAIL From:<god@heavens.sky.com> << 250 <god@heavens.sky.com>... Sender ok >> RCPT To:<xleroy@margaux.inria.fr> << 250 <xleroy@margaux.inria.fr>... Recipient ok >> DATA << 354 Enter mail, end with "." on a line by itself >> From: god@heavens.sky.com (Himself) >> To: xleroy@margaux.inria.fr >> Subject: Hello! >> >> Is everything ok down there? >> . << 250 Ok >> QUIT << 221 margaux.inria.fr closing connection Connection closed by foreign host.

コマンド HELOMAIL そして RCPT はそれぞれクライアントのマシン名、送信者のアドレス、受信者のアドレスをサーバに送信します。DATA コマンドは電子メールの本文を送信してよいかをサーバに訪ねます。その後にメールの本文を入力し、 '.' だけからなる行で終了します(この文字だけからなる行をメール本文に含めたい場合には、'.' を二つ書いておけばサーバは一つ目のピリオドを読み飛ばします) 。

サーバからの応答は全てコメントのついた 3 桁の数字コードです。5xx の形をした応答はエラーを表し、 2xx は全てが上手くいっていることを表します。クライアントが実際のプログラムであれば応答コードだけを解釈します。コメントはメールシステムの開発者を助けるために存在します。

* * *

6.14  完全な例: http リクエスト

http(HyperText Transfer Protocol) プロトコルは有名な “ワールドワイドウェブ” でドキュメントを読むのに主に使われます。この分野はクライアント-サーバの例のニッチな領域です。ページを読み込むクライアントとページを書き込むサーバの間には無数の中継リレーが存在し、それらは実際のクライアントに対して仮想サーバとして振る舞ったり、物理サーバに対して移譲されたクライアントとして振る舞ったりします。これらのリレーではキャッシングやフィルタリングなどのサービスがよく実行されます。

httpプロトコルにはいくつかのバージョンがあります。本質的な部分、つまりクライアントとリレーのアーキテクチャ、に集中するために、一番最初のバージョンの httpプロトコルを改変したシンプルなプロトコルを使います。このプロトコルはホコリをかぶっていますが、それでもほとんどのサーバによって理解されます。節の最後にはウェブを探索する本物のツールを作るために必要となる、より現代的でより複雑なバージョンのプロトコルを示します。ただし例をこの新しいバージョンに書き直す部分は練習問題として残しておきます。

httpプロトコルのバージョン 1.0 は rfc 1945 で規定され、以下の形のリクエストを定義しています:

GET sp uri crlf

ここで sp はスペースを、 crlf は文字列"\r\n" (“リターン” と改行) を表します。この単純なリクエストに対する応答もシンプルであり、応答はヘッダの何もない urlの要素そのものです。

リクエストの終端は EOF によって合図され、そこで接続が閉じられます。バージョン 0.9 のプロトコルから受け継いだこの形のリクエストは一つの接続に対するリクエストを一つだけに制限します。

url の取得

引数 urlを一つ取り、それが表すウェブ上のリソースを取得して表示するプログラム geturl を作ります。

最初のタスクは urlをパースしてプロトコルの名前 (ここでは "http" でなくてはいけません)、サーバのアドレス、ポート番号 (無くても良い)、そしてサーバ上のドキュメントの絶対パスを取り出すことです。この処理には OCamlの正規表現ライブラリ Str を使います。

open Unix;; exception Error of string let error err mes = raise (Error (err ^ ": " ^ mes));; let handle_error f x = try f x with Error err -> prerr_endline err; exit 2 let default_port = "80";; type regexp = { regexp : Str.regexp; fields : (int * string option) list; } let regexp_match r string = let get (pos, default) = try Str.matched_group pos string with Not_found -> match default with Some s -> s | _ -> raise Not_found in try if Str.string_match r.regexp string 0 then Some (List.map get r.fields) else None with Not_found -> None;; let host_regexp = { regexp = Str.regexp "\\([^/:]*\\)\\(:\\([0-9]+\\)\\)?"; fields = [ 1, None; 3, Some default_port; ] };; let url_regexp = { regexp = Str.regexp "http://\\([^/:]*\\(:[0-9]+\\)?\\)\\(/.*\\)"; fields = [ 1, None; 3, None ] };; let parse_host host = match regexp_match host_regexp host with | Some (host :: port :: _) -> host, int_of_string port | _ -> error host "Ill formed host";; let parse_url url = match regexp_match url_regexp url with | Some (host :: path :: _) -> parse_host host, path | _ -> error url "Ill formed url";;

次の関数が示すように、シンプルなリクエストの送信は簡単な処理です。

let send_get url sock = let s = Printf.sprintf "GET %s\r\n" url in ignore (write sock s 0 (String.length s));;

urlはサーバのアドレスとポート番号を含む完全なものであるか、そうでなければアドレスとサーバ上のパスだけが含まれるものであることに注意してください。

応答はドキュメントの内容だけを含み追加の情報を含まないので、応答を読み込むのはより簡単です。リクエストにエラーがあった場合、サーバからはエラーメッセージがhtml ドキュメントとして返ります。このことから、応答がエラーかどうかを確認することなく Misc.retransmit を使って出力すればすむことがわかります。プログラムの残りの部分ではサーバとの接続を確立します。

let get_url proxy url fdout = let (hostname, port), path = match proxy with | None -> parse_url url | Some host -> parse_host host, url in let hostaddr = try inet_addr_of_string hostname with Failure _ -> try (gethostbyname hostname).h_addr_list.(0) with Not_found -> error hostname "Host not found" in let sock = socket PF_INET SOCK_STREAM 0 in Misc.try_finalize begin function () -> connect sock (ADDR_INET (hostaddr, port)); send_get path sock; Misc.retransmit sock fdout end () close sock;;

いつもどおり、コマンドライン引数のパースする処理を追加して完成です。

let geturl () = let len = Array.length Sys.argv in if len < 2 then error "Usage:" (Sys.argv.(0) ^ " [ proxy [:<port>] ] <url>") else let proxy, url = if len > 2 then Some Sys.argv.(1), Sys.argv.(2) else None, Sys.argv.(1) in get_url proxy url stdout;; handle_unix_error (handle_error geturl) ();;

http リレー

この節では httpリレー (あるいは プロキシ) プログラムを作成します。このプログラムはクライアントからの httpリクエストを他のサーバにリダイレクト(あるいはリレー) し、応答をクライアントに転送します。

リレーの役割を図 5 に示します。クライアントがリレーを使った場合、そのリクエストは世界中に存在する個々のhttpサーバではなくリレーサーバに送信します。リレーにはいくつもの利点があります。まず、リレーサーバは最後の、あるいは頻繁に送られる応答を記録してリモートサーバに問い合わせることなくリクエストに答えることができます。これによってネットワークの過負荷を避け、サーバがダウンしているときでも応答が得られるようになります。次に、広告や画像の削除といった応答のフィルターを行うことができます。また、ワールドワイドウェブ全体を一つのサーバを通して見るようにすることでプログラムの開発が容易になります。

Figure 5 — http リレー

コマンド proxy portport 番ポート (省略された場合は httpプロトコルのデフォルトのポート番号) にリレーサーバを作成するようなコマンド proxy を作成します。get_url 関数を作るときに使った関数のコードを Url モジュールに入れて再利用します。書くべきなのはリクエストの解析とサーバの立ち上げの部分だけです。

open Unix open Url let get_regexp = { regexp = Str.regexp "^[Gg][Ee][Tt][ \t]+\\(.*[^ \t]\\)[ \t]*\r"; fields = [ 1, None ] } let parse_request line = match regexp_match get_regexp line with | Some (url :: _) -> url | _ -> error line "Ill formed request"

サービスを確立には establish_server 関数を使うので、定義する必要があるのは接続リクエストを処理する関数だけです:

let proxy_service (client_sock, _) = let service () = try let in_chan = in_channel_of_descr client_sock in let line = input_line in_chan in let url = parse_request line in get_url None url client_sock with End_of_file -> error "Ill formed request" "End_of_file encountered" in Misc.try_finalize (handle_error service) () close client_sock

プログラムの残りの部分ではサービスを確立するだけです:

let proxy () = let http_port = if Array.length Sys.argv > 1 then try int_of_string Sys.argv.(1) with Failure _ -> error Sys.argv.(1) "Incorrect port" else try (getservbyname "http" "tcp").s_port with Not_found -> error "http" "Unknown service" in let treat_connection s = Misc.double_fork_treatment s proxy_service in let addr = ADDR_INET(inet_addr_any, http_port) in Misc.tcp_server treat_connection addr;; handle_unix_error (handle_error proxy) ();;

The http/1.1 プロトコル

単純な httpリクエストはリクエストごとに新しい接続を必要としますが、これは非効率です。なぜならリクエストには通常他のリクエストが続くからです(例えばクライアントが画像つきのウェブページを受け取った場合、クライアントは続けて画像をリクエストします)。さらに接続を確立する時間はリクエストを処理する時間を簡単に上回ります(7 章ではプロセスではなくスレッドでリクエストを処理することでこの時間を削減する方法を見ます)。rfc 2068 で規定される httpバージョン 1.1 では一つの接続で複数のリクエストを処理することが可能になる複雑なリクエストが利用されます3

http/1.1 で使われる複雑なリクエストではサーバは全ての応答の先頭に応答のフォーマットと転送されるドキュメントのサイズを表すヘッダを付けます。これによってサイズが既知となってドキュメントの終端は EOF ではなくなり、更に多くのリクエストを処理できるようになります。複雑なリクエストは以下の形をしています:

GET sp uri sp HTTP/1.1 crlf header crlf

header の部分はキーと値からなるフィールドのリストであり、以下の構文を持ちます:

field : value crlf

':' の周りに余分なスペースを置くことが許されており、スペースはタブまたは複数のスペースと取り替えることができます。ヘッダフィールドは複数行からなっていても構いません。このとき、およびこのときに限り、行末 crlf の次にスペース sp が続きます。最後に、大文字と小文字はフィールドのキーワードと特定のフィールドの値において無視されます。

フィールドが必須かどうかはリクエストの種類によります。例えば GET リクエストでは目的地となるマシンを示すフィールドが必須です。

Host : hostname crlf

この種類のリクエストでは必要に応じて If-Modified フィールドを指定することで、ドキュメントが指定した日時から改変されている場合に限ってドキュメントを返すようにすることができます。

If-Modified : date crlf

header 内のフィールドの数は前もって定まっていませんが、ヘッダの終わりは crlf だけからなる行で示されます。

以下は完全な例です (各行の最後の \r の次には \n が続きます):

GET /~remy/ HTTP/1.1\r Host:pauillac.inria.fr\r \r

複雑なリクエストに対する応答も複雑になります。応答はステータスを表す行、ヘッダ、そして(もしあれば)応答の本体からなります。

HTTP/1.1 sp status sp message crlf header crlf body

応答のヘッダのフィールドはリクエストのものと似た構文を持ちますが、必須、あるいは必須でないフィールドが異なっています (リクエストの種類と応答のステータスによって異なります — プロトコルのドキュメントを見てください)。

応答の body は空でなければ一つのブロックまたは複数のチャンクで送られます。

一つのブロックからなる応答の例を示します:

HTTP/1.1 200 OK\r Date: Sun, 10 Nov 2002 09:14:09 GMT\r Server: Apache/1.2.6\r Last-Modified: Mon, 21 Oct 2002 13:06:21 GMT\r ETag: "359-e0d-3db3fbcd"\r Content-Length: 3597\r Accept-Ranges: bytes\r Content-Type: text/html\r \r <html> ... </html>

ステータス 200 はリクエストが成功したことを示します。301urlが応答の Location フィールドで示される 他の urlにリダイレクトされたことを示します。4XX の形のステータスはクライアントにおけるエラーを、 5XX はサーバにおけるエラーを示します。

練習問題 15

http/1.1 プロトコルで動くリレーを書いてください。

* * *
練習問題 16

リレーにキャッシュを追加してください。ページはハードドライブに保存され、リクエストされたページがキャッシュに存在していてキャッシュが古すぎない場合にはそれを返します。そうでない場合にはサーバは問い合わせを行いキャッシュを更新します。

* * *
練習問題 17

次の wget コマンドを作ってください。wget u1 u2 ... unu1, u2, …, un へのリクエストを送り、応答を ./m1/p1, ./m2/p2, …, ./mn/pn に保存します。ここで mipi はそれぞれサーバの名前とリクエスト ui の絶対パスを表します。連続するリクエストが同じマシン m に対するものである場合、プロトコルの利点を活かしてコマンドが開く接続はひとつだけにするべきです。パーマネント urlによるリダイレクトは追うようにしてください。以下のオプションを追加することもできるでしょう:

-Nファイル ./mi/ui が存在しない場合、あるいは urlよりも古い場合にはダウンロードしない
-rhtml フォーマットの応答に含まれる urlを再帰的に全てダウンロードする。
* * *

1
インターネットは専用のリンクで接続されたローカルネットワーク (通常はイーサネットネットワーク) で構成され、世界中の何百万台のマシンが接続しています。インターネットドメインにおいては、同じイーサネットケーブルで接続されている隣のマシンとの通信と、いくつものルータと通信衛星を使って接続された地球の反対側にあるマシンとの通信にはなんの違いもありません。
2
rfc はたくさんのサイトから匿名 ftp で入手できます。フランスでは ftp.inria.fr のディレクトリ rfc から入手できます。リファレンスサイトは http://www.faqs.org/rfcs/ です。
3
http/1.0 プロトコルでも複雑なリクエストを使って複数のリクエストを一つの接続で処理することは可能ですが、ここではそれとは関係のない http/1.1 で使われるリクエストを説明します。

Previous Up Next