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); } }