18.2 应对不支持 WebSocket 的场景
WebSocket 是一个相对比较新的规范。虽然它早在 2011年底就实现了规范化,但即便如此,在 Web 浏览器和应用服务器上依然没有得到一致的支持。Firefox 和 Chrome 早就已经完整支持 WebSocket 了,但是其他的一些浏览器刚刚开始支持 WebSocket。如下列出了几个流行的浏览器支持 WebSocket 功能的最低版本:
Internet Explorer:10.0
Firefox: 4.0(部分支持),6.0(完整支持)。
Chrome: 4.0(部分支持),13.0(完整支持)。
Safari: 5.0(部分支持),6.0(完整支持)。
Opera: 11.0(部分支持),12.10(完整支持)。
iOS Safari: 4.2(部分支持),6.0(完整支持)。
Android Browser: 4.4。
令人遗憾的是,很多的网上冲浪者并没有认识到或理解新 Web 浏览器的特性,因此升级很慢。另外,有的公司规定使用特定版本的浏览器,这样它们的员工很难(或不可能)使用更新的浏览器。鉴于这些情况,如果你的应用程序使用 WebSocket 的话,用户可能会无法使用。
服务器端对 WebSocket 的支持也好不到哪里去。GlassFish 在几年前就开始支持一定形式的 WebSocket,但是很多其他的应用服务器在最近的版本中刚刚开始支持 WebSocket。例如,我在测试上述例子的时候,所使用的就是 Tomcat 8 的发布候选构建版本。
即便浏览器和应用服务器的版本都符合要求,两端都支持 WebSocket,在这两者之间还有可能出现问题。防火墙代理通常会限制所有除 HTTP 以外的流量。它们有可能不支持或者(还)没有配置允许进行 WebSocket 通信。
在当前的 WebSocket 领域,我也许描述了一个很阴暗的前景。但是,不要因为这一些不支持,你就停止使用 WebSocket 的功能。当它能够正常使用的时候,WebSocket 是一项非常棒的技术,但是如果它无法得到支持的话,我们所需要的仅仅是一种备用方案(fallback plan)。
幸好,提到 WebSocket 的备用方案,这恰是 SockJS 所擅长的。SockJS 是 WebSocket 技术的一种模拟,在表面上,它尽可能对应 WebSocket API,但是在底层它非常智能,如果 WebSocket 技术不可用的话,就会选择另外的通信方式。SockJS 会优先选用 WebSocket,但是如果 WebSocket 不可用的话,它将会从如下的方案中挑选最优的可行方案:
XHR 流。
XDR 流。
iFrame 事件源。
iFrame HTML 文件。
XHR 轮询。
XDR 轮询。
iFrame XHR 轮询。
JSONP 轮询。
好消息是在使用 SockJS 之前,我们并没有必要全部了解这些方案。SockJS 让我们能够使用统一的编程模型,就好像在各个层面都完整支持 WebSocket 一样,SockJS 在底层会提供备用方案。
例如,为了在服务端启用 SockJS 通信,我们在 Spring 配置中可以很简单地要求添加该功能。重新回顾一下程序清单 18.2 中的 registerWebSocketHandlers() 方法,稍微加一点内容就能启用 SockJS:
addHandler() 方法会返回 WebSocketHandlerRegistration,通过简单地调用其 withSockJS() 方法就能声明我们想要使用 SockJS 功能,如果 WebSocket 不可用的话,SockJS 的备用方案就会发挥作用。
如果你使用 XML 来配置 Spring 的话,启用 SockJS 只需在配置中添加 <websocket:sockjs> 元素即可:
要在客户端使用 SockJS,需要确保加载了 SockJS 客户端库。具体的做法在很大程度上依赖于使用 JavaScript 模块加载器(如 require.js 或 curl.js)还是简单地使用标签加载 JavaScript 库。加载 SockJS 客户端库的最简单办法是使用标签从 SockJS CDN 中进行加载,如下所示:
用 WebJars 解析 Web 资源
在我的样例代码中,使用了 WebJars 来解析 JavaScript 库,使其作为项目 Maven 或 Gradle 构建的一部分,就像其他的依赖一样。为了支持该功能,我在 Spring MVC 配置中搭建了一个资源处理器,让它负责解析路径以 “/webjars/**” 开头的请求,这也是 WebJars 的标准路径:
在这个资源处理器准备就绪后,我们可以在 Web 页面中使用如下的 <script> 标签加载 SockJS 库:
注意,这个特殊的标签来源于一个 Thymeleaf 模板,并使用 “@{...}” 表达式来为 JavaScript 文件计算完整的相对于上下文的 URL 路径。
除了加载 SockJS 客户端库以外,在程序清单 18.4 中,要使用 SockJS 只需修改两行代码:
所做的第一个修改就是 URL。SockJS 所处理的 URL 是 “http://” 或 “https://” 模式,而不是 “ws://” 和 “wss://”。即便如此,我们还是可以使用相对 URL,避免书写完整的全限定 URL。在本例中, 如果包含 JavaScript 的页面位于“http://localhost:8080/websocket” 路径下,那么给定的 “marco” 路径将会形成到“http://localhost:8080/websocket/marco” 的连接。
但是,这里最核心的变化是创建 SockJS 实例来代替 WebSocket。因为 SockJS 尽可能地模拟了 WebSocket,所以程序清单 18.4 中的其他代码并不需要变化。相同的 onopen、onmessage 和 onclose 事件处理函数用来响应对应的事件,相同的 send() 方法用来发送 “Marco!” 到服务器端。
我们并没有改变很多的代码,但是客户端-服务器之间通信的运行方式却有了很大的变化。我们可以完全相信客户端和服务器之间能够进行类似于 WebSocket 这样的通信,即便浏览器、服务器或位于中间的代理不支持 WebSocket,我们也无需再担心了。
WebSocket 提供了浏览器-服务器之间的通信方式,当运行环境不支持 WebSocket 的时候,SockJS 提供了备用方案。但是不管哪种场景,对于实际应用来说,这种通信形式都显得层级过低。让我们看一下如何在 WebSocket 之上使用 STOMP(Simple Text Oriented Messaging Protocol),为浏览器-服务器之间的通信增加恰当的消息语义。
Last updated