Jetty8で作るWebSocketチャット(サーバ側編)

Jettyをサーバに使ったWebSocketベースのチャットサンプルのようなものを作ってみた。

ターゲットは Jetty 8 と Firefox7, Chrome14。
WebSocket のバージョンは draft-ietf-hybi-thewebsocketprotocol-10 になるはず。
WebSocket(Wikipedia) の実装状況参照のこと)


このエントリではサーバ側の実装について書いてます。ブラウザ側は次のエントリで。


さて、サーバ側の実装ですが、普通にJettyを使うと xml やらなんやら面倒なので、このエントリではJettyをライブラリとして使う組込形態で使っています。

Jetty の lib ディレクトリに入っている次のファイルが WebSocket サーバに必要な jar ファイルになるので、それぞれ classpath を通しておきます。

jetty-continuation-8.0.3.v20111011.jar
jetty-http-8.0.3.v20111011.jar
jetty-io-8.0.3.v20111011.jar
jetty-security-8.0.3.v20111011.jar
jetty-server-8.0.3.v20111011.jar
jetty-servlet-8.0.3.v20111011.jar
jetty-util-8.0.3.v20111011.jar
jetty-websocket-8.0.3.v20111011.jar
servlet-api-3.0.jar


サーバ側の実装ですが、5つのクラスで作っています。
主要なWebSocket周りは以下の3クラス

  • メインクラス(WebSocketTest) -> Jetty のセットアップと起動
  • WebSocketサーブレット(WSServlet) -> ブラウザからの接続受け付けてWebSocketオブジェクトを返す
  • WebSocket(ChatWebSocket) -> WebSocket後の通信担当

他2クラスはメッセージを受けて他の接続に転送する ChatRoom と WebSocket のファクトリクラス(WebSocketFactory)


まず、メインクラス(WebSocketTest)
関連オブジェクトの作成と URL に対応したサーブレットの登録などの Jetty のセットアップ、ユーザ操作の受付ループが入っています。

import java.io.BufferedReader;
import java.io.InputStreamReader;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

public class WebSocketTest {
    public static void main(String[] args) {
        // チャット機能の本体
        ChatRoom chatroom = new ChatRoom();

        // ChatRoom を抱えた WebSocket を返すファクトリ
        WebSocketFactory wfactory = new WebSocketFactory(chatroom);

        // Jetty の初期化
        // ポート8888をlistenする
        Server server = new Server(8888);
        server.setStopAtShutdown(true);
        server.setGracefulShutdown(1000);
        ServletContextHandler root = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);

        // デフォルトのWebインタフェースの設定
        // ドキュメントルートは起動時のカレントディレクトリ
        root.setResourceBase("./");
        root.addServlet(DefaultServlet.class, "/*");

        // WebSocket 受け付ける Servlet を登録
        ServletHolder wsh = new ServletHolder(new WSServlet(wfactory));
        root.addServlet(wsh, "/ws/*");

        try {
            // Jetty サーバ起動
            server.start();

            BufferedReader reader =
                new BufferedReader(new InputStreamReader(System.in));

            // コマンド入力待ち
            // q なら終了
            // その他はサーバ側からのメッセージ送信扱い
            while (true) {
                System.out.print("? >");
                String line = reader.readLine();
                if (line == null || "".equals(line.trim())) {
                    continue;
                } else if ("q".equals(line.trim())) {
                    break;
                } else {
                    chatroom.sendAll("ROOT: " + line.trim());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();    // こまけぇこたぁ(ry
        } finally {
            // Jetty サーバ停止
            try {
                server.stop();
                server.join();
            } catch (Exception e) {
                e.printStackTrace();    // こまk(ry
            }
        }
    }
}


次にサーブレット(WSServlet)
これは、doWebSocketConnect でブラウザからの WebSocket 接続を受け付けて、コネクションに相当する WebSocket オブジェクトを返してます。実際の通信はその WebSocket オブジェクトの仕事です。

import javax.servlet.http.HttpServletRequest;

import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;

public class WSServlet extends WebSocketServlet {
    private static final long serialVersionUID = 8925349860339039372L;
    private WebSocketFactory factory;
    
