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

Luigi Montanez
Luigi Montanez

简介

借助 WebSocketEventSource,HTML5 可让开发者构建可与服务器实时通信的 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 是一个异步 Ruby Web 框架,可在 EventMachine 之上运行。本文档由 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 服务器。内置了对细线彩虹的支持。
  • Cramp 应用必须与为 Stream Congress 提供支持的主 Rails 应用分开运行,并单独重启和监控。

当前限制

2010 年 12 月 8 日,WebSockets 遭遇了挫折,因为一个安全漏洞被公开。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 网域提供。这可以通过 Web 服务器上的代理实现。假设 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

EventSource 相较于 WebSocket 的最大优势之一是,回退完全基于 JavaScript,不依赖于 Flash。Remy Sharp 的 polyfill 通过在原生不支持 EventSource 的浏览器中实现长轮询来实现此目的。因此,EventSource 目前可在所有已启用 JavaScript 的新型浏览器中使用。

总结

HTML5 为许多令人兴奋的全新网络开发打开了大门。借助 WebSocket 和 EventSource,Web 开发者现在可以使用清晰、明确的标准来实现实时 Web 应用。但并不是所有用户都运行现代浏览器。在选择实现这些技术时,必须考虑优雅降级。服务器端的 WebSockets 和 EventSource 工具仍处于早期阶段。在开发实时 HTML5 应用时,请务必考虑这些因素。