ケーススタディ - Stream Congress のリアルタイム更新

はじめに

HTML5 では、WebSocketsEventSource を使用して、サーバーとリアルタイムに通信するウェブアプリを作成できます。Stream CongressChrome ウェブストアで入手可能): 米国議会の活動に関する最新情報をリアルタイムに提供しています。下院および上院の議題情報、関連ニュース、議員のツイート、その他のソーシャル メディアの最新情報が配信されます。議会の業務内容を扱うため、終日開いたままにすることを想定しています。

WebSocket からの開始

WebSocket の仕様は、ブラウザとサーバー間の安定した双方向 TCP ソケットを実現するという点で大きな注目を集めています。TCP ソケットにはデータ形式が課されません。開発者は自由にメッセージング プロトコルを定義できます。実際には、JSON オブジェクトを文字列として渡すのが最も便利です。ライブ更新をリッスンするクライアント側の JavaScript コードは、シンプルかつシンプルです。

var liveSocket = new WebSocket("ws://streamcongress.com:8080/live");

liveSocket.onmessage = function (payload) {
  addToStream(JSON.parse(payload.data).reverse());
};

ブラウザでの WebSocket のサポートは単純ですが、サーバーサイド サポートはまだ形成段階です。Node.js 上の Socket.IO は、最も成熟した堅牢なサーバーサイド ソリューションの一つです。Node.js のようなイベント ドリブン サーバーは WebSocket に適しています。別の実装として、Python のデベロッパーは TwistedTornado を、Ruby のデベロッパーは EventMachine を使用できます。

生理痛のご紹介

Cramp は、EventMachine 上で実行される非同期の Ruby ウェブ フレームワークです。このドキュメントは、Ruby on Rails コアチームのメンバーである Pratik Naik によって執筆されました。リアルタイム ウェブアプリ用のドメイン固有言語(DSL)を提供する Cramp は、Ruby ウェブ デベロッパーにとって理想的な選択肢です。Ruby on Rails でコントローラを作成することに精通していれば、Cramp のスタイルはご存じでしょう。

require "rubygems"
require "bundler"
Bundler.require
require 'cramp'
require 'http_router'
require 'active_support/json'
require 'thin'

Cramp::Websocket.backend = :thin

class LiveSocket < Cramp::Websocket
periodic_timer :check_activities, :every => 15

def check_activities
    @latest_activity ||= nil
    new_activities = find_activities_since(@latest_activity)
    @latest_activity = new_activities.first unless new_activities.empty?
    render new_activities.to_json
end
end

routes = HttpRouter.new do
add('/live').to(LiveSocket)
end
run routes

Cramp は非ブロックの EventMachine 上を行くため、留意すべき考慮事項がいくつかあります。

  • MySQLPlusem-mongo などの非ブロック データベース ドライバを使用する必要があります。
  • イベント ドリブンのウェブサーバーを使用する必要があります。ThinRainbow のサポートが組み込まれています。
  • Cramp アプリは、Stream Congress を強化するメインの Rails アプリとは別に実行し、個別に再起動してモニタリングする必要があります。

現在の制限事項

2010 年 12 月 8 日にセキュリティの脆弱性が公表され、WebSocket が機能不全に陥りました。Firefox と Opera はどちらもブラウザによる WebSocket のサポートを終了しました。純粋な JavaScript ポリフィルはありませんが、広く採用されている Flash の代替があります。しかし、Flash に頼ることは理想的とは言えません。Chrome と Safari は引き続き WebSocket をサポートしていますが、Flash に依存せずに最新のブラウザをすべてサポートするには、WebSocket を置き換える必要があることがわかりました。

AJAX ポーリングへのロールバック

WebSocket の使用をやめて、従来の AJAX ポーリングに戻すことが決定されました。ディスクとネットワークの I/O の観点からははるかに効率的ではありませんが、AJAX ポーリングによって Stream Congress の技術的な実装が簡素化されました。何よりも、Cramp アプリを別途必要とする必要がなくなりました。AJAX エンドポイントは Rails アプリから提供されていました。クライアント側のコードは、jQuery AJAX ポーリングをサポートするように変更されました。

