SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

Javaで作るルーム機能付きチャットサーバー

チャットソフトの開発で学ぶJava Socketプログラミング


  • X ポスト
  • このエントリーをはてなブックマークに追加

メッセージ

 本来ならば、サーバーとクライアントとの間でやり取りする情報の交換方法を定めなければなりません。これをプロトコルと呼びます。ネットワーク間の通信の場合、相手コンピュータを固定的に定めることができないため、通信に利用する文字コードや形式を明確にする必要があるのです。

 しかし、複雑なメッセージ交換方法を定めてしまうと、その部分に関するソースコードが膨大になってしまいます。とくに、独自のデータ形式の場合はパーサーを実装しなければなりません。そのため、今回は単純に一行文字列のコマンドでメッセージを交換したいと思います。メッセージの末尾は改行文字とします。

コマンド名 値

 コマンドは、コマンド名から始まり半角空白で区切った後にコマンドに付属する値を指定するものとします。値はコマンドの内容によっては省略可能とします。例えば、ユーザーが通常の会話用メッセージを送ってきた場合は次のようなmsgコマンドを送るものと定めます。

msg 任意の文字列

 このほかにも多くの操作用コマンドを用意します。今回は、入室コマンドにenterRoom、退室にはexitRoom、名前の設定にはsetNameというようにコマンドを定めています。詳細はソースコードを参照してください。

public void messageThrow(MessageEvent e) {
    String msgType = e.getName();
    String msgValue = e.getValue();

    //切断する
    if (msgType.equals("close")) {
        try { close(); }
        catch(IOException err) { err.printStackTrace(); }
    }
    //名前を更新する
    else if(msgType.equals("setName")) {
        String name = msgValue;

        //半角文字は使えない記号
        if (name.indexOf(" ") == -1) {
            String before = getName();
            setName(name);
            sendMessage("successful setRoom");
            reachedMessage("msg", before + " から "
                + name + " に名前を変更しました");
        }
        else {
            sendMessage(
                "error 名前に半角空白文字を使うことはできません");
        }
    }
    //新しい部屋を追加する
    else if(msgType.equals("addRoom")) {
        String name = msgValue;

        //半角文字は使えない記号
        if (name.indexOf(" ") == -1) {
            ChatRoom room = new ChatRoom(name , this);
            server.addChatRoom(room);
            sendMessage("successful addRoom");
        }
        else sendMessage(
            "error 名前に半角空白文字を使うことはできません");
    }
    //現在存在する部屋を返す
    else if(msgType.equals("getRooms")) {
        String result = "";
        ChatRoom[] rooms = server.getChatRooms();
        for(int i = 0 ; i < rooms.length ; i++) {
            result += rooms[i].getName() + " ";
        }
        sendMessage("rooms " + result);
    }
    //部屋に入る
    else if(msgType.equals("enterRoom")) {
        ChatRoom room = server.getChatRoom(msgValue);
        if (room != null) {
            room.addUser(this);
            sendMessage("successful enterRoom");
        }
        else sendMessage(
            "error \"" + msgValue + "\" が見つかりません");
        }
        //部屋から出る
        else if(msgType.equals("exitRoom")) {
            ChatRoom room = server.getChatRoom(msgValue);
            if (room != null) {
            room.removeUser(this);
            sendMessage("successful exitRoom");
        }
        else sendMessage(
            "error \"" + msgValue + "\" が見つかりません");
    }
    //指定した部屋のユーザーのリストを返す
    else if(msgType.equals("getUsers")) {
        ChatRoom room = server.getChatRoom(msgValue);
        if (room != null) {
            String result = "";
            ChatClientUser[] users = room.getUsers();
            for(int i = 0 ; i < users.length ; i++) {
                result += users[i].getName() + " ";
            }
            sendMessage("users " + result);
        }
    }
}

 もちろん、本格的なチャットサービスを構築しようと思った場合はこれでは不十分です。送られてきたメッセージに対して、誰が、いつ、送信したのかというメッセージに対するメタデータを付属させる必要があるためです。また、将来の拡張にも備えた方が良いでしょう。個人的には、XMLを使ってメッセージを交換する方法をお勧めします。

 また、ネットワーク負荷や処理効率を優先する場合は、文字ベースのコマンドではなくバイナリデータで通信した方が効率的です。ただし、将来の拡張や変更には弱くなる可能性があります。

