手写一个的在线聊天系统(原理篇2)
Posted Java知音_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写一个的在线聊天系统(原理篇2)相关的知识,希望对你有一定的参考价值。
点击关注公众号,实用技术文章及时了解
摘要
上一篇文章我们了解 Netty 的一些基本原理,并且写了一个简单的 WebSocket 服务端。接下来我们来详细的了解一下 WebSocket 相关的知识点。
一、前提回顾
二、目录介绍
什么是 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 数据帧的统一格式,如下图所示:
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
推荐
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!
以上是关于手写一个的在线聊天系统(原理篇2)的主要内容,如果未能解决你的问题,请参考以下文章