手写一个的在线聊天系统(原理篇2)

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

点击关注公众号,实用技术文章及时了解手写一个的在线聊天系统(原理篇2)

摘要

上一篇文章我们了解 Netty 的一些基本原理,并且写了一个简单的 WebSocket 服务端。接下来我们来详细的了解一下 WebSocket 相关的知识点。

一、前提回顾

基于 Netty 实现在线聊天系统(原理篇一)

二、目录介绍

  • 什么是 WebSocket?
  • WebSocket 如何建立连接?
  • WebSocket 数据传输
  • WebSocket 如何维持连接?

三、什么是 WebSocket?

WebSocket 是一种 网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

WebSocket 协议规范将 ws(WebSocket)wss(WebSocket Secure) 定义为两个新的统一资源标识符(URI)方案,分别对应明文和加密连接。除了方案名称和片段ID(不支持#)之外,其余的 URI 组件都被定义为此 URI 的通用语法。

例子如下所示:

ws://example.com/api
wss://example.com/api

wss 表示使用了 TLS 的 Websocket

大多数浏览器都支持 WebSocket 协议,比如:

  • Google Chrome
  • Firefox、Safari
  • Microsoft Edge
  • Internet Explorer
  • Opera

WebSocket 的优点

1、较少的控制开销

  • 在连接建立后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。
  • 在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);
  • 对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。
  • 相对于 HTTP 请求每次都要携带完整的头部,此项开销显著减少了。

2、更强的实时性

  • 由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。
  • 相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
  • 即使是和 Comet 等类似的长轮询比较,其也能在短时间内更多次地传递数据。

3、保持连接状态

  • 与 HTTP 不同的是,Websocket 需要先建立连接;
  • 这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。
  • 而 HTTP 请求可能需要在每个请求都携带状态信息(如身份认证等)。

4、更好的二进制支持

  • Websocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容。

5、可以支持扩展

  • Websocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。

6、更好的压缩效果

  • 相对于 HTTP 压缩,Websocket 在适当的扩展支持下,可以沿用之前内容的上下文;
  • 在传递类似的数据时,可以显著地提高压缩率。

四、WebSocket 如何建立连接?

WebSocket 是独立的、建立在 TCP 上的协议。

Websocket 通过 HTTP/1.1 协议的101状态码进行握手。

为了建立 Websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(Handshaking)。

一个典型的 Websocket 握手请求如下:

客户端请求:

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

服务器回应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
  • 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 数据帧的统一格式,如下图所示:

手写一个的在线聊天系统(原理篇2)

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),顾名思义,就是完整消息对应的数据帧还没接收完。

例子如下所示:

Client: FIN=1, opcode=0x1, msg="你好,Server"
Server: (消息立即被处理) "你好,Client".
Client: FIN=0, opcode=0x1, msg="Hello"
Server: (继续等待后续消息)
Client: FIN=1, opcode=0x0, msg="world!"
Server: (处理完成消息) "good!"

第一条消息:

FIN=1, 表示是当前消息的最后一个数据帧。
opcode=0x1,表示客户端发送的是文本类型。

第二条消息:

FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成。
FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧。

六、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 不会执行,因此,如果间隔时长较大(超过设置阈值),我们就认为电脑休眠被唤醒了。

参考

  • https://zh.m.wikipedia.org/zh-hans/WebSocket
  • https://datatracker.ietf.org/doc/html/rfc6455
  • https://cloud.tencent.com/developer/article/1341903

推荐

Java面试题宝典

技术内卷群,一起来学习!!

手写一个的在线聊天系统(原理篇2)

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

原文始发于微信公众号(Java知音):手写一个的在线聊天系统(原理篇2)