Previous | Next | Trail Map | Custom Networking and Security | ソケットについて


ソケットのサーバ側を書く

このセクションでは、ソケットコネクションのサーバ側の書き方について、詳細なク ライアントサーバの例を用いて説明する。クライアントサーバの対におけるサーバは 「ノック」 ジョークの役目を果たす。ノックジョークは幼い子どもに好まれる言葉 遊びで、だじゃれを乗せて次のような感じで遊ぶ。

Server: "Knock knock!"
Client: "Who's there?"
Server: "Dexter."
Client: "Dexter who?"
Server: "Dexter halls with boughs of holly."
Client: "Groan."

この例は、独立して動作する 2 つの Java プログラムから成る。1 つはクライアン トプログラム、もう 1 つはサーバプログラムである。クライアントプログラムは KnockKnockClient という 1 つのクラスによって実装され、前のページの EchoTest の例を基礎としている。サーバプログ ラムは KnockKnockServer および KKState という 2 つのクラスによって実装され ている。 KnockKnockServer にはサーバプログラムの main() メソッドが含まれ ており、ポートを監視し、コネクションを確立し、ソケットを読み書きするという面 倒な作業のすべてはこのクラスで実行される。KKState はジョークの役目を果たす。 現在のジョークや現在の状態 (ノックが送られてきた、きっかけが送られてきたなど ) を把握し、現在の状態に応じてジョークのさまざまなテキスト片を送り出す。この ページでは、これら 2 つのプログラムの各クラスを詳細に調べた後、実行方法につ いて説明する。

ノックサーバ

このセクションではノックサーバプログラムを実装するコードを丹念に見ていく。 KnockKnockServer クラスの完全な ソースも参照できる。 サーバプログラムは、指定されたポートを待機するための新規の ServerSocket オブ ジェクトを作成するところから始まる。サーバを書く場合、他のサービスに専有され ていないポートを選択する必要がある。 KnockKnockServer はポート 4444 を監視しているが、これは 4 が私の好みの数であ ることと、ポート 4444 が私の環境でほかに使用されていないからである。

try {
    serverSocket = new ServerSocket(4444);
} catch (IOException e) {
    System.out.println("Could not listen on port: " + 4444 + ", " + e);
    System.exit(1);
}

ServerSocket は、クライアントサーバのソケットコネクションのサーバ側を、シス テムに依存せずに実装する java.net のクラスである。 ServerSocket のコンストラクタは、指定されたポートを何らかの理由 (ポートがす でに使用されているなど) で監視できない場合は例外をあげる。この場合、 KnockKnockServer は終了するしかない。

サーバが正常にそのポートと接続できた場合は、ServerSocket オブジェクトが正常 に作成され、サーバは次の手順へ進む。次の手順とはクライアントからのコネクショ ンを受け入れることである。

Socket clientSocket = null;
try {
    clientSocket = serverSocket.accept();
} catch (IOException e) {
    System.out.println("Accept failed: " + 4444 + ", " + e);
    System.exit(1);
}

accept() メソッドは、クライアントが起動され、サーバが監視してい るポート (この例ではポート 4444) でのコネクション要求が発せられるまでブロック (待機) する。accept() メソッドは、クライアントとのコネクションを正常に確立すると、新規の Socket オブジェクトを返し、このオブジェクトが新規のロ ーカルポートに結びつけられる。サーバは、当初監視していたポート とは異なるポート上で、この新規の Socket を通じてクライアントと通信することが できる。したがって、サーバは ServerSocket を通して元のポート上でクライアントのコネクション要求を引き続き監視することができる。このプログラム例のバージョン は新たなクライアントコネクション要求を監視することはないが、後に示すこのプログラムの変更バージョンではこれを行う。

その次の try ブロック内のコードは、クライアントとの通信における サーバ側を実装する。サーバのこのセクションはクライアント側 (この例は前のペー ジで紹介されている。また、KnockKnockClient クラスを後で詳しく説明するときに も出てくる) と非常によく似ている。

まず、初めの 6 行から説明する。

DataInputStream is = new DataInputStream(
                 new 
BufferedInputStream(clientSocket.getInputStream()));
PrintStream os = new PrintStream(
             new 
BufferedOutputStream(clientSocket.getOutputStream(), 1024), 
false);
String inputLine, outputLine;
KKState kks = new KKState();

コード片の最初の 2 行は accept() メソッドによって返されたソケッ トで入力ストリームを開き、 次の 2 行は同様に、同じソケットで出力ストリームを開いている。 その次の行は、ソケットとの読み書きに使用されるローカル文字列の対を宣言し、作 成しているだけである。最後の行は KKState オブジェクトを作成する。これは、現 在のジョーク、ジョーク内の現在の状態などを把握するオブジェクトである。 このオブジェクトはクライアントとサーバの双方が一致して通信に使用する言語、す なわちプロトコル を実装する。

以下に示すコードで、サーバがまず発信する。

outputLine = kks.processInput(null);
os.println(outputLine);
os.flush();

