第8講:サーバ・クライアント間の通信を行う

その1 Javaプログラムにおける通信のしくみを理解する

 ここでは「通信の準備」と「通信の実行」の2段階に分けて、通信を行うプログラムの作成に不可欠な概念の解説と実際に使用するクラスの紹介、その利用方法の説明を行います。

(1)通信の準備

 プログラム同士が通信を行うというのは、たとえば片方のプログラムが送信した「こんにちは」という文字列が、もう片方のプログラムによって受け取られディスプレイに表示される、というような状況を指しています。基本となるのは複数のプログラム同士での「データの送受信」です。

 ここでは二つのプログラムがそれぞれ「サーバ」と「クライアント」として通信する場合を考えてみます。

 双方がデータのやりとりを行う場合、双方を結んで情報を運ぶための「線」が必要となります。Javaにおいてはこの「線」のことを「Socket(ソケット)」という概念で表します。「Socket」は逆方向の情報の流れ(Stream)をもつ二本の通信線を一本に束ねたものだと考えられます。仮にAとBという二つのプログラムが通信を行うとすれば、一方の通信線がAにとっての「InputStream」でありかつBにとっての「OutptStream」、もう一方の通信線はAにとっての「OutputSteam」かつBにとっての「InputStream」となるわけです。この2本の通信線を束ねる「Socket」を介して情報のやりとりが行われるのです。

プログラムA Socket プログラムB
InputStream
OutputStream
←←←←←←←←←←←
→→→→→→→→→→→
InputStream
OutputStream

 通信をはじめる一番最初の段階では、まだこの「Socket」は二つのプログラムを結んでいません。クライアント側からサーバ側に向かって「Socket」が送り込まれて、初めて通信が開通します。クライアントがサーバに「Socket」を送り込むことを、一般に「接続要求を送る」といいます。

 クライアントが「Socket」を送り込む際には、必ず「どのサーバコンピュータ」の「何番の通信口」に向かって送り込むのかを明らかにしなければなりません。その際の「どのサーバコンピュータ」のことを一般に「サーバ名」、「何番の通信口」のことを「ポート番号」、と呼びます。サーバには複数の「通信口=ポート」があり、番号で管理しています。サーバの側は、プログラムによって決まった番号のポートを監視しています。クライアントはサーバが監視するポート番号に「Socket」を送らなければなりません。

 無事にサーバに「Socket」が受け取られて、はじめて通信の準備が完了します。

 つぎに、これらの手順を実現するJavaのクラスに関する説明です。この「通信の準備」を行うに当たっては、「java.net.パッケージ」に用意されている次の二つのクラスを利用します。

ServerSocketクラス
 サーバの側に用いられます。インスタンス化の際に引数として「ポート番号(整数型)」を要求します。インスタンス化が済んだ時点でポートの監視が始まります。そのとき、もしもクライアントから接続要求(「Socketクラス(後ほど説明)」のインスタンスが送られてくる)があれば、「acceptメソッド」で受け取ります。この時点で通信の準備が完了します。
Socketクラス
 クライアントの側に用いられます。インスタンス化の際に引数として「サーバ名(文字列型)、ポート番号(整数型)」の順に二つの引数を要求します。インスタンス化が行われた時点で接続要求がサーバに送られます。

 この二つのクラスを用いて行われる通信の準備の状況を、時系列的に図式化すると次のようになります。図は右に向かって時間が進行してゆくものとしています。

       
------------- 時間の進行------------->

サーバ側
ServerSocketのインスタンス化、ポートを監視

待機
acceptメソッドを起動、Socketのインスタンス(接続要求)を受け取り同型の箱へ代入 接続完了

クライアント側
サーバのあとからプログラムを起動 Socketのインスタンス化、接続要求を送信

待機
接続完了

(2)通信の実行

 通信の準備が終わったら、次は実際にデータのやりとりを行います。データのやりとりはサーバ、クライアント双方を結ぶSocketを介して行います。先ほど説明したとおり、Socketには通信を行う双方のプログラムにとっての入力の流れ(InputStream)、出力の流れ(OutputStream)が用意されています。データを通信したい場合にはOutputStreamを、受信したい場合にはInputStreamを、それぞれ取得し、そこにデータを書き込む(送信)/そこからデータを読み込む(受信)ことで、通信が行われます。つまり、Socketが開通した後は、

  1. InputStream/OutputStreamの取得
  2. OutputStreamへのデータの書き込み → データの送信
  3. InputStreamからのデータの読み込み → データの受信
  4. 通信終了の合図を出す(通信の切断)

 というような命令を実行することで通信が実行されるわけです。

 それでは、それぞれを行うためにはどのようなクラスとメソッドを利用すればよいのでしょうか。この場合、送受信するデータの型によって利用すべきクラスが異なってきます。ここでは「時間割表示システム」の実現を想定しています。通信されるデータは「授業の情報」、すなわちClassTableのインスタンスです。そこで、「ある任意のクラスのインスタンスを自由に送受信できる」ことを前提に、利用するクラスを解説します。

