SHOEISHA iD

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

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

特集記事

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

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


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

Javaでルーム機能付きのチャットサーバーおよびチャットクライアントアプリケーションを開発する方法を解説します。

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

完成図
完成図

はじめに

 ここでは、Javaを用いて不特定多数のユーザーの接続を実現するチャットプログラムを作ります。最も単純なチャットプログラムは、1対1で文字列をお互いに送受信するだけですが、不特定多数のユーザーが任意のタイミングでメッセージを交換し合う場合は、サーバーが正しくユーザーを管理し、ユーザーの要求に応じて情報を送らなければなりません。

 本稿で実装するチャットサーバーでは、ユーザー同士を単純につなげるのではなく、チャットルームを作成して、チャットルームに入室しているユーザー同士のみメッセージを交換する、より本格的なチャットサービスを実装します。これを管理するには、チャットサーバー、チャットルーム、チャットユーザーの関係と役割をオブジェクトに与え、任意の数のチャットルームとチャットユーザーを動的な配列で管理しなければなりません。

 本来ならば、サーバーとクライアント間の通信にはネットワークトラフィックの問題やセキュリティ、将来の拡張などのさまざまな要素が存在し、それらを十分に分析してプロトコルを策定する必要があります。しかし、厳密にプロトコルを策定し、バイナリデータで通信しては、アプリケーションの振る舞いが分かり難くなってしまいます。学習用を目的とする今回のプログラムでは、単純な一行テキストのコマンドでサーバーとクライアント間の通信を実現することにしました。

対象読者

 ここでは、Java言語の文法、およびオブジェクト指向について基本的な理解がある方を対象としています。また、プログラムではjava.langjava.utiljava.iojava.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アプリケーションの実装経験がある方ならば見慣れた設計でしょう。

会員登録無料すると、続きをお読みいただけます

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

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

メールバックナンバー

次のページ
メッセージ

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

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

もっと読む

この記事の著者

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

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

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

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

この記事をシェア

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

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング