>> Home
  + Past histories
  + Links

>> download

>> documents
  + Archtecrure Guide
  + Configuration
  + Tutorial
  + API Guide
  + Seasar Service

>> samples
  + Naval Battle
  + Chat

>> BBS
  + geoboard
    (旧掲示板)

>> Mail To Amoi
Home > documents > Tutorial > Chapter 5
チュートリアル
第5章 マルチルームチャットSockletを作る
 この章では、複数のチャットルームを持つチャットシステムを作り上げます。 HelloWorld Sockletよりも少し踏み込んだ形でSockletを作り、Sockletへの理解を深めてください。
通信プロトコル
 この汎用SocketServerでは、接続用初期コマンド以降のクライアントとサーバ間のやり取りについては、特に既定されていません。初期コマンド発行後は、自由にCS間でやり取りを行なうことが出来ます。つまり、Sockletを制作する場合にはまず、どういった文字列の形式でやり取りするのか、どういった文字列を送るとサーバはどのように応答するのか、このようなことを決める必要があります。XML文字列を交換するのも良いでしょう。固定幅文字列をやり取りするのも、コーディングが楽かもしれません。
 そこでまずは、このクライアントとサーバ間のやり取りについての約束 - これを通信プロトコルと呼びます - を決めましょう。
クライアントとサーバ間のやり取りを箇条書きにする
 通信プロトコル、と言っても難しく考える必要はありません。まずは思いつくままに、クライアントとサーバ間でやり取りされる内容について、箇条書きにしてみましょう。クライアントからサーバへ送られるメッセージは"(C->S)"、逆にサーバからクライアントへ送られるメッセージは"(S->C)"とします。また、サーバから接続中の全クライアントへ送信したい場合は、"(S->CA)"とします。  簡単なチャットでしたら、これぐらいで十分でしょう。
接続用初期コマンドを使用する
 上で決めたやり取りの内、「ログイン」の部分については接続初期コマンドで対応できそうです。
 「接続用初期コマンド」とは、Flashクライアントがこの汎用SocketServerへと接続してくる時、まず真っ先にを送信する必要がある文字列のことです。接続用初期コマンドの役目は、これから接続するSockletを選択することですが、さらに補助データとして、ユーザー名、パスワード、初期化パラメータを送ることが出来ます。Sockletを制作する際は、このうち、どれを使用して、どれを使用しないかを決める必要があります。
 この章で作成するチャットシステムでは、チャットユーザの名前を指定するのに初期コマンドの補助データ「ユーザー名」を使用し、初期化パラメータでチャットルームを選択することにしましょう。
やり取りの方式を決める
 さて、クライアントとサーバ間でどのようなメッセージがやり取りされるかを書き出したら、次はやり取りの方式を決めましょう。どのような文字列で上記のようなメッセージを表現するかを決める必要があります。
 ここでは、クライアントからサーバへ送信するメッセージは固定長の文字列とし、サーバからクライアントへ送られるメッセージはXMLで表現することにします。FlashでのXMLの取り扱いは簡単ですが、Java側での取り扱いには少々テクニックを要するためです:-P

 それではまず、クライアントからサーバへ送る固定長文字列について考えます。
 まずは、クライアントから送られてくる文字列が確実に「コマンドである」ことを保障させるために、文字列の頭は必ず"@"としましょう。文字列の頭がこれ以外の文字の場合は、コマンドとは認識せず、特に処理もしないようにします。また、サーバ側の処理を簡単にするため、コマンドの形は

@command=option

とします。"@command"の部分は固定長とすると、さらに処理は簡単になるでしょう。

 次に、サーバからクライアントへ送るXMLの書式について考えます。ノード名は何でも良いのですが、ここでは<message>としましょう。サーバから送られてくるメッセージには、
  1. ログイン通知
  2. チャット発言
  3. ログアウト通知