    public WSServlet(WebSocketFactory factory) {
        if (factory == null)
            throw new IllegalArgumentException();

        this.factory = factory;
    }

    // WebSocket の URL に接続がきたら WebSocket を返す
    @Override
    public WebSocket doWebSocketConnect(HttpServletRequest arg0, String arg1) {
        System.out.println("doWebSocketConnect");
        return factory.getNewWebSocket();
    }
}


ブラウザと WebSocket での通信を担当する ChatWebSocket。
基本は onXXXX で各イベントでの処理を書いてます。そして送信用の口として send メソッドを追加してます。

import java.io.IOException;

import org.eclipse.jetty.websocket.WebSocket;

// 1本のWebSocketコネクションに対応するクラス
public class ChatWebSocket implements WebSocket.OnTextMessage {
    private Connection connection = null;
    private ChatRoom chatroom;

    public ChatWebSocket(ChatRoom chatroom) {
        if (chatroom == null)
            throw new IllegalArgumentException();

        this.chatroom = chatroom;
    }

    // WebSocket が繋がったら chatroom に join する
    @Override
    public void onOpen(Connection con) {
        connection = con;
        chatroom.join(this);
    }

    // WebSocket が切れたら chatroom から leave する
    @Override
    public void onClose(int arg0, String arg1) {
        chatroom.leave(this);
    }

    // メッセージが飛んできたら chatroom にお任せする
    @Override
    public void onMessage(String msg) {
        chatroom.onMessage(this, msg);
    }

    // この接続先にメッセージを投げる
    public void send(String msg) throws IOException {
        connection.sendMessage(msg);
    }
}

以上が WebSocket 周りの取り扱い。


続いてメッセージを受けて投げ返してというチャットの動作をさせる ChatRoom。
接続された WebSocket コネクションを抱え込んで、どれかからメッセージが届いたら全コネクションにメッセージを転送して、というチャットのおきまりパターンです。

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ChatRoom {
    private int idcounter = 0;
    private Map<ChatWebSocket, Integer> users = new ConcurrentHashMap<ChatWebSocket, Integer>();

    // WebSocketコネクションを登録する
    // ついでに適当なIDを割り当てる
    public void join(ChatWebSocket ws) {
        if (users.containsKey(ws))
            return;
        idcounter++;
        users.put(ws, idcounter);
        sendAll("login " + idcounter);
        System.out.println("login " + idcounter);
    }

    // 登録されていたコネクションを取り除く
    public void leave(ChatWebSocket ws) {
        int id = users.get(ws);
        users.remove(ws);
        sendAll("logoff " + id);
        System.out.println("logoff " + id);
    }

    // WebSocketからメッセージが届いたら付加情報を付けて全コネクションに流す
    public void onMessage(ChatWebSocket ws, String msg) {
        int id = users.get(ws);
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        msg = id + ": " + msg + " " + fmt.format(new Date());
        sendAll(msg);
        System.out.println(msg);
    }

    // 抱えてる全コネクションに流す
    public void sendAll(String msg) {
        for (ChatWebSocket socket : users.keySet()) {
            try {
                socket.send(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

最後は WebSocketFactory。
ChatRoom を抱えた WebSocket を返すだけ。

import org.eclipse.jetty.websocket.WebSocket;

// ChatRoom を抱えた ChatWebSocket を返すだけのファクトリ
public class WebSocketFactory {
    private ChatRoom chatroom;
    public WebSocketFactory(ChatRoom chatroom) {
        if (chatroom == null)
            throw new IllegalArgumentException();
        this.chatroom = chatroom;
    }

    public WebSocket getNewWebSocket() {
        return new ChatWebSocket(chatroom);
    }
}