WebSocket 调研总结

本文是将我 confluence 上的文章搬运过来,侧重于前后端实时通讯的横向比较以及 Spring-boot 资源的整理。

前后端实时通讯解决方案比较

Web 为了实现即时通信,有以下几种方案,HTTP short-polling, HTTP long-polling, Streaming, Server-sent Events (SSE), WebSockets。下面会介绍以及比较各种方案的优劣。

HTTP short-polling

每隔一段时间就自动发出一个 HTTP 请求给服务器。

这个是最老旧的方式,导致连接数会很多,并且每次发送的请求都会有 HTTP 的 Header,消耗流量。很显然对于服务器的压力大。

HTTP long-polling

长轮询是对短轮询的改进版,客户端发送 HTTP 给服务器之后,如果没有消息就会阻塞到有消息或者超时了,才会返回给客户端。消息返回之后再建立连接,如此反复。

在某种程度上减小了网络带宽和 CPU 利用率等问题。这种方式会导致实时性不高,并且网络带宽利用率低的问题也没有从根源上解决。每个 Request 都会带相同的 Header。

Streaming

Iframe Streaming

在页面中插入一个隐藏的 iframe, 利用其 src 属性在服务器和客户端之间创建一条长链接,服务器向 iframe 传输数据。

实现简单,但无法知道连接状态。

AJAX multipart streaming (XHR Streaming)

实现思路:浏览器必须支持 multi-part 标志,客户端通过 AJAX 发出请求 Request,服务器保持住这个连接,然后可以通过 HTTP1.1 的 chunked encoding 机制(分块传输编码)不断 push 数据给客户端,直到 timeout 或者手动断开连接。

客户端一次连接,服务器数据可多次推送。但是并非所有浏览器都支持 XHR Streaming。

Flash Streaming

通过内嵌使用了 Socket 类的 Flash 程序,JavaScript 通过调用此 Flash 程序提供的 Socket 接口与服务器端的 Socket 接口进行通信,JavaScript 通过 Flash Socket 接收到服务器端传送的数据。

缺点很明显,要装 Flash.

Server-Sent Events

服务器发送事件(SSE)也是 HTML5 公布的一种服务器向浏览器客户端发起数据传输的技术。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如自动重新连接、事件 ID 以及发送任意事件的能力。

SSE 就是利用服务器向客户端声明,接下来要发送的是流信息(streaming),会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,可以类比视频流。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

必须将 Content-Type 指定为 event-steam。

SSE 的优势

  • 通过简单的 HTTP 传输,而不是自定义协议。
  • Can be poly-filled with JS to “backport” SSE to browsers that do not support it yet
  • 支持重连

    SSE 的劣势

  • 不支持二进制传输

  • 最大连接限制(每个浏览器仅限 6 个,并且不将会在 Chrome 和 Firefox 中修复)
  • 单向

WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。—— By Wikipedia

过去,创建需要在客户端和服务之间双向通信的web应用,需要通过HTTP来轮询服务器来获取更新然后如果是推送消息则发送另一个请求。这样做会存在一些问题。

  • 服务器端被迫提供两类接口,一类提供给客户端轮询新消息,一类提供给客户端推送消息给服务器端。
  • HTTP协议有较多的额外费用 (overhead),每次发送消息都会有一个 HTTP header 信息,而且如果不用 Keep-Alive 每次还都要握手。
  • 客户端的脚本比如JS可能还需要跟踪整个过程,也就是说我发送一个消息后,我可能需要跟踪这个消息的返回。
    一个简单的办法是使用单个 TCP 连接双向传输。这是为什么提供 WebSocket 协议。与 WebSocket API 结合,它提供了一个替代 HTTP 轮询的方式从 web 页面到远程服务器的双向通信。

一个简单的办法是使用单个 TCP 连接双向传输。这是为什么提供 WebSocket 协议。与 WebSocket API 结合,它提供了一个替代 HTTP 轮询的方式从 web 页面到远程服务器的双向通信。

