摘要:上一篇文章我们了解 Netty 的一些基本原理,并且写了一个简单的 WebSocket 服务端。接下来我们来详细的了解一下 WebSocket 相关的知识点。
一、前提回顾
二、目录介绍
- 什么是 WebSocket?
- WebSocket 如何建立连接?
- WebSocket 数据传输
- WebSocket 如何维持连接?
三、什么是 WebSocket?
WebSocket 是一种 网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
WebSocket 协议规范将 ws(WebSocket) 和 wss(WebSocket Secure) 定义为两个新的统一资源标识符(URI)方案,分别对应明文和加密连接。除了方案名称和片段ID(不支持#)之外,其余的 URI 组件都被定义为此 URI 的通用语法。
例子如下所示:
1 | ws://example.com/api |
大多数浏览器都支持 WebSocket 协议,比如:
- Google Chrome
- Firefox、Safari
- Microsoft Edge
- Internet Explorer
- Opera
WebSocket 的优点
1、较少的控制开销
1 | 在连接建立后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。 |
2、更强的实时性
1 | 由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。 |
3、保持连接状态
1 | 与 HTTP 不同的是,Websocket 需要先建立连接; |
4、更好的二进制支持
1 | Websocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容。 |
5、可以支持扩展
1 | Websocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。 |
6、更好的压缩效果
1 | 相对于 HTTP 压缩,Websocket 在适当的扩展支持下,可以沿用之前内容的上下文; |
四、WebSocket 如何建立连接?
WebSocket 是独立的、建立在 TCP 上的协议。
Websocket 通过 HTTP/1.1 协议的101状态码进行握手。
为了建立 Websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(Handshaking)。
一个典型的 Websocket 握手请求如下:
1 | 客户端请求: |
- Connection:必须设置 Upgrade,表示客户端希望连接升级。
- Upgrade:字段必须设置 Websocket,表示希望升级到 Websocket 协议。
- Sec-WebSocket-Key:是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” ,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
- Sec-WebSocket-Version:表示支持的 Websocket 版本。RFC6455 要求使用的版本是13,之前草案的版本均应当弃用。
- Origin:Origin 字段是必须的。如果缺少 origin 字段,WebSocket 服务器需要回复 HTTP 403 状态码(禁止访问)。
五、WebSocket 数据传输
WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
- 发送端:将消息切割成多个帧,并发送给服务端。
- 接收端:接收消息帧,并将关联的帧重新组装成完整的消息。
数据帧
接下来,我们来了解一下数据帧的格式。详细定义参考 RFC6455 5.2节。
WebSocket 数据帧的统一格式,如下图所示:
FIN:1个比特
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。
RSV1, RSV2, RSV3:各占1个比特
一般情况下全为0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接出错。
Opcode: 4个比特
操作代码,Opcode 的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。
可选的操作代码如下:
- %x0:表示一个延续帧。当 Opcode 为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
- %x1:表示这是一个文本帧(frame)。
- %x2:表示这是一个二进制帧(frame)。
- %x3-7:保留的操作代码,用于后续定义的非控制帧。
- %x8:表示连接断开。
- %x9:表示这是一个ping操作。
- %xA:表示这是一个pong操作。
- %xB-F:保留的操作代码,用于后续定义的控制帧。
Mask: 1个比特
表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
如果 Mask 是1,那么在 Masking-key 中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask 都是 1。
Payload length
数据载荷的长度,单位是字节。为7位、7+16位,或7+64位。
假设数 Payload length == x,则
- x 为 0~125,则数据的长度为 x 字节。
- x 为 126,则后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
- x 为 127,则后续8个字节代表一个64位的无符号整数(最高位必须为0),该无符号整数的值为数据的长度。
Masking-key:0 or 4字节
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask 为1,且携带了4字节的 Masking-key。如果 Mask 为0,则没有 Masking-key。
备注:载荷数据的长度,不包括 Masking-key 的长度。
Payload data:(x+y) 字节
载荷数据:包括了 Extension data(扩展数据)、Application data(应用数据)。其中,扩展数据 x 字节,应用数据 y 字节。
- Extension data(扩展数据): 如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
- Application data(应用数据): 任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度减去扩展数据长度,就得到应用数据的长度。
数据传输
WebSocket 客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。
数据分片
WebSocket 的每条消息可能被切分成多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据 FIN 的值来判断,是否已经收到消息的最后一个数据帧。
- FIN=1 表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。
- FIN=0,则接收方还需要继续监听接收其余的数据帧。
opcode 在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。
例子如下所示:
1 | Client: FIN=1, opcode=0x1, msg="你好,Server" |
第一条消息:
1 | FIN=1, 表示是当前消息的最后一个数据帧。 |
第二条消息:
1 | FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成。 |
六、WebSocket 如何维持连接?
当浏览器对 WebSocket 建立的长连接都有节能策略,即 持续一段时间内没有数据传输时,浏览器会主动断开长连接。因此,我们如果需要维持长连接长时间不断开,需要设计特定的心跳来维持这条 WebSocket 连接,即心跳机制。
心跳机制
心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了,需要重连(服务器回复是用来检测网络和后端是否正常工作)。
注意:Nginx 中也有相关的长连接维持时长设置。 如果 WebSocket 连接在间隔比较短的时间就被后端主动断开(即触发close事件),而前端没有触发任何关闭操作,可以检查下 Nginx 相关配置项。
如何处理断网或者后端异常情况
在浏览器网络断开的情况下,WebSocket 是不会收到任何的事件的。由于 WebSocket 在断网时的表现和在线时无消息收发的状态无法区分,我们需要用其他的方法来进行判断和区分。
具体的方法有如下几种:
- 使用心跳包。我们在发送心跳包后,会收到相关的返回数据。如果我们无法收到此数据,就认为目前网络或者后端异常。
- offline事件。浏览器会在断网后给页面发送一个offline事件(不准确,可以作为参考),我们可以根据此事件来断开长连接。
如何快速的恢复连接
当网络恢复时,我们需要快速的恢复长连接。我们可以根据以下几个方案,来恢复我们的 WebSocket 连接。
- 递增重试的时长:当我们短卡网络时,我们立即设置一个递增的时长(如 1,2,3,5,10,20 秒)来尝试恢复长连接。
- online 事件重置重试的时长:在浏览器网络恢复时,会发送一个online事件(同样不准确)。在监听到 online 事件时,我们只需要重置这个时长,立即尝试恢复即可(因为 online 事件触发时,网络仍然有可能处于抖动状态)。
- 检测休眠重置重试的时长:当浏览器休眠时,JavaScript 不会执行。当电脑被唤醒时,如果 online 事件没有触发,那么重试的时长有可能由于多次尝试变成一个较大的值。因此我们在检测到休眠被唤醒后,需要立即重置重试的时长。具体方法为:设置一个setInterval,每次判断上次执行与本次执行时长间隔。因为休眠时 JavaScript 不会执行,因此,如果间隔时长较大(超过设置阈值),我们就认为电脑休眠被唤醒了。
参考
- 本文作者: th3ee9ine
- 本文链接: https://www.blog.ajie39.top/2022/07/05/基于 Netty 实现在线聊天系统(原理篇二) /
- 版权声明: 本博客所有文章除特别声明外,均采用 LICENSE 下的许可协议。转载请注明出处!