チャットルーム

 チャットルームは、チャットルームに入室しているユーザーのリストを管理し、部屋に入室しているユーザーのいずれかがmsgコマンドを送信してきた場合に、そのメッセージを同じ部屋のすべてのユーザーに正しい形に変換して送信します。チャットルームはChatRoomクラスとして実装しています。

 ユーザーがaddRoomコマンドを送信してきた場合、ChatClientUserは新しいChatRoomインスタンスを生成してサーバーに追加します。ChatRoomクラスはユーザーの追加や削除を行うメソッドを公開しています。

 以下のコードは、ChatRoomクラスのメソッドです。addUser()メソッドは新しくこの部屋にユーザーを追加(入室)させ、removeUser()メソッドは現在入室しているユーザーを解除(退室)させます。

//この部屋にユーザーを追加(入室)する
public void addUser(ChatClientUser user) {
    user.addMessageListener(this);
    roomUsers.add(user);
    for(int i = 0 ; i < roomUsers.size() ; i++) {
        roomUsers.get(i).reachedMessage("getUsers", name);
        roomUsers.get(i).sendMessage("msg >"
            + user.getName() + " さんが入室しました");
    }
}

//指定したユーザーをチャットルームから退室させる
public void removeUser(ChatClientUser user) {
    user.removeMessageListener(this);
    roomUsers.remove(user);
    for(int i = 0 ; i < roomUsers.size() ; i++) {
        roomUsers.get(i).reachedMessage("getUsers", name);
        roomUsers.get(i).sendMessage("msg >"
            + user.getName() + " さんが退室しました");
    }

    //ユーザーがいなくなったので部屋を削除する
    if (roomUsers.size() == 0) {
        ChatServer.getInstance().removeChatRoom(this);
    }
}

public void messageThrow(MessageEvent e) {
    ChatClientUser source = e.getUser();

    //ユーザーが発言した
    if (e.getName().equals("msg")) {
        for(int i = 0 ; i < roomUsers.size() ; i++) {
            //msg 発言ユーザー名 メッセージ
            String message = e.getName() + " "
                + source.getName() + ">" + e.getValue();
            roomUsers.get(i).sendMessage(message);
        }
    }
    //ユーザーが名前を変更した
    else if(e.getName().equals("setName")) {
        for(int i = 0 ; i < roomUsers.size() ; i++) {
            roomUsers.get(i).reachedMessage("getUsers", name);
        }
    }
}

 ChatRoomクラスはMessageListenerクラスを実装しています。入室しているユーザーがメッセージを送ってきたかどうかは、addUser()メソッドでユーザーを登録したときに、ChatClientUserオブジェクトのaddMessageListener()メソッドでChatRoomオブジェクトをリスナとして追加することで判断できます。これで、ユーザーが送信したメッセージを常に監視することができ、興味のあるメッセージであればmessageThrow()メソッド内で処理します。

チャットクライアントを作る

 今回、クライアントアプリケーションはJava Swingコンポーネントを使って実装します。コードの大部分はコンポーネントの定義と操作に関する部分であり、流れや設計としては複雑なものではありません。クライアントはChatClientクラスとして実装しています。ChatClientコンストラクタはコンポーネントを初期化して組み合わせているだけなので、この部分のコードの説明は割愛します。

 クライアントアプリケーションが接続するサーバーはHOSTフィールドに、ポート番号はPORTフィールドに固定してあります。今回はローカル環境でのテストのみを想定しているため 「localhost」に固定してあります。別のコンピュータに接続する場合はHOSTフィールドの値を変更してください。

 サーバーはServerSocketクラスを使ってクライアントからの接続を待機しました。これに対してクライアントはSocketクラスを使ってサーバーに接続しなければなりません。サーバーとは異なり、外部からの接続を待機する必要はありません。サーバーへの接続はconnectServer()メソッドで行なっています。

