案例研究 - 视频流会议中的实时动态

路易吉·蒙塔内斯
Luigi Montanez

简介

借助 HTML5,开发者可以通过 WebSocketsEventSource 构建与服务器进行实时通信的 Web 应用。Stream Congress(可在 Chrome 应用商店下载)提供有关美国国会运作的实时动态。直播内容包括众议院和参议院的议程动态、相关新闻动态、国会议员的推文以及其他社交媒体动态。该应用会保持全天打开状态,因为它记录了国会的业务。

从 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 是在 EventMachine 上运行的异步 Ruby Web 框架。由 Ruby on Rails 核心团队成员 Pratik Naik 编写。Cramp 可为实时 Web 应用提供网域特定语言 (DSL),是 Ruby Web 开发者的理想选择。熟悉在 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
  • 必须使用事件驱动型 Web 服务器。内置对 ThinRainbows 的支持。
  • Cramp 应用必须与为 Stream Congress 提供支持的主 Rails 应用分开运行,并且单独重启和监控。

当前限制

2010 年 12 月 8 日,一个安全漏洞被公开,WebSocket 遭受了挫败。Firefox 和 Opera 都取消了浏览器对 WebSocket 的支持。虽然不存在纯 JavaScript polyfill,但有一个已被广泛采用的 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

稳定版 Polyfill

与 WebSocket 相比,EventSource 最显著的优势之一是后备广告完全基于 JavaScript,不依赖于 Flash。Remy Sharp 的 polyfill 通过在原生不支持 EventSource 的浏览器中实现长轮询来实现这一点。因此,EventSource 目前可在所有已启用 JavaScript 的现代浏览器中运行。

总结

HTML5 为许多令人振奋的新网页开发可能性打开了大门。借助 WebSockets 和 EventSource,网络开发者现在可以采用干净、定义明确的标准来支持实时 Web 应用。但并非所有用户都使用新型浏览器。选择实现这些技术时,必须考虑优雅降级。用于 WebSocket 和 EventSource 的服务器端工具仍处于早期阶段。开发实时 HTML5 应用时,请务必牢记这些因素。