WebSocket简介与最佳实践

4,949 阅读9分钟

一、背景:源于一个需求

需求:“客户端扫二维码,将客户端参数信息展示在web端”

问题焦点:实时的web应用,Client 跟 Server 之间,实时的双向通信。


二、传统解决方案?

轮询(Polling):又称定期轮询

Client 定期向 Server 发送请求,以此保持与 Server 端数据的同步。典型应用场景:

  • Ajax技术,局部刷新Web页面

缺点:

  • 带宽和 CPU 资源:由于 Client 定期向 Server 发送请求,当 Server 端没有数据更新时,Client仍旧发送请求,这造成带宽的浪费以及Server端CPU的耗费

  • 实时性:轮询间隔内,会有数据延迟


长轮询(Long Polling):对普通轮询的改进和提高

目标:节省带宽,降低无效的网络传输。

基本原理:

  • 保持连接:HTTP 层,保持连接,Server 接收到 Client 的请求之后,如果没有数据更新,则连接保持一段时间;

  • 直到有数据更新或者连接超时,这样可以减少无效的 Client 与 Server 之间的交互;

  • 通过保持连接,减少 Request 和 Response 的数量,节省带宽;

缺陷:

  • 节省带宽,效果有限:HTTP的数据包HEAD部分数据量很大(400+Byte),但真正有效的数据很少(10Byte),这样的数据包在网络中周期传输,浪费带宽。

  • 数据更新频繁场景下,数据实时性:当Server端数据频繁更新时,Server端必须等待下一个请求到来,才能发送更新的数据,这中间的延迟最高为 1.5 x RTT(往返时间)

  • 网路拥塞场景下,等待时间更久,因为需要重新建立连接;


本质原因:

  • 连接保持:需要重新建立 HTTP 连接(HTTP 1.1 只能缓解,无法从原理上,彻底解决)

  • 数据格式:仍然为应用层的数据格式, HTTP HEADER 数据占用比较大

事件流方式(SSE)

  • 通过 SSE ,客户端可以自动获取数据更新,而不用重复发送HTTP请求。一旦连接建立,“事件”便会自动被推送到客户端。服务器端SSE通过 事件流(Event Stream) 的格式产生并推送事件。

  • 可以实现服务器到客户端的单向数据通信。

  • SSE相较于轮询具有较好的实时性,使用方法也非常简便。

缺点:

  • 大并发情况下,服务器可能会宕机。

  • SSE只支持服务器到客户端单向的事件推送,而且所有版本的IE(包括到目前为止的Microsoft Edge)都不支持SSE。如果需要强行支持IE和部分移动端浏览器,可以尝试 EventSource Polyfill(本质上仍然是轮询)

三、WebSocket是什么?

B/S的请求-响应模式

  • 传统Web中,是由 Browser 主动向 Server 端发送请求,以此获得 Server 端数据;

  • 如果要实现实时通信,实时获取 Server 端的数据,通常是 Client 端定期发送HTTP请求,Server端进行响应并返回数据;

  • HTTP协议:基于请求/响应模式的、单向的、无状态的、应用层协议;

HTTP协议为什么不允许Server主动向Client推送数据?

如果允许 Server 向 Client 主动推送数据,则 Client 很容易受到攻击;特别是广告商会将广告信息,强行推送给 Client,因此 HTTP 的单向特性是必要的。

WebSocket简介

WebSocket协议借用HTTP协议的101( switch protocol) 状态码来达到协议转换,切换为WebSocket协议,它本身是基于Tcp协议的。

特点:

  • Client 跟 Server 之间,双向通信技术,Client 和 Server,都可以主动发起通信

  • 是一种网络通信协议

  • 建立在传输层 TCP 协议之上

优点:

  • 节省带宽;(HTTP 协议的 HEAD 比较大)

  • 节省服务器CPU资源;(HTTP 协议的 Polling 方式,即使 Server 没有数据也要接收 Request)

下图展示了Polling和WebSocket两种模式下,Web应用的效率:


四、协议

首先,WebSocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。HTTP的生命周期通过Request来界定,也就是一个Request一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。HTTP1.1进行了改进,可以使用keep-alive保持连接,但是仍旧是一个Request = 一个Response, Response显得非常的被动,不能主动发起。

握手

来自客户端的握手信息:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

重点请求首部含义:

  • Connection: Upgrade:表示要升级协议

  • Upgrade: websocket:表示要升级到 websocket 协议。

  • Sec-WebSocket-Version: 13:表示 websocket 的版本。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader ,里面包含服务端支持的版本号。

  • Sec-WebSocket-Key:是一个Base64 encode的值,由浏览器随机生成,与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,用于服务端校验,提供基本的防护,比如恶意的连接。

  • Sec_WebSocket-Protocol:是一个用户定义的字符串,用来区分同 URL 下,不同的服务所需要的协议。

来自服务器的握手信息:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

