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

Luigi Montanez
Luigi Montanez

簡介

開發人員可透過 WebSocketsEventSource,建立能夠與伺服器即時通訊的網頁應用程式。Stream Congress (可在 Chrome 線上應用程式商店中取得) 會即時更新美國國會的運作情形。它能串流播放 House 和參議院的樓層更新、相關新聞快報、美國國會成員的推文,以及其他社群媒體動態。這個應用程式會記錄國會的業務,因此應全天開啟。

開始使用 WebSocket

WebSockets 規格因其可在瀏覽器和伺服器之間建立穩定的雙向 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 撰寫。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
  • 必須使用事件導向網路伺服器,內建支援 ThinRainbows
  • Cramp 應用程式必須與負責 Stream Congress 的主要 Rails 應用程式分開執行,並且可獨立重新啟動及監控。

目前限制

2010 年 12 月 8 日,WebSockets 因安全漏洞遭到公開而受到挫折。Firefox 和 Opera 都已移除瀏覽器對 WebSocket 的支援。雖然沒有純 JavaScript Polyfill,但有個 Flash 備用方案已被廣泛採用。然而,對 Flash 的看法卻不盡理想。雖然 Chrome 和 Safari 仍會繼續支援 WebSocket,但為了不依賴 Flash 就能支援所有現代瀏覽器,我們必須更換 WebSocket。

復原至 AJAX 輪詢

結果是決定捨棄 WebSockets 並改回「舊型」的 Vertex 輪詢。雖然從磁碟和網路 I/O 的角度來看,AJAX 輪詢的效率不高,但它簡化了 Stream Congress 的技術實作。最重要的是,我們捨棄了對單獨的 Cramp 應用程式的必要性。而是由 Rails 應用程式提供 AJAX 端點。用戶端程式碼已修改為支援 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 端點。

穩定版聚酯纖維

EventSource 相較於 WebSocket 的一大優勢,就是備用方案完全以 JavaScript 為基礎,不需仰賴 Flash。Remy Sharp 的polyfill 在原生不支援 EventSource 的瀏覽器中實作長輪詢,以達成這項目標。因此,EventSource 目前可在所有已啟用 JavaScript 的新版瀏覽器上運作。

結論

HTML5 為網頁開發人員開啟了許多嶄新且令人期待的可能性。有了 WebSockets 和 EventSource,網路開發人員現在就能使用簡潔且明確定義的標準,實現即時網路應用程式。但並非所有使用者都會使用新式瀏覽器。選擇導入這些技術時,請務必考量降級功能。而伺服器端的 WebSockets 和 EventSource 工具仍處於初期階段。開發即時 HTML5 應用程式時,請務必考量這些因素。