Cometとは?
ブラウザベースのチャットをつくろうとする場合、以前は定期的にクライアントからリクエストを送信して更新を確認するという手法がとられました。そうすると、平均して更新間隔の1/2の遅延が発生し、更新がないときの問い合わせが無駄になるなど、ユーザーにもサーバーにもうれしい手法ではありませんでした。
そこで使われるようになったのがCometです。
Cometは、HTTPでクライアントからの接続への返答を保留して、サーバーからデータを送信する必要がでたときに返答を返すことで、サーバーからのリアルタイムデータ送信を行う手法の総称です。
Servlet3.0でのComet対応
Cometでは、クライアントからの接続を保持しつづけるので、これまでのServletの仕組みをつかって実現しようとすると、各接続にスレッドを割り当てることになり、スレッド数が多くなりすぎるため、多くのユーザーには対応できませんでした。
そこで、サーバー側で専用の仕組みを持つ必要があるのですが、これまでTomcatやGlassfish、Jettyなどで独自の仕様で対応されていました。
Servlet3.0では、Comet対応が標準化されたので、共通のコードで実現することができるようになりました。
とりあえず非同期サーブレット
ということで、接続を持続するサーブレットを書いてみます。
package chat; import java.io.IOException; import java.util.*; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet(name = "PollingServlet", urlPatterns = {"/polling"}, asyncSupported=true) public class PollingServlet extends HttpServlet { public static final String CONTEXT_NAME = "contexts"; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ServletContext servletContext = request.getServletContext(); List<AsyncContext> contexts = (List<AsyncContext>)servletContext.getAttribute(CONTEXT_NAME); if(contexts == null){ contexts = new ArrayList<AsyncContext> (); servletContext.setAttribute(CONTEXT_NAME, contexts); } final AsyncContext ac = request.startAsync(); contexts.add(ac); } }
WebServletアノテーションでasyncSupported=trueの指定をすると、非同期サーブレットになります。
HttpServletRequestオブジェクトのstartAsync()メソッドを呼び出すと、接続を管理するAsyncContextオブジェクトが得られるので、どこかに保存して、処理を終えます。
今回は、AsyncContextオブジェクトをservletContextに保持させています。
サーブレット自体の処理が終わっても、接続が引き継がれます。
なんらかのタイミングでこの接続に対してデータを送信すればいいのですが、今回は別のリクエストを受け取ったタイミングでデータを送信してみます。
package chat; import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet(name = "PushServlet", urlPatterns = {"/push"}) public class PushServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { request.setCharacterEncoding("utf-8"); String text = request.getParameter("text"); ServletContext servletContext = request.getServletContext(); List<AsyncContext> contexts = (List<AsyncContext>) servletContext.getAttribute(PollingServlet.CONTEXT_NAME); if(contexts != null){ for(AsyncContext ac : contexts.iterator()){ try{ ac.getResponse().setCharacterEncoding("utf-8"); PrintWriter writer = ac.getResponse().getWriter(); writer.println(text); writer.close(); ac.complete(); }catch(Exception e){ e.printStackTrace(); } } contexts.clear(); } out.println("ok"); } finally { out.close(); } } }
これはサーブレット自体は通常のものですが、ここで保存していたAsyncContextに対してレスポンスを返しています。
このようにすると、/pollingを開くと接続が保持されたままになって、別のブラウザ画面で/push?text=hogeのようにパラメータを渡して開くと、/pollingを開いていた画面にパラメータの文字列が表示されます。
チャット画面の作成
あとは、これらを呼び出す画面を作ると、チャットっぽくなります。
<!DOCTYPE html> <html> <head> <title>チャット</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script type="text/javascript" src="resources/jquery-1.4.4.min.js"></script> <script type="text/javascript"> $(function(){ connect(); $("#btn").click(button) }); /* 接続 */ function connect(){ $.ajax({ url:"./polling", type:"GET", data:{}, complete: fin, success:pushed }); } /* 投稿 */ function pushed(data, status, hxr){ $t = $("<div>").wrapInner(data); $("#result").append($t); } /* 接続終了の再接続 */ function fin(hxr, status){ connect(); } /* ボタンが押されたときの処理 */ function button(){ $.post("./push", {text: $("#txt").val()}); $("#txt").val(""); } </script> </head> <body> <h1>comet chat</h1> <input type="text" id="txt"/><button id="btn">入力</button> <div id="result"></div> </body> </html>
ここではjQueryを使ってAjax通信を行っています。初期化のときと接続が切れたときとconnect関数を呼び出して、常に接続が行われているようにしているのが、Cometです。