読者です 読者をやめる 読者になる 読者になる

SIerだけど技術やりたいブログ

5年目のSIerのブログです

リアルタイムなwebアプリを実現する方法(ポーリング、Comet、Server Sent Events、WebSocket)

このまえ仕事で、リアルタイムなwebアプリを実現する方法の話題になった。
色々な実現方法が出てきて会話についていけなかったので、軽く試す。

実現方法の一覧

  • ポーリング
  • Comet
    • ロングポーリングで実現するパターン
    • マルチパートで実現するパターン
  • Server Sent Events
  • WebSocket

具体的にどんな方法?どんなメリットがあるの?

以下の資料が理解に役立ちます。

サンプルコード

動作確認レベルのコードを書いた。

ポーリング(Ajax)

https://github.com/kimullamen/polling.git

クライアント側

window.setTimeoutを使って定期的にサーバにリクエストをなげる。 結果を受け取ったら画面の一部を更新する。

$(function() {
  var POLLLING_INVERVAL_TIME_IN_MILLIS = 10000;//10s
  (function polling() {
    getCountUp();
    window.setTimeout(polling, POLLLING_INVERVAL_TIME_IN_MILLIS);
  }());
  
  function getCountUp() {
    $.ajax({
    type : "GET",
    url : "countUp",
    content : "application/json",
    dataType : "json",
  }).done(function(data) {
    $("dd").text(data.count);//html要素変更する
  }).fail(function(jqXHR, textStatus) {
    $("dd").text("error occured");//html要素変更する
    });
  }
});

サーバ側

単純にリクエストに対してレスポンスを返すだけ。 更新対象のデータが更新されていれば新しい値を返せるけど、更新されてなかったら変更のないデータを返すことになる。

Comet(ロングポーリング) 同期

https://github.com/kimullamen/comet-synchronized.git

クライアント側

成功しても失敗しても即座にサーバにリクエストを投げる。 結果を受け取ったら画面の一部を更新する。

$(function() {
  (function getCountUp() {
  $.ajax({
    type : "GET",
    url : "countUp",
    content : "application/json",
    dataType : "json",
  }).done(function(data) {
    $("dd").text(data.count); //html要素変更する
    getCountUp();
  }).fail(function(jqXHR, textStatus) {
    $("dd").text("error occured"); //html要素変更する
    getCountUp();
    });
  })();
});

サーバ側

サーバ側を同期処理にするなら、Servletの処理をブロックさせて待機させる。

@WebServlet(name = "CountUpServlet", urlPatterns = { "/countUp" })
public class CountUpServlet extends HttpServlet {
  // countUpされる値
  AtomicInteger count = new AtomicInteger(0);
  
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    //インクリメントされるまで待つ
    int prev = count.get();
    while (count.get() <= prev) {
    }
    resp.setContentType("application/json");
    resp.getWriter().write("{\"count\":\"" + count.get() + "\"}");
  }  

  @Override
  public void init() throws ServletException {
    super.init();
    // 5sごとにcountをインクリメントする
    Timer timer = new Timer();
    TimerTask task = new TimerTask() {
      @Override
      public void run() {
        count.incrementAndGet();
      }
    };
    timer.schedule(task, 5000, 5000);
  }  
}

Comet(ロングポーリング) 非同期

https://github.com/kimullamen/comet-asynchronized.git

クライアント側

Comet(ロングポーリング) 同期 と一緒。

サーバ側

サーバ側を非同期処理にするなら、AsyncContext を利用してServletで処理がブロックされるのを防ぐ。 非同期にすることによって、リクエストを処理するスレッドの占有を防ぐことができるため、サーバ資源の節約になる。 ※

値が更新され次第、クライアントに値を返す。

@WebServlet(name = "CountUpServlet", urlPatterns = { "/countUp" }, asyncSupported = true)
public class CountUpServlet extends HttpServlet {
  // countUp対象
  int count;
  List<AsyncContext> queue = new ArrayList<>();

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    final AsyncContext context = req.startAsync();
    queue.add(context);
  }

  @Override
  public void init() throws ServletException {
    super.init();
    // 5sごとにインクリメントする
    Timer timer = new Timer();
    TimerTask task = new TimerTask() {
      @Override
      public void run() {
        count++;
        broadcast();
      }
    };
    timer.schedule(task, 5000, 5000);
  }

  synchronized public void broadcast() {
    CopyOnWriteArrayList<AsyncContext> target = new CopyOnWriteArrayList<>(queue);
    synchronized (queue) {
      queue = new ArrayList<>();
    }

    for (AsyncContext context : target) {
      HttpServletResponse resp = (HttpServletResponse) context.getResponse();
      resp.setContentType("application/json");
      try {
        PrintWriter writer = resp.getWriter();
        writer.write("{\"count\":\"" + count + "\"}");
        writer.close();
        context.complete();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

※ AsyncContextを利用するメリットについては、別ブログに書いた。

kimulla.hatenablog.com

Server Sent Events

クライアント側

javascriptAPIが標準化されているのでそれを利用する。

EventSource - Web API インターフェイス | MDN

$(function() {
  (function getCountUp() {
    var sse = new EventSource(
        "http://localhost:8080/server-sent-events/countUp");
    sse.addEventListener('message', function(event) {
      $('dd').text(JSON.parse(event.data).count);
    });
  })();
});

サーバ側

1つのコネクション内で複数のリクエスト・レスポンスのやりとりをするため、HTTPレスポンスを閉じずに開きっぱなしにする。 そのためコネクションはcloseしない。

@WebServlet(name = "CountUpServlet", urlPatterns = { "/countUp" }, asyncSupported = true)
public class CountUpServlet extends HttpServlet {
  // countUp対象
  int count;
  List<AsyncContext> queue = new ArrayList<>();

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    final AsyncContext context = req.startAsync();
    // タイムアウトしないようにmax値入れる
    context.setTimeout(Long.MAX_VALUE);
    queue.add(context);
  }

  @Override
  public void init() throws ServletException {
    super.init();
    // 5sごとにインクリメントする
    Timer timer = new Timer();
    TimerTask task = new TimerTask() {
      @Override
      public void run() {
        count++;
        broadcast();
      }
    };
    timer.schedule(task, 5000, 5000);
  }

  synchronized public void broadcast() {
    CopyOnWriteArrayList<AsyncContext> target = new CopyOnWriteArrayList<>(queue);

    // SSEのためにコネクションはcloseしない
    for (AsyncContext context : target) {
      HttpServletResponse resp = (HttpServletResponse) context.getResponse();
      resp.setContentType("text/event-stream");
      resp.setCharacterEncoding("UTF-8");
      try {
        PrintWriter writer = resp.getWriter();
        writer.write("data: {\"count\":\"" + count + "\"}\n\n");
        writer.flush();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

}

https://github.com/kimullamen/server-sent-events.git

WebSocket

流行ってるしソースだけ。

https://github.com/kimullamen/websocket.git
※Jerseyなどは対応するAPIがあるらしいけどServletを直接利用した

実装してみた感想

  • ポーリング
    • 実装がシンプル
    • 大してリアルタイム性が求められないならこれで充分では
  • Comet
    • 標準的なservlet3のAPIだけで実装するとasyncContextを直接触る感じになって、自分のような雑魚には不安
  • Server Sent Events
    • やっぱりservletだけで実装するのはアレ
    • cometの標準化版だし、今後javaEEで対応してくれるなら良いのでは
  • WebSocket
    • 参照できるサイトも多くて実装しやすかった

参考にしたサイト