SockJS - WebSocket 模拟
WebSocket 技术正在普及,但所有浏览器都支持它还需要一段时间。与此同时,有很多项目旨在替代 WebSockets,并为 Web 应用提供“实时”功能。但是所有尝试都只解决了一部分通用问题,并没有任何单一解决方案有效、可扩展,并且不需要特殊的部署技巧。
这就是为什么一个新项目 诞生了:SockJS - 又一个 WebSocket 模拟库,但这一次做得很好。SockJS 拥有雄心勃勃的目标
- 简单易用的浏览器端和服务器端 API,尽可能接近 WebSocket API。
- 完善的扩展和负载均衡技术文档。
- 传输必须完全支持跨域通信。
- 传输必须在遇到限制性代理时优雅地回退。
- 连接建立应快速完成。
- 客户端侧不使用 Flash,只使用 Javascript。
- 客户端 Javascript 必须经过相当充分的测试。
- 此外,服务器端代码应该简单,以降低为不同语言编写服务器的成本。
简单 API
这听起来可能很明显,但 WebSocket API 实际上相当不错。这是 Ian Hickson 和其他人领导的巨大努力的结果。我们不应忘记,以前曾经有不太成功的尝试 实现类似的事情 - WebSocket API 并不是在真空中开发的。
然而,我还没有看到任何试图紧密模拟此 API 的 Javascript 库。早期的 Socket.io 尝试过这样做,但现在已经发展得相当不同了。
WebSocket 没有定义服务器端 API,但很容易想出一个与客户端侧类似的思想和抽象的方案。
部署故事
SockJS 原生支持跨域通信。您可以(也应该)将 SockJS 服务器隔离并托管在与您的主要网站不同的域上。这种方法有很多优点,坦率地说,这是唯一合理的部署策略。
负载均衡故事
单个 SockJS 服务器的容量是有限的。如果您预计一台服务器不足以满足您的需求 - 请查看下面的扩展场景。
为 SockJS 服务器使用多个域
最简单的解决方案是将每个 SockJS 服务器放在不同的域名下,例如 sockjs1.example.com
和 sockjs2.example.com
,并允许客户端随机选择服务器。
使用支持 WebSocket 的负载均衡器
您可以选择将所有 SockJS 流量托管在一个域下,并使用一个不错的支持 WebSocket 的负载均衡器来拆分流量。有一个 HAProxy 配置文件示例,它可以作为良好的起点。
使用几乎任何负载均衡器
这不是首选解决方案,但在负载均衡器不支持 WebSockets 的环境中,仍然可以运行可扩展的 SockJS。共享托管提供商就是这样 - 例如 CloudFoundry。为了使连接建立更快,您可以在客户端和服务器端都禁用 WebSocket 协议。在这种环境中,负载均衡器必须将单个 SockJS 会话的所有请求转发到单个 SockJS 服务器 - 负载均衡器必须支持两种变体之一的粘性会话(会话亲和性)
- 基于前缀的粘性会话。发送到 SockJS 的所有请求都以会话 ID 为前缀。好的负载均衡器可以将此作为会话亲和性算法的线索(例如 HAProxy 可以做到这一点)。
JSESSIONID
cookie 粘性会话。默认情况下,SockJS 服务器会设置此 cookie。一些负载均衡器了解此 cookie 并启用会话粘性(例如,这是 CloudFoundry 的情况)。
强大的传输协议
除了原生的 WebSockets,SockJS 还支持几种精心选择的传输协议,它们都支持跨域通信。基本思想是,每个浏览器都应该有一个合适的流式传输和轮询协议。轮询协议必须在具有限制性代理的环境中工作,并支持旧浏览器。每种浏览器都有三种方式可以建立连接
原生 WebSocket
WebSocket 是最快、最好的传输协议,它原生支持跨域连接。不幸的是,它还没有被浏览器广泛支持。此外,某些浏览器可能在使用代理时遇到问题(例如,Firefox WebSocket 实现无法通过大多数代理工作)。浏览器供应商达成协议并处理代理问题还需要一些时间。
流式传输协议
SockJS 支持的流式传输协议基于 http 1.1 分块 - 它允许浏览器将单个 http 响应分成多个部分接收。流式传输协议的一个很好的例子是 EventSource 或通过 XHR(ajax)进行流式传输。从浏览器发送的消息使用另一个 XHR 请求发布。
每个浏览器都支持不同的流式传输协议集,并且它们通常无法进行跨域通信。幸运的是,SockJS 可以通过使用 Iframe 并使用 Html5 PostMessage API 与其通信来解决此限制。这非常复杂,但幸运的是,大多数浏览器都支持它(IE7 除外)。
轮询传输
SockJS 为旧浏览器(包括 IE7)支持几种旧的轮询协议。不幸的是,这些技术非常慢,但我们对此无能为力。
轮询传输也可以在客户端侧代理不支持 WebSockets 或 http 分块的情况下使用 - 这是流式传输协议所必需的。
连接建立应快速完成
打开 SockJS 连接应该很快,在某些部署中,可能需要在用户访问的每个 http 页面上建立 SockJS 连接。
如果浏览器支持,SockJS 会首先尝试打开原生 WebSocket 连接。根据网络和服务器设置,它可能会成功或失败。失败应该很快发生,除非客户端位于行为异常的代理后面 - 在这种情况下,可能需要长达 5 秒才能超时。
在排除 WebSocket 传输后,SockJS 会打开一个 XHR 请求,旨在检查代理是否支持分块。遇到不支持 http 分块的代理并不罕见。在这种环境中运行流式传输协议会导致超时。如果分块正常工作,SockJS 会选择浏览器支持的最佳流式传输协议。否则,将使用轮询传输。
所有这些,根据浏览器,可能需要 3 或 4 次从浏览器到服务器的往返时间,再加上一个 DNS 请求。除非您位于代理故障或南极洲,否则它应该相当快。
这是 SockJS 避免使用 Flash 传输的原因之一 - 如果端口 843 被阻止,Flash 连接至少需要 3 秒。
客户端 Javascript 必须经过相当充分的测试
SockJS 比较年轻,测试还没有完成。也就是说,我们有多个端到端 QUnit 测试。目前,它们部署在几个地方
- https://sockjs.popcnt.org/(托管在欧洲)
- https://sockjs.cloudfoundry.com/(CloudFoundry,websockets 已禁用,负载均衡)
- https://sockjs.cloudfoundry.com/(CloudFoundry SSL,websockets 已禁用,负载均衡)
- https://sockjs.herokuapp.com/(Heroku,websockets 已禁用)
服务器端代码应该简单
目前,SockJS-node 实现使用大约 1200 行 CoffeeScript 代码。大约 340 行用于 WebSocket 协议,220 行用于简单的 http 抽象,只有大约 230 行用于核心 SockJS 逻辑。
浏览器和服务器之间使用的 SockJS 协议已经相当简单,我们正在努力使其更加清晰。
我们确实打算至少支持 Node 和 Erlang 服务器,我们很高兴看到 Python 和 Ruby 实现。SockJS 的目标是多语言。
总结
SockJS 仍处于起步阶段,还有很多工作要做,但我们相信它已经足够稳定,可以用于实际应用程序。如果您计划开发实时 Web 应用程序,不妨试一试!(文章也发布在 github 页面)