跳至主内容

SockJS - WebSocket 模拟

·阅读 7 分钟
Marek Majkowski

WebSocket 技术正在快速发展,但要等到所有浏览器都支持还需要一段时间。在此期间,有大量的项目旨在替代 WebSockets 并为 Web 应用程序启用“实时”功能。但所有这些尝试只解决了通用问题的一部分,而且没有一个单一的解决方案是有效的、可扩展的且不需要特殊部署技巧的。

这就是一个新项目诞生的原因SockJS —— 又是另一个 WebSocket 仿真库,但这一次我们做对了。SockJS 有着宏大的目标:

  • 简洁的浏览器端和服务器端 API,尽可能贴近 WebSocket API。
  • 文档完善的扩展和负载均衡技术。
  • 传输协议必须完全支持跨域通信。
  • 传输协议在遇到限制性代理时必须能够优雅地降级。
  • 连接建立过程应当快速。
  • 客户端不使用 Flash,仅使用 Javascript。
  • 客户端 Javascript 必须经过相当充分的测试。
  • 此外,服务器端代码应当简单,以降低为不同语言编写服务器的成本。

简洁的 API

这听起来可能显而易见,但 WebSocket API 实际上非常出色。它是 Ian Hickson 等人付出巨大努力的成果。不应忘记,此前也曾有过一些不那么成功的尝试来实现类似的功能 —— WebSocket API 并非凭空产生。

然而,我还没有看到任何 Javascript 库尝试紧密地仿真该 API。早期的 Socket.io 曾尝试这样做,但时至今日,它已经演变得相当遥远了。

WebSocket 没有定义服务器端 API,但完全可以构思出一套与客户端具有相似理念和抽象的方案。

部署方案

SockJS 开箱即用地支持跨域通信。你可以也应该将 SockJS 服务器与你的主网站隔离,托管在不同的域名下。这种方法有多个优点,坦率地说,这是唯一合理的部署策略。

负载均衡方案

单台 SockJS 服务器的容量是有限的。如果你预计单台服务器无法满足你的需求,请查看下方的扩展场景。

为 SockJS 服务器使用多个域名

最简单的解决方案就是将每个 SockJS 服务器放在不同的域名下,例如 sockjs1.example.comsockjs2.example.com,并允许客户端随机选择服务器。

使用支持 WebSocket 的负载均衡器

你可以选择将所有 SockJS 流量托管在一个域名下,并使用一个优秀的、支持 WebSocket 的负载均衡器来分配流量。这里有一个 HAProxy 示例配置文件,可以作为一个很好的起点。

使用几乎任何负载均衡器

这不是首选方案,但在负载均衡器不支持 WebSocket 的环境中运行可扩展的 SockJS 也是可能的。共享托管服务提供商通常就是这种情况,例如 CloudFoundry。为了让连接建立得更快,你可以在客户端和服务器端禁用 WebSocket 协议。在这种环境下,负载均衡器必须将单个 SockJS 会话的所有请求转发到单个 SockJS 服务器 —— 负载均衡器必须以以下两种变体之一支持粘性会话(会话亲和性):

  • 基于前缀的粘性会话。所有发往 SockJS 的请求都以会话 ID 为前缀。优秀的负载均衡器可以将其作为会话亲和性算法的依据(例如 HAProxy 可以做到)。
  • JSESSIONID cookie 粘性会话。默认情况下,SockJS 服务器会设置此 cookie。一些负载均衡器能够识别该 cookie 并启用会话粘性(例如 CloudFoundry 的情况即是如此)。

强大的传输协议

除了原生的 WebSocket,SockJS 还支持几种精心挑选的传输协议,所有这些协议都支持跨域通信。其基本理念是:为每种浏览器提供一种合适的流式传输协议和轮询协议。轮询协议必须能在受限的代理环境中工作并支持旧版浏览器。每种浏览器建立连接的方式有三种:

原生 WebSocket

WebSocket 是最快且最好的传输协议,它开箱即用地支持跨域连接。遗憾的是,它尚未得到浏览器的广泛支持。此外,某些浏览器在代理方面可能会遇到问题(例如,Firefox 的 WebSocket 实现无法通过大多数代理工作)。浏览器厂商就协议和代理处理达成一致还需要一些时间。

流式传输协议

SockJS 支持的流式传输协议基于 http 1.1 分块(chunking)—— 它允许浏览器分多次接收单个 http 响应。流式传输协议的一个典型例子是 EventSource 或基于 XHR (ajax) 的流式传输。从浏览器发送的消息通过另一个 XHR 请求提交。

每种浏览器支持的流式协议集合不同,且它们通常无法实现跨域通信。幸运的是,SockJS 能够通过使用 Iframe 并利用 Html5 PostMessage API 与其通信来规避这一限制。这虽然比较复杂,但幸运的是,大多数浏览器都支持(IE7 除外)。

轮询传输

SockJS 为古老的浏览器(包括 IE7)提供了一些经典的轮询协议支持。遗憾的是,这些技术相当缓慢,但目前对此也没有太多办法。

轮询传输也可以用于客户端代理不支持 WebSocket 也不支持 http 分块的情况——而流式协议需要这些支持。

连接建立应当快速

打开 SockJS 连接应当迅速,在某些部署中,可能需要在用户访问的每个 http 页面上建立 SockJS 连接。

如果浏览器支持,SockJS 首先会尝试打开原生 WebSocket 连接。根据网络和服务器设置,它可能会成功或失败。失败应该发生得相当快,除非客户端位于行为异常的代理之后——在这种情况下,超时可能需要长达 5 秒。

在排除 WebSocket 传输后,SockJS 会发起 XHR 请求,旨在检查代理是否支持分块。遇到不支持 http 分块的代理并不罕见。在这种环境下运行流式协议会因超时而失败。如果分块工作正常,SockJS 会选择浏览器支持的最佳流式传输协议。否则,将使用轮询传输。

根据浏览器的不同,上述过程可能需要浏览器到服务器的 3 到 4 次往返时间,再加上一次 DNS 请求。除非你身处破损的代理之后或居住在南极洲,否则它应该会相当快。

这也是 SockJS 避免使用 Flash 传输的原因之一 —— 如果 843 端口被阻塞,Flash 连接可能需要至少 3 秒

客户端 Javascript 必须经过相当充分的测试

SockJS 还比较年轻,测试尚未完全完善。尽管如此,我们拥有多个端到端的 QUnit 测试。目前,它们已部署在以下几个地方:

服务器端代码应当简单

目前,SockJS-node 的实现使用了大约 1200 行 CoffeeScript 代码。其中约 340 行用于 WebSocket 协议,220 行用于简单的 http 抽象,只有约 230 行用于核心 SockJS 逻辑。

浏览器和服务器之间使用的 SockJS 协议本身已经相当简单,我们正在努力使其更加直观。

我们确实打算至少支持 Node 和 Erlang 服务器,并且也非常乐意看到 Python 和 Ruby 的实现。SockJS 旨在成为一种多语言兼容的解决方案。

总结

SockJS 还很年轻,还有大量工作要做,但我们相信它对于实际应用而言已经足够稳定。如果你计划开发实时 Web 应用,不妨尝试一下!(文章也发布在 github pages

© . This site is unofficial and not affiliated with VMware.