コードの最初の行では、サーバがクライアントに発信する最初の行を KKState オブ ジェクトから取り出している。この例では、サーバが最初に発信する内容は "Knock! Knock!" である。

その次の 2 行は、クライアントソケットに接続された出力ストリームに書き込みを 行い、その後、出力ストリームをフラッシュしている。このコードの並びは、クライ アントとサーバ間の対話を開始するものである。

コードの次のセクションは、クライアントとサーバでやり取りすべき情報が残ってい る間、ソケットの読み書きを通して、クライアントとサーバの間でメッセージを送っ たり受け取ったりするループである。サーバはすでに "Knock! Knock!" で対話を開 始しているで、今度はサーバはクライアントからの応答を待たなければならない。 したがって while ループは入力ストリームからの読み取りを繰り返す 。 readLine() メソッドは、クライアントが何かをその出力ストリーム ( これはサーバの入力ストリーム) に書き込んで応答するまで、待機する。クライアン トが応答すると、サーバはクライアントの応答を KKState オブジェクトに渡し、 KKState オブジェクトに適切な応答を要求する。 サーバは println()flush()を呼び出して、ソケッ トに接続された出力ストリームを介してクライアントへただちに応答を送信する。 KKState オブジェクトから作成されたサーバの応答が "Bye." の場合、これはクライ アントがもうジョークはいらないと発信したことを表し、ループは終了する。

while ((inputLine = is.readLine()) != null) {
    outputLine = kks.processInput(inputLine);
    os.println(outputLine);
    os.flush();
    if (outputLine.equals("Bye."))
        break;
}

KnockKnockServer クラスはきちんとしたサーバである。KnockKnockServer のこのセクション の最後の数行で、入力ストリームと出力ストリーム、クライアントソケット、サーバ ソケットのすべてを閉じ、クリーンアップを行っている。

os.close();
is.close();
clientSocket.close();
serverSocket.close();

ノックプロトコル

KKState クラスは、クライアントとサーバが 通信に使用するプロトコルを実装する。 このクラスは、クライアントとサーバの対話がどこまで進んでいるかを記録し、クライアントの発言に対するサーバの返事を返す。KKState オブジェクトはすべ てのジョークのテキストをもっており、サーバの発言に対してクライアントが適切な応答を与えるようにしている。サーバが "Knock! Knock!" と言ったときに、ク ライアントに "Dexter who?" と言わせたりはしないのである。

すべてのクライアントサーバの対は、対話するための何らかのプロトコルをもってい なければならない。そうしないと、相互にやり取りされるデータには意味がなくなっ てしまう。自前のクライアントとサーバが使用するプロトコルは、それらがタスクを 実行するのに必要な通信によってまったく異なる。

ノッククライアント

KnockKnockClient クラスは KnockKnockServer に向けて発信するクライアントプログラムを実装する。 KnockKnockClient は前のセクションのEchoTest プログラムを基礎としているので、読 者はある程度理解しているはずである。しかし、ともかくこのプログラムをよく調べ 、サーバで何が起きているかを念頭に置きながら、クライアントで何が起きているか を見てみよう。

クライアントプログラムを開始する時点では、サーバがすでに実行中で、クライアン トからのコネクション要求を待つポートを監視している必要がある。

kkSocket = new Socket("taranis", 4444);
os = new PrintStream(kkSocket.getOutputStream());
is = new DataInputStream(kkSocket.getInputStream());

したがって、クライアントプログラムが最初に行うことは、サーバが動作しているマ シン上の、サーバが待機しているポートでソケットを開くことである。 KnockKnockClient のプログラム例は KnockKnockServer が監視しているのと同じポ ート番号 4444 でソケットを開いている。KnockKnockClient はホスト名として taranisを使用している。これは当社のローカルネットワークにある ( 仮の) マシンの名前である。 このプログラムを読者のマシンで入力し実行する場合は、この名前を読者のネットワ ークにあるマシンの名前に変更する必要がある。これは、読者が KnockKnockServer を実行するマシンである。

それから、クライアントはそのソケットで入力ストリームと出力ストリームを開く。

その次にくるのは、クライアントとサーバの間の通信を実装するループである。サー バがまず発信するので、クライアントは当初は受信待ちでなければならない。これは ソケットに接続された入力ストリームを読み取ることによって行われる。サーバが発 信するとき、それが "Bye." であれば、クライアントはループを終了する。そうでな ければ、クライアントはテキストを標準出力に表示し、標準入力へ入力されたユーザ からの応答を読み取る。ユーザがキャリジリターンを入力した後、クライアントはソ ケットに接続されている出力ストリームを通してテキストをサーバに送る。

while ((fromServer = is.readLine()) != null) {
    System.out.println("Server: " + fromServer);
    if (fromServer.equals("Bye."))
        break;
    while ((c = System.in.read()) != '¥n') {
        buf.append((char)c);
    }
    System.out.println("Client: " + buf);
    os.println(buf.toString());
    os.flush();
    buf.setLength(0);
}

