0%

前端的WebSocket设计思路 —— 设计篇

本文主要以前端的视角来讲解Websocket相关内容,重点是WebSocket的设计过程以及设计思路,文章的思路适用于前端全领域,不管是pc端也好,移动端也罢,甚至uni-app、小程序,实现思路都是一样的。

为什么我要写这一篇文章?因为目前我发现技术圈里写的WebSocket相关文章,都太基础了,太入门了,只是简单的教你怎么用,却没有说该如何做到好用,如何去进行设计,我觉得这才是我们学习的重点。

本篇文章主讲设计过程及思路,至于如何将设计转化为实现,我后续将在下一篇文章中写出。

设计图


交互设计

前后端统一数据体:

// 请求体
interface IRequest {
msgId: number
sequenceId: string
uniqueId: string
data?: Record<string, unknown>
}

// 响应体
interface IResponse {
msgId: number
sequenceId: string
uniqueId: string
code: number
message?: string
data?: Record<string, unknown>
}
字段非空释义作用
msgIdsocket处理事项
uniqueId消息唯一id前端实现消息匹配机制,每条socket消息都会携带,后端需原样传回给前端
code后端响应状态码200代表成功
message后端响应提示信息
data前端/后端传递的数据体

心跳设计

前端定时推送一条消息给后端,后端收到后也推送一条回复,当一定时间内,其中一方没有给予回复时,则认为已失联,需断开并重新建立连接

字段设计:

  • lastRequestTime:最后请求时间

  • lastResponseTime:最后回复时间

  • failCount:失败次数

  • maxFailCount:最大失败次数

  • interval:心跳间隔时间(ms)

  • timeout:超时时间(ms)

  • timer:setTimeout定时器对象

实现逻辑:

  1. 前端发送心跳,并记录本次心跳的发送时间戳

  2. 后端回复心跳,前端收到消息后记录本次回复的时间戳

  3. 假如 lastResponseTime - lastRequestTime > timeout ,则 failCount +1

  4. 假如 failCount > maxFailCount ,则不再发送心跳,直接执行socket重连

  5. socket重连时,会清除心跳定时器

  6. socket重连成功时,failCount、lastRequestTime、lastResponseTime 归零,并开启心跳

消息码:

  • 客户端request:100201

  • 服务端response:100200


重连设计

当通信双方有一方出现异常后,需要重新建立新的socket连接,可能出现异常断联的情况如下:

  • 客户端断网

  • 心跳超时

  • 服务端异常

正常情况下,只要出现上述异常情况,socket消息都无法发出,客户端要能够捕获到上述情况,并作出重连处理

字段设计:

  • lock:重连锁

  • interval:重连的时间间隔

  • timer:setTimeout定时器对象

实现逻辑:

  1. 客户端检测到异常,判断重连锁是否为true,如果为true则退出(为true代表socket正在重连,避免客户端多次调用重连方法的情况)

  2. 清除定时器对象,避免出现定时器堆积而引发其他问题

  3. 关闭现有的socket连接

  4. 开启重连锁,并建立新的连接

  5. 假如重连失败,一定时间后再次调用重连,回到第1步

  6. 重连成功,关闭重连锁

重点:

  • 重连锁的状态控制

设计图:


插件系统

插件系统是我们在socket里实现的一种功能机制,其主要作用在于,当socket进入某种状态时,会通知插件,并向插件传递相关数据

插件属于权利的顶点,可以控制socket实例的任何参数及行为,并可拓展出其他能力

生命周期

插件系统存在以下几个生命周期,每当socket触发生命周期方法,会将其生命周期类型和相应数据,传递给插件系统

  • open:socket建立连接成功时

  • close:socket关闭连接时

  • request:socket向服务端发送消息时

  • response:socket向客户端推送消息时

其中,open和close在一个连接内只会触发1次,而request和response可能会触发多次

插件本质

插件的本质就是一个函数,其类型定义如下:

export type TPluginParams = {
eventType: 'open' | 'close' | 'request' | 'reponse'
data?: Record<string, unknown>
socket: SocketManage
}

// 定义一个插件
const myPlugin: TPluginParams = (eventType, data, socket) => {
// ...
}

注意事项:

  • open 和 close 的 data参数为 undefined

  • 插件内无法使用 this,可采用第三个参数 socket 代替

使用场景:

  1. 日志搜集

  2. 开发调试

  3. 数据加工及转发

  4. ……


消费者机制

消费者机制主要用于前端实现消息匹配机制

痛点情景

前端中,假如函数A和函数B,同时发出了一个msg=1000的消息,那么,此时服务端就会推送2条一模一样的响应回来

此时,我们如何知道哪条数据是函数A的,哪条数据是函数B的?

解决问题

消费者机制除了解决上述场景的问题之外,还实现了消息发起者在得到服务端的响应后,再去处理其它事情

也就是实现了 请求与响应的唯一性与匹配

消费者本质

消费者,本质上就是一个函数,这个函数负责去消费指定的服务端响应

大白话就是,消费者一定会接收到指定的服务端响应,当拿到响应后,消费者接下来可以去干嘛

类型定义如下:

export type TConsumers = (data: unknown) => void

const myConsumer:TConsumers = (data) => {
// ...
}

匹配机制

如何实现一个消费者,有 **针对性 **的去消费指定的响应?

生命周期

一个消费者的生命周期只有一次,也就是单次回话

当一个消费者收到响应并去执行后,其就从消费者集合表中移除,因为每一个消费者都是唯一的,消费者、客户端请求、服务端响应属于1对1关系


监视者机制

概述

本质上,监视者就是一个函数,这个函数的作用就是监听、监视某些状态,那具体它监视什么状态呢?Websocket的响应数据

当服务端推送一条消息至客户端时,会被监视者捕获到,并且会将socket响应的消息传递给监视者

监视者VS消费者

监视者与消费者的作用是一样的,区别在于:

  1. 消费者:socket响应与消费者属于1:1关系

  2. 监视者:socket响应与监视者属于1:n关系,也就是,一个socket响应,可能会被多个监视者捕获到并进行处理

应用场景

  • 当我们发送一条消息后,希望能在准确的得到这条消息的回复后,做其它处理时,可采用消费者

  • 当我们推送一条消息后,不关心服务端什么时候会回复,也不关心请求与响应的匹配性,可采用监视者

  • 服务端主动推送消息的时候,需要用监视者来监听

设计图


消息队列

消息队列的作用在于socket发生异常时,暂存所有待发送的消息,等socket恢复正常时,再将消息队列里的消息推送给服务端

进入队列的条件:

  • 连接断开

  • 心跳超时

  • 连接异常

  • 重连中

实现逻辑:

  1. 客户端发送消息时,先检查socket是否正常,若正常则直接发送,否则push至消息队列

  2. 当连接恢复正常时,延迟300ms后,一次性取出整条队列,循环遍历并执行发送

  3. 若在遍历发送的过程中突然出现异常,则立即push回到队列中

  4. 若在遍历发送过程中又有新的消息入队,则在执行本条队列完毕后,重新执行第2步

  5. 每次出现异常均重新执行以上步骤

第2点延迟300毫秒是因为在这期间可能还有新的消息会入队,等待一下

设计图:


-------------本文结束    感谢阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!