Supported transports, by browser (html served from http:// or https://)

Browser Websockets Streaming Polling
IE 6, 7 no no jsonp-polling
IE 8, 9 (cookies=no) no xdr-streaming † xdr-polling †
IE 8, 9 (cookies=yes) no iframe-htmlfile iframe-xhr-polling
IE 10 rfc6455 xhr-streaming xhr-polling
Chrome 6-13 hixie-76 xhr-streaming xhr-polling
Chrome 14+ hybi-10 / rfc6455 xhr-streaming xhr-polling
Firefox <10 no ‡ xhr-streaming xhr-polling
Firefox 10+ hybi-10 / rfc6455 xhr-streaming xhr-polling
Safari 5.x hixie-76 xhr-streaming xhr-polling
Safari 6+ rfc6455 xhr-streaming xhr-polling
Opera 10.70+ no ‡ iframe-eventsource iframe-xhr-polling
Opera 12.10+ rfc6455 xhr-streaming xhr-polling
Konqueror no no jsonp-polling

WebSocket vs SSE

实际上,所有能够用 SSE 实现的都可以用 WebSockets 替代,WebSockets 受到了更多的青睐并且也有更多的浏览器的支持。

但是 SSE 可以解决一些特定的业务并且后端也容易实现,SSE 也可以通过 JS polyfilled 不支持 SSE 的老浏览器。详见:Modernizr github page

所以应该对应不同的场景使用不同的技术。

SSE 使用场景:

  • Stock ticker streaming
  • twitter feed updating
  • Notifications to browser

WebSocket Protocol

协议全文详见:RFC 6455

WebSocket 协议主要分为两个部分,一个是握手的规则,另一个是数据传输。

握手

客户端发起请求,采用标准的 HTTP 报文,且仅支持 GET 方法,通过 HTTP Upgrade 机制升级协议至 WebSocket。这样的话只要使用 80/443 端口就可以了。

  • Sec-WebSocket-Version
    客户端表明自己想要使用的版本号(一般都是 13 号版本),如果服务器不支持这个版本,则需要返回自己支持的版本。客户端拿到 Response 以后,需要对自己支持的版本号重新握手。这个 header 客户端必须要发送。

  • Sec-WebSocket-Key

    客户端请求自动生成的一个 key。这个 header 客户端必须要发送。

  • Sec-WebSocket-Accept

    服务器针对客户端的 Sec-WebSocket-Key 计算的响应值。这个 header 服务端必须要发送。

  • Sec-WebSocket-Protocol

    用于协商应用子协议:客户端发送支持的协议列表,服务器必须只回应一个协议名。如果服务器一个协议都不能支持,直接握手失败。客户端可以不发送子协议,但是一旦发送,服务器无法支持其中任意一个都会导致握手失败。这个 header 客户端可选发送。

  • Sec-WebSocket-Extensions

    用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一或多个扩展。这个 header 客户端可选发送。服务端如果都不支持,不会导致握手失败,但是此次连接不能使用任何扩展。

WebSocket 协议扩展

  • 多路复用扩展(A Multiplexing Extension for WebSockets)
    这个扩展可以通过握手时建立的 TCP 连接建立多条独立的逻辑上的 WebSocket ( logical connection )通道,每个虚拟的 WebSocket 通道以非零的 ID 标注,0 号通道为控制通道。

    在 header 里添加 “Sec-WebSocket-Extensions: mux”开启多路复用扩展。在实际上的 WebSocket 连接建立的时候就会自动多开启一条 multiplexed connection。只有客户端通过 AddChannelRequest 才能够创建新的 logical WebSocket 连接。这样接受者就能 ( May ) 并行处理不同通道发送过来的 frames 了。

    每个通道(除了控制通道)会分配一个定额来限制(默认 64 KB)每个 frame 的 Payload 大小。详文:A Multiplexing Extension for WebSockets

  • 压缩扩展(Compression Extensions fos’sr WebSocket)
    给 WebSocket 协议增加了压缩功能。(例如 x-webkit-deflate-frame 扩展)

如果不进行多路复用扩展,每个 WebSocket 连接都只能独享专门的一个 TCP 连接,而且当遇到一个巨大的消息分成多个帧的时候,容易产生队首阻塞的情况。队首阻塞会导致延迟,所以分成多个帧能尽量的小是关键。不过在进行了多路复用扩展以后,多个连接复用一个 TCP 连接,每个信道依旧会存在队首阻塞的问题。除了多路复用,还要进行多路并行发送消息。

如果通过 HTTP2 进行 WebSocket 传输,性能会更好一点,毕竟 HTTP2 原生就支持了流的多路复用。利用 HTTP2 的分帧机制进行 WebSocket 的分帧,多个 WebSocket 可以在同一个会话中传输。

数据格式

由一个或多个 Frame 组成。

frame 格式以及每个字段具体含义请参阅 RFC 文档。

数据传递

一旦 WebSocket 客户端,服务端建立连接后,后续的操作都是基于数据帧的传递。

WebSocket 根据 opcode 来区分操作的类型。

连接保持 + 心跳

客户端与服务端之间长时间没有数据往来,但仍然需要保持连接。这个时候,可以采用心跳来实现。

ping
pong
ping, pong 对应的 opcode 分别是 0x9, 0xA。

关闭连接

发送或接受一个 close 的 opcode 0x8. 启动 WebSocket 关闭阶段。如果一个终端在收到 close frame 之前没有发出 close frame, 那么这个终端就必须返回一个 close frame. 如果当前有正在传输的 frame, 那么 close frame 将会在这个 frame 传输结束后再发出。并不能保证终端在发出 close frame 后还能传输数据。

WebSocket 和 Proxy

详文:【HTML5 Web Sockets 与代理服务器交互](http://www.infoq.com/cn/articles/Web-Sockets-Proxy-Servers)

proxy

可见全部走 secure 肯定是没问题的,使用 https 绕过中间代理,非 https 会导致 http 消息头丢失导致握手升级失败。

STOMP

协议详见:STOMP

STOMP 是属于消息队列的一种协议,很多 MQ 都已经支持。为浏览器和服务器之间的通信增加适当的消息语义。高层协议便于定义应用间所发送消息的语义。WebSocket 协议过于底层,并没有规定 payload 格式,而对于 Pub/Sub 模式,使用 STOMP 最合适不为过了。

STOMP Frame 格式

COMMAND
header1 : value1
header2 : value2
Body^@

STOMP 的优势:

Use of STOMP as a sub-protocol enables the Spring Framework and Spring Security to provide a richer programming model vs using raw WebSockets.

SockJS

详情:SockJs ( Github – The MIT License )

SockJS 是一个 JS 库,它提供了一个类似于 WebSocket 的东西,SockJS 提供了一个连贯的、跨浏览器的 JS API, 他在浏览器和服务器之间创建了一个低延迟、全双工、跨域通信的通道。

SockJS 的一大好处在于提供了浏览器兼容性,优先使用 Native WebSocket, 在不支持 WebSocket 的浏览器中会自动降为各个浏览器专门的方式(streaming or polling)。一些浏览器缺少对于 WebSocket的支持,因此,回退选项是必要的,Spring 框架也提供了基于 SockJS 协议的透明回退选项。

SockJS 限制,SockJS 不允许对于一个域名同时打开多个 SockJS 连接。原因是 browsers don’t allow opening more than two outgoing connections to a single domain. 单个 SockJS session 需要两个连接——一个是下载数据,一个是发送信息。同时打开第二个 SockJS session 会阻塞。解决方法:使用子域名。

SockJS 也支持了 multiplex extension :sockjs websocket-multiplex, How to do proper multiplexing on WebSockets or on SockJS

Spring

详文见:spring framework 5.0.0.BUILD-SNAPSHOT

spring 4.x 就已经支持 WebSocket, STOMP, SockJS.

Message Queue

RabbitMQ

RabbitMQ Web STOMP Plugin

Kafka

自有一套协议

ActiveMQ

ActiveMQ STOMP Protocol Support