個案研究 - 串流會議的即時更新

路易吉蒙塔尼茲
Luigi Montanez

引言

HTML5 可讓開發人員透過 WebSocketsEventSource,建立能夠與伺服器即時通訊的網頁應用程式。Stream Congress (可於 Chrome 線上應用程式商店取得) 提供美國國會作品的即時最新資訊。它會串流播放 House 和 Senate 的樓層動態、相關新聞動態、國會成員的推文,以及其他社群媒體的最新動態。這款應用程式必須全天保持開啟,因為它會呈現國會事業。

從 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 隆重登場

Cramp 是非同步的 Ruby 網路架構,在 EventMachine 中運作。是 Ruby on Rails 核心團隊成員 Pratik Naik 編寫的程式。Cramp 是為即時網頁應用程式提供特定網域專屬語言 (DSL),對 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
  • 且必須使用事件導向網路伺服器。內建細胞彩虹
  • Cramp 應用程式必須獨立於支援串流會議的 Rails 應用程式之外執行,並獨立重新啟動及監控。

目前限制

WebSocket 於 2010 年 12 月 8 日公布安全漏洞時遇到了逆向問題。Firefox 和 Opera 已移除 WebSocket 的瀏覽器支援。雖然目前沒有純 JavaScript Polyfill,但也有廣泛採用的 Flash 備用廣告。不過,透過 Flash 廣告素材並不是理想的做法。即使 Chrome 和 Safari 繼續支援 WebSocket,仍顯而易見地,在不使用 Flash 的情況下,就能支援所有新型瀏覽器,而 WebSocket 仍需要更換。

復原為 AJAX 輪詢

最終決定淘汰 WebSocket,並改用「復古」AJAX 輪詢作業。雖然從磁碟和網路 I/O 的效率大幅降低,但是 AJAX 輪詢讓您可以簡化串流會議的技術實作。最重要的是,他們不再需要另外使用 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 網域提供。這項作業可在網路伺服器中透過 Proxy 完成。假設 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 端點。

穩定 Polyfill

與 WebSocket 相比,EventSource 的最大優點之一,就是完全以 JavaScript 為基礎,不依賴 Flash。Remy Sharp 的 polyfill 可以藉由在原生不支援 EventSource 的瀏覽器中實作長時間輪詢來完成。因此,EventSource 現在可以適用於所有啟用 JavaScript 的新版瀏覽器。

結語

HTML5 將開啟許多新奇有趣的網頁開發方式。有了 WebSocket 和 EventSource,網頁程式開發人員現在都已採用簡潔且定義完善的標準來啟用即時網頁應用程式。但並非所有使用者都採用新式瀏覽器。選擇實作這些技術時,必須考慮優雅降級。WebSocket 和 EventSource 的伺服器端工具仍處於早期階段。開發即時 HTML5 應用程式時,請務必考量這些因素。