の3種類がありますので、属性commandを使用して、どのメッセージなのかを区別できるようにしましょう。また、発言者やログインしてきたユーザを指定できるようにuser属性も付けてあげましょう。具体的には、以下のようになります。

<message command="2" user="あもい">こんにちは!</message>

これは、"あもい"さんが「こんにちは!」と言う発言をした時に、サーバから全クライアントに送られてくるメッセージですね。
具体的な文字列の中身を決める
 さて、ここまで決めたら今度は具体的なコマンドの中身を決めていきます。
 まずはクライアントからサーバへ送られてくるコマンドについて決めましょう。とは言うものの、ログインは接続用初期コマンドを使用するため、決めるべきコマンドは、チャット発言コマンドのみです。それでも一応、将来の拡張性を考えて「やり取りの方式を決める」で決めた通りのフォーマットを定義しましょう。
 そこで、発言コマンドは「@speak=発言内容」とします。これでC->Sのプロトコルはおしまい。

 次にサーバからクライアントへ送るコマンドについて考えます。前項でノードはmessage、属性はcommandとuserと決めましたので、command属性に設定する値のみを考えれば良いですね。これも前項の通り
  1. ログイン通知
  2. チャット発言
  3. ログアウト通知
で良いでしょう。つまり、<message command="1" .../>の場合はログイン通知になります。

 ここまでの流れを、簡単にシーケンス図にしてみます。"⇒"は複数のクライアントへの送信を表しています。

サーバ   クライアント   備考
   
xxx:あもい::room=普通チャット
      接続用初期コマンド
             
   
+Welcome
-Login failure
      ログイン成功時
ログイン失敗時
             
   
<message command="1" user="あもい" />
       
             
   
@speak=こんにちは!
      あもいさんが発言
             
   
<message command="2" user="あもい">
こんにちは!
</message>
       
    .
.
.
       
   
ソケット切断
       
             
   
<message command="3" user="あもい" />
       
Sockletクラスを作成する
 プロトコルが決まりましたら、Sockletのプログラミングを開始しましょう。まずはリスト5.3のソースをご覧ください。これは、第1章でご紹介したHelloWorldと全く同じ中身のものです。このソースを少しずつチャットシステムに変えていきましょう。

リスト5.3
 1: package jp.wda.gpss.samples.chap5;
 2: 
 3: import jp.wda.gpss.GeneralSocklet;
 4: import jp.wda.gpss.SocketProcessor;
 5: 
 6: public class SimpleChatSocklet extends GeneralSocklet {
 7: 	public boolean checkConnection(SocketProcessor client) {
 8: 		client.send("Hello! World.");
 9: 		return true;
10:  	}
11: 	
12: 	public void preRemoveClient(SocketProcessor client) {
13: 		sendToAllClients("A client went away...");
14: 		return;
15: 	}
16: 	
17: 	public boolean doCommand(SocketProcessor client, String command) {
18: 		sendToAllClients(command);
19: 		return true;
20: 	}
21: }
        
ログイン処理
 「通信プロトコル」で、ログイン情報(ユーザー名 / チャットルーム名)は接続用初期コマンドを用いて送信すると決めましたので、ログイン処理はcheckConnectionメソッドで行なうことが出来ます。そこでまずはgetClientsメソッドを用いて、このクライアントと同じ名前のユーザが存在するかしないかを確認してみましょう。getClientで1件もクライアントが取得できなかったら、存在しないと言えます。「username==このクライアントのユーザー名&a.room==チャットルーム名」という検索条件でクライアントを抽出します。
 checkConnectionメソッドで行なわれる「ログイン処理」はリスト5.4のようになります。

リスト5.4
 1: 	public boolean checkConnection(SocketProcessor client) {
 2: 		String roomname = client.getInitParam("room");
 3:
 4: 		// このユーザーと同じユーザー名のクライアントを取得
 5: 		List cls = getClients("username==" + 
    		                       client.getUserName() + 
    		                       "&a.room==" + roomname);
 6: 		if(cls.size() > 0){
 7: 			client.send("-Login failure");
 8: 			return false;
 9: 		}
10: 		// ログイン成功!
11: 		client.send("+Welcome");
12: 		sendToClients("<message command=\"1\" user=\"" + 
    		               client.getUserName() + "\" />", 
    		               "a.room==" + roomname);
13: 		client.setAttribute("room", roomname);
14: 		return true;
15:  	}
        

 ここであえて「固有属性の"room"」を検索しているのは、初期化パラメータだと自分自身も検索されてしまうためです。また、ログインしたままチャットルームを移動する、といったことを行なうためにも固有属性の方が都合が良いでしょう。(初期化パラメータの値は変更出来ません。)ログイン成功後に固有属性"room"にチャットルーム名を設定してます。
 同名のログインユーザを検索した後、そのクライアントにログインの許可・不許可メッセージを送っています。この時点では、XMLメッセージではなく、固定長メッセージを送信しています。第6章で説明するSockletService.asを使用するためです。SockletService.asでは、checkConnectionの結果返されるメッセージの、1文字目のみを確認します。+であれば成功、-であれば失敗と解釈します。
 また、メッセージ送信の後、即クライアントソケットを切断するために、ログイン失敗の場合は偽を返しています。

  さらに、ログイン成功の場合は、ログイン中の他のクライアントに、新しいクライアントが接続してきたことを通知しています。リストの12行目になります。「通信プロトコル」で決めたとおり、ログイン通知メッセージは「<message command="1" user="このクライアントのユーザー名" />」ですね。全クライアントへの通知は、sendToAllClientsメソッドを使用しますが、ここでは特定のチャットルームにログインしているクライアントにのみメッセージを送信したいので、sendToClientsメソッドを使用しています。送信先のクライアント抽出条件は、「固有属性"room"の値が、このクライアントの初期化パラメータ"room"の値と同じクライアント」を意味しています。
チャット発言を処理する
 次に、クライアントから送られてくるチャット発言を処理するルーチンを書きます。この処理は、doCommandメソッドに記述します。「通信プロトコル」で決めた通り、クライアントから送られてくる発言コマンドは常に「@speak=発言内容」の形です。ここでの処理はリスト5.5のようになります。

リスト5.5
 1: 	public boolean doCommand(SocketProcessor client, String command) {
 2: 		if(command.length() <= 7)   { return true; }
 3: 		if(command.charAt(0) != '@'){ return true; }
 4: 	
 5: 		String roomname = (String)client.getAttribute("room");
 6: 		String message  = command.substring(7);
 7: 	
 8: 		sendToClients("<message command=\"2\" user=\"" + 
    		               client.getUserName() + "\">" + 
    		               message + 
    		               "</message>", 
    		               "a.room==" + roomname);
 9: 		return true;
10: 	}
        

 念のため、送られてきたコマンドの一文字目を確認し、プロトコル通り"@"でない場合は、何もしません。また、メッセージから発言内容を抽出するため、コマンドの文字数を確認し、7文字以上である場合のみ発言ありと解釈し、所属チャットルームの全クライアントにメッセージを送信しています。
切断時処理
 最後に、クライアントソケットが切断された時の処理を記述します。残りのメソッドは…preRemoveClientですね。

リスト5.6
 1: 	public void preRemoveClient(SocketProcessor client){
 2: 		String roomname = (String)client.getAttribute("room");
 3: 		sendToClients("<message command=\"3\" user=\"" + 
    		               client.getUserName() + "\"/>", 
    		               "a.room==" + roomname);
 4: 		return;
 5: 	}

 同じチャットルームに所属するクライアントに、このクライアントが切断されたことを通知するのみです。
Socklet完成
 これでSockletクラスは完成です。さっそくコンパイルし、サーバへと配備してください。
 でも、Sockletだけではチャットになりませんね。そこで次の章では、このSockletへ接続するFlashクライアントを作成します。
 
 この章で作成したSimpleChatSockletは、こちらからダウンロードできます。