重点响应首部含义:

  • Sec-WebSocket-Accept 根据客户端请求首部的 Sec-WebSocket-Key 计算出来。

  • Connection和Upgrade依然是表示协议升级为websocket协议。

切分数据帧

WebSocket 客户端、服务端通信的最小单位是 帧(frame),由 1 个或多个帧组成一条完整的消息(message)。

  • 发送端:将消息切割成多个帧,并发送给服务端;

  • 接收端:接收消息帧,并将关联的帧重新组装成完整的消息;

数据传递

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

WebSocket的每条消息可能被切分成多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据FIN(是数据帧当中的一个标识,用于判断当前帧是否为当前消息的最后一帧)的值来判断,是否已经收到消息的最后一个数据帧。

当接收到消息的最后一帧,即可以对消息进行处理。

心跳检测

很多原因都会触发连接关闭,一般情况是都会触发连接的onClose事件,但是当断网情况下是不会触发的onclose,这时候就不知道连接是断掉的,此时可以采用心跳重连,客户端每隔一段时间向服务端发送ping数据,服务端一旦苏醒,将进行pong响应,此时即可重新连接。心跳重连不是轮询,轮询会不断建立连接(多个连接),而心跳还是当前这个连接,只是一直发探测消息而已。

关闭连接

一旦发送或接收到一个Close控制帧,websocket 关闭阶段握手启动。

关闭状态码表(关闭原因)

状态码

名称

描述

0–999

保留段, 未使用.

1000

CLOSE_NORMAL

正常关闭; 无论为何目的而创建, 该链接都已成功完成任务.

1001

CLOSE_GOING_AWAY

终端离开, 可能因为服务端错误, 也可能因为浏览器正从打开连接的页面跳转离开.

1002

CLOSE_PROTOCOL_ERROR

由于协议错误而中断连接.

1003

CLOSE_UNSUPPORTED

由于接收到不允许的数据类型而断开连接 (如仅接收文本数据的终端接收到了二进制数据).

1004

保留. 其意义可能会在未来定义.

1005

CLOSE_NO_STATUS

保留. 表示没有收到预期的状态码.

1006

CLOSE_ABNORMAL

保留. 用于期望收到状态码时连接非正常关闭 (也就是说, 没有发送关闭帧).

1007

Unsupported Data

由于收到了格式不符的数据而断开连接 (如文本消息中包含了非 UTF-8 数据).

1008

Policy Violation

由于收到不符合约定的数据而断开连接. 这是一个通用状态码, 用于不适合使用 1003 和 1009 状态码的场景.

1009

CLOSE_TOO_LARGE

由于收到过大的数据帧而断开连接.

1010

Missing Extension

客户端期望服务器商定一个或多个拓展, 但服务器没有处理, 因此客户端断开连接.

1011

Internal Error

客户端由于遇到没有预料的情况阻止其完成请求, 因此服务端断开连接.

1012

Service Restart

服务器由于重启而断开连接.

1013

Try Again Later

服务器由于临时原因断开连接, 如服务器过载因此断开一部分客户端连接.

1014

由 WebSocket 标准保留以便未来使用.

五、优势

  • 相较于HTTP协议,WebSocket支持持久连接;

  • 服务器与客户端之间交换的标头信息很小,大概只有2字节;

  • 客户端与服务器都可以主动传送数据给对方,真正的全双工;

  • 不用频率创建TCP请求及销毁请求,减少网络带宽资源的占用,同时也节省服务器资源;

六、总结

WebSocket在用于双向传输、推送消息方面能够做到灵活、简便、高效,但在普通的Request-Response过程中并没有太大用武之地,比起普通的HTTP请求来反倒麻烦了许多,甚至更为低效。比如某些场景只需要简单的Request-Response,如果换做WebSocket还需要增加一个请求标识RequestId,增加成本。每项技术都有自身的优缺点,在适合它的地方能发挥出最大长处,而看到它的几个优点就不分场合地全方位推广的话,可能会适得其反。

七、实践

1、流程图


2、Attention Point

  • spring websocket需要tomcat为7.x及以上版本;

  • websocket的功能支持需要nginx和本机都增加支持websocket协议的配置;

3、Details

  1. web端页面初始化生成一个唯一标识,标识当前web端页面;

  2. 将唯一标识放在二维码的url地址中生成二维码;

  3. app端扫一扫,请求二维码对应的服务,同时这个请求携带了web端页面的唯一标识和app的一些参数信息;

  4. 服务端收到请求,将参数进行处理返回给携带了唯一标识的web端页面;

  5. web端组件展示服务端响应的信息;

4、前端插件推荐

作者使用的是SpringMVC+React的前后端分离框架,这里推荐几个React不错的插件方便使用:

二维码生成插件:github.com/zpao/qrcode…

Cookie插件:www.npmjs.com/package/rea…

八、参考

ningg.top/websocket-i…

segmentfault.com/a/119000001…

segmentfault.com/a/119000001…

附:轮询、长轮询、短连接、长连接区别对比