OutputStream/InputStreamの取得
 OutputStream/InputStreamの取得を行うためには、Socketクラスのメソッドである「getOutputStream/getInputStream」メソッドを利用します。これらのメソッドが起動されると、返値として、取得したOutputStream/InputStreamのインスタンスが返されます。このインスタンスを引数にして、Streamを用いてデータの通信を行うクラスをインスタンス化することで、OutputStream/InputStreamが取得されデータ送受信の準備が完了します。
 通信の際にやりとりするデータの型が「任意のクラスのインスタンス」の場合、データ通信は「ObjectOutputSream/ObjectInputSreamクラス」を用いて行います。従ってgetOutputStream/getInputStreamメソッドの返値であるOutputStream/InputStreamインスタンスを引数として、これらのクラスのインスタンス化を行うことで、OutputStream/InputStreamが取得されデータ送受信の準備が完了します。
 ということで、結局以下の記述によってOutputStream/InputStreamの取得とデータ送受信の準備が完了することになります。

データ送信の準備(但しObjectOutputStreamのインスタンス名をoos、Socketのインスタンス名をsocketとする)

ObjectOutputStream oos =                       new  ObjectOutputStream(socket.getOutputStream());

データ受信の準備(但しObjectInputStreamのインスタンス名をois、Socketのインスタンス名をsocketとする)

ObjectInputStream ois =
   new ObjectInputStream(socket.getInputStream());
OutputStreamへのデータの書き込み(送信)
 OutputStreamへのデータの書き込みは、「ObjectOutputStream」の「writeObject」メソッドを利用して行います。気づいた人もいるかもしれませんが、このクラスとメソッドはファイル出力の際にも利用されています。つまり、「ObjectOutputStream」は。インスタンス化の際の引数次第でデータの出力先が変わるようになっているのです。しかし「送信するデータ(任意のクラスのインスタンス)をOutputStreamに書き込む」という「ObjectOutputStream」の機能自体は、全く変わりません。
 従って命令の記述は以下のようになります。

データの書き込み(送信)(但しObjectOutputStreamのインスタンス名をoosとする)

oos.writeObject(送信したいインスタンス名);
 但しネットワーク通信を行う際には、このあとでもう一つのメソッドを起動する必要があります。
oos.flush();
 ネットワーク通信の場合、送信されるデータは「writeObject」メソッドを起動した直後、すぐに送信されるわけではなく、一旦メモリー上に待機させられます。待機状態にあるデータを送り出すのがこの「flush」メソッドです。
InputStreamからのデータの読み込み(受信)
 InputStreamからのデータの読み込みも、ファイルからの読み込みと同様「ObjectInputStream」の「readObject」メソッドを利用します。読み込んだデータは代入によって受け取ります。その際には、必ず読み込むデータ(=任意のクラスのインスタンス)のもとのクラス名を明示する「キャスト」を行います。
 たとえばVectorのインスタンスを読み込む際には以下のように記述します。

データの読み込み(受信)(但し、ObjectInputStreamのインスタンス名をois、読み込むデータをVector型のインスタンスとする)

Vector vec = (Vector)ois.readObject();
 尚、一度ObjectOutputStream/ObjectInputStreamをインスタンス化してしまえば、その後Streamはずっと開通状態になります。従ってwriteObject/readObjectメソッドは、必要なときに何度でも利用することができます。
通信終了の合図(通信の切断)
 サーバーとクライアント間ですべての通信が終了したら、どちらかの側がもう片方に通信終了の合図を出す必要があります。どちらが合図を出すかは、そのシステムの仕様によって異なります。今回のシステムの場合、サーバがクライアントに対して「授業の情報」を送信することで、お互いのやりとりが完了します(このシステムの詳しい仕様については、次回に定義します)。そこで、「授業の情報」を送信した後に、サーバの側からクライアントに対して「終了の合図」を出させることにします。
 「終了の合図」を出すためには、送受信が終わったあと次のメソッドを実行します。

通信終了の合図(但し、ObjectOutputStreamのインスタンス名をoosとする)

oos.close();
 このメソッドをサーバが実行することで、サーバーの側から通信が切断されます。

 ということで、このつぎでは「通信の準備」「通信の実行」を用いて簡単なクライアント・サーバの通信を行うサンプルプログラムを作成します。


リンク:

<全体のトップページ>

     |

<第8講のトップページ>

     |-------------------------|

<第8講・その1>        <第8講・その2>