var fillStream = function(mostRecentActivity) {
  $.getJSON(requestURL, function(data) {
    addToStream(data.reverse());

    setTimeout(function() {
      fillStream(recentActivities.last());
    }, 15000);
  });
};

AJAX polling, though, is not without its downsides. Relying on the HTTP request/response cycle means that the server sees constant load even when there aren't any new updates. And of course, AJAX polling doesn't take advantage of what HTML5 has to offer.

## EventSource: The right tool for the job

Up to this point, a key factor was ignored about the nature of Stream Congress: the app only needs to stream updates one way, from server to client - downstream. It didn't need to be real-time, upstream client-to-server communication. 

In this sense, WebSockets is overkill for Stream Congress. Server-to-client communication is so common that it's been given a general term: push. In fact, many existing solutions for WebSockets, from the hosted [PusherApp](http://pusherapp.com) to the Rails library [Socky](https://github.com/socky), optimize for push and don't support client-to-server communication at all.

Enter EventSource, also called Server-Sent Events. The specification compares favorably to WebSockets in the context to server to client push:

- A similar, simple JavaScript API on the browser side.
- The open connection is HTTP-based, not dropping to the low level of TCP.
- Automatic reconnection when the connection is closed.

### Going Back to Cramp

In recent months, Cramp has added support for EventSource. The code is very similar to the WebSockets implementation:

```ruby
class LiveEvents < Cramp::Action
self.transport = :sse

periodic_timer :latest, :every => 15

def latest
@latest_activity ||= nil
new_activities = find_activities_since(@latest_activity)
@latest_activity = new_activities.first unless new_activities.empty?
render new_activities.to_json
end
end

routes = HttpRouter.new do
add('/').to(LiveEvents)
end
run routes

EventSource に関して注意すべき重要な問題は、クロスドメイン接続が許可されないことです。つまり、Cramp アプリはメインの Rails アプリと同じ streamcongress.com ドメインから提供される必要があります。これは、ウェブサーバーでプロキシを使用して実現できます。Cramp アプリが Thin を搭載し、ポート 8000 で実行されていると仮定すると、Apache の構成は次のようになります。

LoadModule  proxy_module             /usr/lib/apache2/modules/mod_proxy.so
LoadModule  proxy_http_module        /usr/lib/apache2/modules/mod_proxy_http.so
LoadModule  proxy_balancer_module    /usr/lib/apache2/modules/mod_proxy_balancer.so

<VirtualHost *:80>
  ServerName streamcongress.com
  DocumentRoot /projects/streamcongress/www/current/public
  RailsEnv production
  RackEnv production

  <Directory /projects/streamcongress/www/current/public>
    Order allow,deny
    Allow from all
    Options -MultiViews
  </Directory>

  <Proxy balancer://thin>
    BalancerMember http://localhost:8000
  </Proxy>

  ProxyPass /live balancer://thin/
  ProxyPassReverse /live balancer://thin/
  ProxyPreserveHost on
</VirtualHost>

この構成では、EventSource エンドポイントを streamcongress.com/live に設定しています。

安定したポリフィル

WebSocket に対する EventSource の大きなメリットの 1 つは、フォールバックが完全に JavaScript ベースで、Flash に依存しないことです。Remy Sharp のpolyfillは、EventSource をネイティブにサポートしていないブラウザでロング ポーリングを実装することで、これを実現しています。そのため、EventSource は現在、JavaScript が有効になっている最新のブラウザで動作します。

まとめ

HTML5 は、新しくて刺激的なウェブ開発の可能性を広げます。WebSocket と EventSource により、ウェブ デベロッパーは、リアルタイムのウェブアプリを実現するためのクリーンで明確に定義された標準を手に入れました。ただし、すべてのユーザーが最新のブラウザを使用しているわけではありません。これらのテクノロジーを実装する場合は、グレースフル デグラデーションを考慮する必要があります。また、WebSocket と EventSource 用のサーバーサイドのツールはまだ初期段階です。リアルタイム HTML5 アプリを開発する場合は、これらの要素に留意することが重要です。