10-3: サーバ/クライアントモデルによる通信


・ 今度はさらにネットワークの下層のレベルを取り扱います。 TCP/IPプロトコルに基づく通信プログラムを作ってみましょう。 ここで紹介するのはごく単純な実験用のサンプルですが、 すべての通信プログラムの基本となる重要な考え方が登場します。 「サーバ」と「クライアント」という役割分担、 「ソケット」の概念について解説します。
 プログラム間の通信では、 あらかじめ通信の役割分担を決めておくと便利です。 これは特に通信を開始する時に重要になります。 最も広く用いられているのは次のような方法です。 まず通信プログラムのどちらか一方が先に起動されます。 これを「サーバ(server)」と呼びます。 サーバは常に通信を監視していて相手の接続を待ち受けます。 一方、他の通信プログラムはこのサーバに対して接続の要求を送ります。 こちらは「クライアント(client)」と呼ばれます。 クライアントの接続の要求をサーバが受け付けると、 その時点で通信が開始されます。
 このような関係を「サーバ/クライアントモデル」と呼び、 多くの通信アプリケーションはこのモデルに従って設計されています。 先に紹介したインターネット上の資源にアクセスするサンプルでも、 サンプルプログラム自身はクライアントであり、 通信相手として Webのサーバーの存在を仮定していました。 というわけで通信プログラムを作成する場合には、 サーバに相当するクラスとクライアントに相当するクラスの両方を設計する 必要があります。 また、 一般に1つのサーバに対して複数のクライアントが接続することができますが、 以下では簡単のために1対1の通信のサンプルを紹介します。

・ TCP/IPの通信プログラムを実現するためには、 さらに「ソケット(socket)」の概念の理解が必要です。 ソケットは直感的には通信ケーブルを接続するための「コンセントの口」を イメージすればいいでしょう。 もちろん現実のコンセントではありませんが、 それに似たものを通信プログラムに「取り付け」てしまおうというアイディアです。 この「コンセントの形」をある程度標準化しておけば、 いちいち「裸の線」をつなぐよりも、 ずっと手軽な操作でネットワークへの接続が実現されます。 逆にプログラムの側から見たソケットは、 ネットワークの出入口をハードウェアに依存しない形式で標準化したものになります。 ちょうどファイルや入出力デバイスと同じように ネットワークに対してストリームの入出力を行うことも可能になります。
 ソケットというアイディアは、 元々は BSD系の UNIXの通信プログラムのライブラリから始まったものです。 現在では多くのシステム向けにソケットライブラリが開発されています。 JDKではこの後実際に見ていくように java.netパッケージの中に Socket というクラスが提供されています。