public void connectServer() {
    try {
        socket = new Socket(HOST, PORT);
        msgTextArea.append(">サーバーに接続しました\n"); 
    }
    catch(Exception err) {
        msgTextArea.append("ERROR>" + err + "\n"); 
    }
}

 このSocketクラスはサーバー側で接続してきたクライアントを表したSocketクラスと同じです。入出力APIを用いてサーバーにデータを送信することができるので、コマンドをprintln()メソッドなどでコマンド文字列を送信すればサーバーを操作することができます。

 クライアントアプリケーションもサーバーアプリケーションと同様にマルチスレッドで構築する必要があります。特に、サーバーからのメッセージの受信は常に監視する必要があるため、新しいスレッドを作成してrun()メソッド内で受信を待機させる必要があります。クライアントアプリケーションはGUI対応なので、イベントディスパッチスレッドを停止させてしまうと、ユーザーからの入力イベントを処理できなくなり、結果としてウィンドウがビジー状態になってしまいます。

public void run() {
    try {
        InputStream input = socket.getInputStream();
        BufferedReader reader
            = new BufferedReader(new InputStreamReader(input));
        while(!socket.isClosed()) {
            String line = reader.readLine();

            String[] msg = line.split(" ", 2);
            String msgName = msg[0];
            String msgValue = (msg.length < 2 ? "" : msg[1]);

            reachedMessage(msgName, msgValue);
        }
    }
    catch(Exception err) { }
}

 これが、ChatClientクラスのメッセージ受信用スレッドとなります。ChatClientRunnableインタフェースを実装し、コンストラクタから新しいスレッドを起動します。コードの内容は、サーバーのChatClientUserクラスのrun()メソッドとほとんど同じであることがわかると思います。

 ChatClientクラスは、sendMessage()メソッドでサーバーに任意のコマンド文字列を送信し、reachedMessage()メソッドで受信したメッセージを処理します。サーバーは、クライアントに対して他のユーザーのメッセージのほかに、現在作られている部屋のリストや、入室している部屋のユーザーリストなどを送信してきます。このようなメッセージを受け取り、適切にコンポーネントを更新しなければなりません。

//メッセージをサーバーに送信する
public void sendMessage(String msg) {
    try {
        OutputStream output = socket.getOutputStream();
        PrintWriter writer = new PrintWriter(output);

        writer.println(msg);
        writer.flush();
    }
    catch(Exception err)
        { msgTextArea.append("ERROR>" + err + "\n"); }
}

//サーバーから送られてきたメッセージの処理
public void reachedMessage(String name, String value) {
    //チャットルームのリストに変更が加えられた
    if (name.equals("rooms")) {
        if (value.equals("")) {
            roomList.setModel(new DefaultListModel());
        }
        else {
            String[] rooms = value.split(" ");
            roomList.setListData(rooms);
        }
    }
    //ユーザーが入退室した
    else if (name.equals("users")) {
        if (value.equals("")) {
            userList.setModel(new DefaultListModel());
        }
        else {
            String[] users = value.split(" ");
            userList.setListData(users);
        }
    }
    //メッセージが送られてきた
    else if (name.equals("msg")) {
        msgTextArea.append(value + "\n"); 
    }
    //処理に成功した
    else if (name.equals("successful")) {
        if (value.equals("setName"))
            msgTextArea.append(">名前を変更しました\n"); 
    }
    //エラーが発生した
    else if (name.equals("error")) {
        msgTextArea.append("ERROR>" + value + "\n"); 
    }
}

最後に

 今回作成したチャットサーバー・クライアントを、ルーム機能付きの公開用サーバーを開発する参考にしていただければ幸いです。しかし、このプログラムにはまだ多くの問題を残しています。例えば、このプログラムはネットワーク上での本格的な運用を想定していないため、セキュリティやエラーチェックが不十分です。

 また、機能的にも追加要素がたくさん考えられます。ユーザーの検索、特定のユーザーにのみメッセージを送るウィスパー機能、チャットルームのホストユーザーに対する特別な権限(ユーザーの拒否、強制退出など)、ユーザーの入室・発言時にサウンドを鳴らすなど、必要な機能はたくさんあると思います。

 しかし、こうした機能の追加もサーバーとクライアント間の通信部分を拡張すれば実現することができます。今回作成したソースコードでは、サーバー、クライアント、チャットルームなどが役割ごとにクラス化されているため、機能の変更や拡張は難しいものではありません。

参考資料

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

赤坂 玲音(アカサカ レオン)

平成13年度「全国高校生・専門学校生プログラミングコンテスト 高校生プログラミングの部」にて最優秀賞を受賞。2005 年度~ Microsoft Most Variable Professional Visual Developer - Visual C++。プログラミング入門サイト WisdomSoft の管理人。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/193 2006/05/08 10:31

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング