はじめに
ここでは、Javaを用いて不特定多数のユーザーの接続を実現するチャットプログラムを作ります。最も単純なチャットプログラムは、1対1で文字列をお互いに送受信するだけですが、不特定多数のユーザーが任意のタイミングでメッセージを交換し合う場合は、サーバーが正しくユーザーを管理し、ユーザーの要求に応じて情報を送らなければなりません。
本稿で実装するチャットサーバーでは、ユーザー同士を単純につなげるのではなく、チャットルームを作成して、チャットルームに入室しているユーザー同士のみメッセージを交換する、より本格的なチャットサービスを実装します。これを管理するには、チャットサーバー、チャットルーム、チャットユーザーの関係と役割をオブジェクトに与え、任意の数のチャットルームとチャットユーザーを動的な配列で管理しなければなりません。
本来ならば、サーバーとクライアント間の通信にはネットワークトラフィックの問題やセキュリティ、将来の拡張などのさまざまな要素が存在し、それらを十分に分析してプロトコルを策定する必要があります。しかし、厳密にプロトコルを策定し、バイナリデータで通信しては、アプリケーションの振る舞いが分かり難くなってしまいます。学習用を目的とする今回のプログラムでは、単純な一行テキストのコマンドでサーバーとクライアント間の通信を実現することにしました。
対象読者
ここでは、Java言語の文法、およびオブジェクト指向について基本的な理解がある方を対象としています。また、プログラムではjava.lang
、java.util
、java.io
、java.swing
パッケージを利用しますが、これらのパッケージのクラスについて詳しい説明は割愛します。
この文書は、java.net
パッケージのServerSocket
およびSocket
クラスを使ってネットワーク上のコンピュータに接続し、不特定多数のユーザーが任意のタイミングでメッセージを交換し合うプログラムの実装方法について解説します。
必要な環境
Javaの実行、および開発はマルチプラットフォームです。開発環境はJ2SE Development Kit(JDK) 5.0、またはそれ以降のバージョンのものをSun Microsystemsのウェブサイトからダウンロードしてインストールしてください。ソースコードの記述は使い慣れたテキストエディタをご用意ください。
Javaアプリケーションを実行するにはJ2SE Runtime Environment(JRE) 5.0、またはそれ以降のバージョンをご用意ください。本稿に付属しているソースコードのコンパイルおよび実行は、JDK 5.0およびJRE 5.0で確認しています。それ以外のバージョンでは動作しないことがあるかもしれませんが、ご了承ください。
今回作成するチャットプログラムはローカルコンピュータ上で実験することを前提としているため、ネットワークは必須ではありません。
チャットサーバーを作る
サーバー/クライアントシステムを構築する場合、まずはサーバーがどのようなサービスを提供するべきなのかを設計します。今回作るプログラムでは、チャットサーバーがユーザーに対してチャットルームを提供し、チャットルームに接続しているユーザー同士のメッセージを交換させます。このことから、チャットサーバー、チャットルーム、チャットユーザーを表すクラスが必要になります。プログラムではチャットサーバーをChatServer
、チャットルームをChatRoom
、チャットユーザーをChatClientUser
という名前のクラスで表現しています。
サーバーは、ChatServer
クラスのmain()
メソッドから起動します。ChatServer
クラスはコンストラクタをprivate
で隠蔽しているシングルトンクラスです。シングルトンとは、アプリケーションの実行プロセス単位に常にインスタンスがひとつしか存在しないクラスのことです。ChatServer
クラスのオブジェクトがプロセス内に複数存在する必要はありません。
private
修飾子でコンストラクタを隠蔽すると、外部からnew
演算子を用いてコンストラクタを呼び出せなくなります。よって、クラスの外部でChatServer
インスタンスを作ることはできません。ChatServer
クラスのインスタンスを取得するには、ChatServer
クラスのgetInstance()
クラスメソッドを呼び出します。このメソッドはstatic
修飾子が指定されているメソッドなのでクラス名から呼び出すことができます。
getInstance()
メソッドは、ChatServer
クラスのinstance
クラスフィールドがnull
であるかどうかを調べ、これがnull
であればまだChatServer
クラスのインスタンスが生成されていないと判断して、唯一のインスタンスを生成します。それ以降は、生成したインスタンスを単純に返すだけのメソッドとなります。
private static ChatServer instance; //現在開いている部屋オブジェクトの動的配列 private ArrayList<ChatRoom> roomList; //現在チャットに参加している全ユーザーの動的配列 private ArrayList<ChatClientUser> userList; public static ChatServer getInstance() { if (instance == null) { instance = new ChatServer(); } return instance; } private ChatServer() { roomList = new ArrayList<ChatRoom>(); userList = new ArrayList<ChatClientUser>(); }
このような設計のクラスをシングルトンと呼び、GoFデザインパターンの中でも特に有名なパターンです。アプリケーションシステム全体の設定を表現するクラスや、ゲームアプリケーションでシステム全体の制御に関する情報を提供するクラスなどで、この設計を採用することができます。
main()
メソッドでは、ChatServer.getInstance()
メソッドからChatServer
オブジェクトを取得してstart()
メソッドを起動します。このメソッドを起動すると、サーバーはwhile()
ループで新しいクライアントの接続を待機するようになります。
チャットサーバーは、接続してくるユーザーに対して、存在しているチャットルームの情報や、チャットルームに入室しているユーザーの情報などを提供しなければなりません。さらに、ユーザー名の設定、チャットルームの作成、チャットルームの入室・退室などの機能も提供する必要があります。ChatServer
オブジェクトは、このうちサーバーに接続しているユーザーの追加や削除、チャットルームの追加や削除などの機能を提供します。
チャットルームの操作に関するメソッドは次のものがあります。
public void addChatRoom(ChatRoom room)
public ChatRoom getChatRoom(String name)
public ChatRoom[] getChatRooms()
public void removeChatRoom(ChatRoom room)
public void clearChatRoom()
チャットユーザーの操作に関するメソッドは次のものがあります。
public void addUser(ChatClientUser user)
public ChatClientUser getUser(String name)
public ChatClientUser[] getUsers()
public void removeUser(ChatClientUser user)
public void clearUser()
クライアントの接続
ChatServer
クラスのstart()
メソッドでは、最初にjava.net.ServerSocket
クラスのインスタンスを生成しています。サーバーとクライアントがお互いに通信するには、ソケットと呼ばれる仮想的な通信インタフェースを構築する必要があります。ソケットはコンピュータのIPアドレスと接続先ポート番号で相手のコンピュータと接続します。
java.net.ServerSocket
クラスは、クライアントからのTCP接続要求を待機し、クライアントからの接続を確認するとクライアントソケットであるjava.net.Socket
オブジェクトを返してくれます。
ソケットはネットワークの低レベルの(より物理レベルに近い)問題や管理を抽象化してくれるため、ファイルや標準入力、標準出力に対する入出力と同じようにjava.io
パッケージの入出力ストリームを用いてネットワーク上のコンピュータと通信することができます。
ServerSocket
クラスのコンストラクタでは、開くサーバーのポート番号を渡しています。クライアントコンピュータはこのポート番号に対してクライアントソケットで接続する必要があります。サーバーがクライアントを待機する準備ができれば、ServerSocket
オブジェクトのaccept()
メソッドを呼び出してクライアントを待機します。このメソッドは、クライアントからの接続が確立するまで待機します。サーバーアプリケーションをGUIに対応するような場合はイベントディスパッチスレッドとは別のスレッドでaccept()
メソッドを呼び出すようにしてください。accept()
メソッドがクライアントとの接続を確立させれば、クライアントのSocket
オブジェクトを返します。このメソッドが返したSocket
オブジェクトに対して入出力を行えば、クライアントコンピュータにデータを送信したり、クライアントコンピュータからのデータを受信することができます。
public void start() { try { server = new ServerSocket(2815); while(!server.isClosed()) { //新しいクライアントの接続を待つ Socket client = server.accept(); //ユーザーオブジェクトを生成する ChatClientUser user = new ChatClientUser(client); addUser(user); } } catch(Exception err) { err.printStackTrace(); } }
クライアントがサーバーに接続してSocket
オブジェクトを取得することができれば、接続中のクライアントを管理するためのChatClientUser
オブジェクトを生成してサーバーに追加します。
ChatClientUserクラス
チャットプログラムの場合、ユーザーがサーバーに対してメッセージを送信するタイミングは任意であり、同期的な通信を行うことはできません。また、別のユーザーがメッセージを送信してきた場合、サーバーは同じ部屋のチャットユーザーにメッセージが送られてきたことを通知する必要があります。ユーザーがいつメッセージを送信してくるか特定できない以上、サーバー側はSocket
オブジェクトからの入力を待機して監視しなければならないので、ChatClientUser
オブジェクトごとにデータ受信用のスレッドを作ります。
このクラスは、別のスレッドでユーザーからのメッセージを受信するためにRunnable
インタフェースを実装しています。
public void run() { try { //ユーザーの情報を取得する InputStream input = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(input)); //ユーザーのメッセージ送信を確認 while(!socket.isClosed()) { String line = reader.readLine(); System.out.println("INPUT=" + line); String[] msg = line.split(" ", 2); String msgName = msg[0]; String msgValue = (msg.length < 2 ? "" : msg[1]); reachedMessage(msgName, msgValue); } } catch(Exception err) { err.printStackTrace(); } }
run()
メソッドでは、ソケットが有効な限りwhile
文のループを実行し続けます。ループでは、最初にソケットからの入力(すなわち受信)を待機し、メッセージが入力されると、メッセージ処理を行うreachedMessage()
メソッドを呼び出します。
ChatClientUser
クラスでは、メッセージの受信をイベントとして処理します。reachedMessage()
メソッドでは自分自身に登録されているリスナオブジェクトすべてにメッセージを受信したことを通知します。リスナオブジェクトはArrayList<MessageListener>
型のmessageListeners
フィールドに格納されています。
public void reachedMessage(String name, String value) { MessageEvent event = new MessageEvent(this, name, value); for(int i = 0 ; i < messageListeners.size() ; i++ ) { messageListeners.get(i).messageThrow(event); } }
リスナとはMessageListener
インタフェースを実装するオブジェクトを指します。リスナには、受信したメッセージなどの情報を提供するMessageEvent
オブジェクトを渡します。MessageEvent
クラスはjava.util.EventObject
クラスを継承するイベント状態オブジェクトです。
class MessageEvent extends EventObject { private ChatClientUser source; private String name; private String value; public MessageEvent(ChatClientUser source, String name, String value) { super(source); this.source = source; this.name = name; this.value = value; } //イベントを発生させたユーザー public ChatClientUser getUser() { return source; } //このイベントのコマンド名を返す public String getName() { return this.name; } //このイベントの public String getValue() { return this.value; } } interface MessageListener extends EventListener { void messageThrow(MessageEvent e); }
MessageListener
インタフェースは、メッセージを処理するmessageThrow()
メソッドを宣言しています。ChatClientUser
オブジェクトに対してMessageListener
を実装するオブジェクトを設定することで、ユーザーが送信したメッセージをサーバーが受信したタイミングでmessageThrow()
メソッドが呼び出されます。
ChatClientUser
クラスのリスナ登録用メソッドは次のように実装しています。
//このオブジェクトにメッセージリスナを登録する public void addMessageListener(MessageListener l) { messageListeners.add(l); } //指定したメッセージリスナをこのオブジェクトから解除する public void removeMessageListener(MessageListener l) { messageListeners.remove(l); } //このオブジェクトに登録されているメッセージリスナの配列を返す public MessageListener[] getMessageListeners() { MessageListener[] listeners = new MessageListener[messageListeners.size()]; messageListeners.toArray(listeners); return listeners; }
メッセージのイベント処理は、基本的にAWTのイベント処理モデルに従っています。Java AWTによるGUIアプリケーションの実装経験がある方ならば見慣れた設計でしょう。