ケーススタディ - 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 は、最も成熟した堅牢なサーバーサイド ソリューションの一つです。WebSocket には、Node.js などのイベント ドリブン サーバーが適しています。他の実装として、Python デベロッパーは TwistedTornado を使用できます。Ruby デベロッパーは EventMachine を使用できます。

Cramp のご紹介

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 などのノンブロッキング データベース ドライバを使用する必要があります。
  • イベント ドリブンのウェブサーバーを使用する必要があります。ThinRainbows のサポートが組み込まれています。
  • 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>

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

安定したポリフィル

WebSocket と比較した EventSource の最も重要な利点の 1 つは、フォールバックが完全に JavaScript ベースであり、Flash に依存しないことです。Remy Sharp の ポリフィルは、EventSource をネイティブにサポートしていないブラウザにロングポーリングを実装することで、この問題を解決します。そのため、EventSource は JavaScript が有効になっているすべての最新ブラウザで機能します。

まとめ

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