サーバがクライアントに別のジョークを聞きたいか尋ね、ユーザがノーと言い、サー バが "Bye." と発信したときに通信は終了する。

処理をきちんと行うため、クライアントはその入力および出力ストリームとソケットを閉じる。

os.close();
is.close();
kkSocket.close();

プログラムを実行する

先に開始するのはサーバプログラムでなければならない。まず、他の Java プログラ ムの場合と同じように Java インタープリタを使用してサーバプログラムを実行する 。 サーバを実行するマシンは、クライアントプログラムがソケットを作成するときに指 定するマシンであることに留意する。

次に、クライアントプログラムを実行する。クライアントはネットワーク上のどのマ シンで実行してもよい。サーバと同じマシンで実行させる必要はない。

クライアントの実行開始を急ぎすぎると、 サーバがそれ自身を初期化し、ポートを監視し始めるより前に、クライアントを始動 してしまうことがある。この場合は、クライアントを始動しようとしたときに次のよ うなエラーメッセージが表示される。

Exception:  java.net.SocketException: Connection refused
(例外:  java.net.SocketException:
コネクションは拒否されました)

この場合は、クライアントを再度始動してみる。

KnockKnockClient プログラムのソースコードでホスト名を変更し忘れた場合は、次 のようなエラーメッセージが表示される。

Trying to connect to unknown host: java.net.UnknownHostException: taranis
(接続先のホストがありません:
java.net.UnknownHostException: taranis)

この場合は、KnockKnockClient プログラムを変更し、読者のネットワークに合わせ た正しいホスト名を指定する。それからクライアントプログラムを再コンパイルし、 再度実行してみる。

1 番目のクライアントがサーバに接続している間に、別のクライアントを開始しよう とすると、2 番目のクライアントは滞ってしまう。複数のクライアントのサポートに ついては、次のセクションで説明する。

クライアントとサーバの間のコネクションを正常に確立できると、画面に次のように 表示される。

Server: Knock! Knock!

ここで、ユーザは次のように応答しなければならない。

Who's there?

クライアントはユーザが入力した内容をエコー表示し、そのテキストをサーバへ送信 する。サーバは多数のノックジョークのレパートリーの中から 1 つを選びその 1 行目を応答する。このときの画面は次のようになる (ユーザの入力するテキストは太字で 示す)。

Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip

ここで、次のように応答する。

Turnip who?"

ここでも、クライアントはユーザが入力した内容を表示し、そのテキストをサーバへ 送信する。サーバは "落ち" で応答する。このときの画面は次のようになる (読者の 入力するテキストは太字で示す)。

Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Turnip who?
Client: Turnip who?
Server: Turnip the heat, it's cold in here! Want another? (y/n)

別のジョークを聞きたいときは "y" を、そうでないときは "n" を入力する。"y" を 入力すると、サーバは "Knock! Knock!" を再開する。"n" を入力すると、サーバは "Bye." と応え、クライアントとサーバの両方が終了される。

どこかで入力ミスをすると、KKState オブジェクトがそれをキャッチし、サーバが次 のようなメッセージで応答し、ジョークを再開する。

Server: You're supposed to say "Who's there?"! Try again. Knock! 
Knock!

KKState オブジェクトはスペリングと句読文字については細かく調べるが、大文字と 小文字は区別しない。

複数のクライアントをサポートする

KnockKnockServer の例は、わかりやすくするため、1 つだけのコネクション要求を 監視し、処理するように作成されている。しかし、複数のクライアント要求が同じポ ート、つまり同じ ServerSocket に送られてくる場合もある。クライアントのコネク ション要求はそのポートの待ち行列に入れられるため、サーバはコネクションを順次 的に受け入れなければならない。 しかし、スレッドを用いて、1 つのスレッドがそれぞれのクライアントコネクション を処理するようにすれば、同時に複数のコネクション要求を処理することができる。

このようなサーバのロジックの基本フローは以下のとおりである。

while (true) {
    accept a connection ;
    create a thread to deal with the client ;
end while

スレッドは必要に応じてクライアントコネクションを読み書きする。

トライしてみよう: 同時に複数のクライアントをサービスできるように KnockKnockServer を変更してみ る。以下に解答例を示す。これは、 KKMultiServerKKMultiServerThread の 2 つのク ラスから成る。 KKMultiServer は ServerSocket でクライアントのコネクション要求を監視しながら ループし続ける。要求がくると、KKMultiServer はコネクションを受け入れ、コネクションを確立するための新規の KKMultiServerThread オブジェクトを作成し、 accept() から返されたソケットをスレッドオブジェクトに渡し、スレッド を開始する。その後、サーバはコネクション要求の監視を再開する。 KKMultiServerThread オブジェクトは、ソケットを読み書きすることによってクライ アントと通信する。新規のノックサーバを実行した後、続けて複数のクライアントを 実行してみる。

参照

java.net.ServerSocket


Previous | Next | Trail Map | Custom Networking and Security | ソケットについて