TCP/IPによる通信

 また TCP/IP の通信では「ポート番号」と呼ばれる値を、 あらかじめ決めておく必要があります。 ポート番号には物理的な意味はありません。 通信の相手を特定するための番号です。 通信し合うプログラムが共通するポート番号を持つことで、 混信を防ぎ現実の回線を擬似的に多重化して利用することができます。 したがってポート番号はアプリケーションごとに固定されて用いられます。 たとえば Webのサーバーは 80 もしくは 8080 のポートを用いることになっています。 新しい通信プログラムが何番のポートを用いるかは自由ですが、 既存のアプリケーションのポート番号と衝突しないように注意が必要です。
 それでは、典型的なサーバー・クライアント間の通信プログラムのサンプルを 見てみましょう。 クライアント側を MessageClient.javaサーバ側を MessageServer.java という名前にします。
 クライアントが最初に行う仕事は通信の口となる Socket のオブジェクトの 生成です。 Socketのオブジェクトを生成するために必要な情報は、 「通信相手」と「ポート番号」の2つです。 ここでは、 通信相手のマシン名を実行時にコマンドライン引数から指定することにします。 また、ポート番号の値は他のアプリケーションが利用していない大きな値 (10001)を用います。
 ソケットのオブジェクトが得られれば後の処理は簡単です。 Socketクラスは、その入出力を行うための InputStream および OutputStream のクラスを内部に持っています。 それらを取り出して利用します。 ここでは複数行のテキストの情報をサーバから受け取ることを前提にしているので、 BufferedReaderのオブジェクトを生成して、 ネットワークからの入力処理を行っています。
 例外処理のパターンも通常の入出力処理とほぼ同じです。 サーバの指定が正しくない場合やソケットの生成に失敗した場合には、 UnknownHostException か SocketExceptionが発生します。
 では、次にサーバの側を見てみましょう。 サーバがネットワーク接続のために行う仕事は、 クライアントより少し複雑になります。 そのために、java.net には ServerSocket という名前のクラスが用意されています。 サーバは最初にこの ServerSocketのオブジェクトを生成します。 コンストラクタにはポート番号の情報が必要ですが、 この値は先ほどのクライアントのプログラムのものと一致するように指定します。 ServerSocket の accept()メソッドは、 指定されたポート番号の通信を監視する働きをします。 プログラムの処理はここでブロックされ、 クライアントが接続してくるまで待ち続けます。
 クライアントからの接続があると、 accept()メソッドは Socketクラスのオブジェクトを返値として返します。 サーバ側のプログラムは Socketのオブジェクトの生成を明示的には記述しません。 なぜなら、 サーバにはあらかじめ通信相手のマシン名が予測できないのが普通だからです。 クライアントが接続してきた時点で初めて通信相手が確定し、 そのための Socketが生成されるわけです。
 Socketのオブジェクトが得られた後の処理は、クライアントと同じです。 ここでは複数行のメッセージをクライアントに送信するために、 BufferedWriterのオブジェクトを生成しネットワークに出力処理を行います。 このサンプルではサーバーからクライアントに対して一方的な送信が行われていますが、 この逆ももちろん可能です。 一般には双方向の送受信が行われ、 この点ではサーバーとクライアントはまったく対等です。 また、このサンプルのサーバは クライアントへのメッセージの転送を終えるといったん接続を切り、 再び監視モードに戻って 次のクライアントからの接続を待つように設計されています。

・ さてサンプルプログラムを実行してみましょう。 通信を確認するためには2つのプログラムを別々の javaコマンドによって 起動する必要があります。 できれば LAN内に接続された2台以上のマシンがあることが望ましいのですが、 プログラムの動作を確認するだけならば1台のマシン上でも可能です。 その場合は 作業用の端末のウィンドウを複数開いておくと便利でしょう。 また、2つのプログラムは 当然のことながら両者を順序よく起動しなければなりません。 最初にサーバの方からです。 サーバが起動したことを確認した上で、 クライアントを起動します。 起動する時に、サーバが実行されているマシン名(IPアドレス)を コマンドライン引数として渡してください。
 ソケットを使った通信プログラムを Cなどで開発した経験がある方には、 java.net の Socket や ServerSocketの取り扱いは 驚くほど単純に感じられたことでしょう。 Cのソケットライブラリにはもっと多くのシステムコールが存在し、 それらを手順を踏んで呼び出す必要があります。 上のサンプルの中では、 ServerSocketの accept()メソッドが特別な役割をする程度です。 また Cのソケットライブラリは、 システムコールや構造体の名称や仕様がシステムによって微妙に異なります。 JDKの場合にはそうした違いは全く存在しません。 その秘密はどこにあるのでしょうか?
 Socketクラスはその内部に SocketImplというクラスのオブジェクトを内部に含んでいます。 ただし SocketImplは抽象クラスで、 実装を持つサブクラスがシステムごとに用意されています。 システムに依存する部分はこの SocketImplクラスの中に閉じこめられています。 この工夫のおかげで Socketクラス自身はシステムには依存しない形で記述され、 しかもシステムごとに異なるネットワークのコントロールを可能にします。


Socketクラスの実現の仕組み