设计思路:
前期经过1~2周的预研工作,试了socket相关的方案,因架构组大佬要求比较高,要实现一个大而全的整体方案
比如支持TCP/HTTP/WS等协议的。套件就定位到了MQTT
优先需要搞一些TCP协议的,研究相关技术,发现前端都是根据scoket的方式,采用WS或者WSS协议,服务端在进行转换TCP进行传输,因安全性问题,前端不支持。
为什么非要搞TCP呢?
因为华为云还是什么防火墙不支持scoket透传,这个就有点麻烦了?
经过几次沟通,最后服务端搞定了这个事情,然后我们又回归了WS方式。
设计动机:
系统业务层具有实时互动场景的需求,比如上课后,在线课堂上老师和学生需要进行相关主题互动,比如投票、抢答什么的。
设计规则:
- MQTT 前端只需要支持订阅消息获取,不支持发送消息。
- 发送消息采用HTTP接口方式,服务端业务侧会调用框架层来做消息传递,其实也就是MQTT的主题消息订阅和发布,只不过做了服务端的框架层,不需要业务层做过多的思考。
- 会话级别的订阅,就是用户登录后,会根据当前的会话标记进行客户端注册和默认主题订阅,这样消息接收会默认都会传送到这个会话消息中,然后根据里面的系统标记和业务标记来区分,当前信令的业务含义。
业务消息体规则:
title:消息标题,用于弹窗/鼠标上浮时展示
content:消息内容,用于保存通用消息通知内容字符串
subscriber:订阅方,用于前端判断哪个业务页面需要订阅此消息。
publisher:发布方,后台自定义,前端可以不关注,也可以做校验,只有特定发布方才处理。
cmdType:指令类型,字符串,提供给前端业务页面做判断需要执行的事件
uniqueKey:唯一键,用于前端业务页面判断,只处理自己这个业务数据的场景,
如:教室课堂页面,应该只处理自己这个教室课堂ID的消息
data:业务数据
以上消息中,业务接收到消息后,主要是根据subscriber
来区分业务类型,比如是课堂交互还是作业通知类,这种大业务类上的区分,主要支持在线实时互动和消息的简单通知
其他的字段cmdType、data 可以根据特点的业务场景来进行分类
API
API | 含义 |
---|---|
register | 系统进入初始化IM逻辑 |
createConnect | 创建新得MQTT连接,支持特定场景创建 |
IMConfig | 配置获取 |
代码实践
/*
* @Description: IM管理器-消息中心
* @Author: daerduo
*/
import { BaseStore, Config, IM } from "@basic-library";
import { SocketEmitter,cache } from '@basic-utils'
/**
* IM-消息中心
* 1、通信连接-配置信息
* 2、消息连接、会话订阅、消息通知
*/
const getURL = () => {
const { protocol, hostname, port, path, proxy = "" } = Config.BSConfig?.MQTT_USER_CONFIG
let portStr = port ? `:${Number(port)}` : ''
const host = hostname || window.location.host
return `${protocol}://${host}${portStr}${proxy}${path}`
}
const IMConfig = {
getURL: () => getURL(),
options: Config.BSConfig?.MQTT_USER_CONFIG
}
/**
* @description: 接收信息-内部消息中心执行
* @param {*} payload
* @return {*}
*/
const subscriberEmit = (payload) => {
payload.subscriber && SocketEmitter.emit(payload.subscriber, payload)
}
const register = () => {
const { userInfo = {}, loginStatus } = BaseStore.app
const { openIM = false, options } = Config.BSConfig?.MQTT_USER_CONFIG
if (openIM && loginStatus) {
try {
const token = cache.getCache('token','local')
const clientId = `topic_web_session_${token}` + '_' + Date.now()
const defaultTopic = `topic_web_session_${token}`
IM.connect(getURL(), {
clientId,
defaultTopic,
...options
}).then(() => {
// 开始订阅当前会话
IM.subscribe(defaultTopic)
// 开始监听消息--会话消息【通知】
/**
* title:消息标题,用于弹窗/鼠标上浮时展示
* content:消息内容,用于保存通用消息通知内容字符串
* subscriber:订阅方,用于前端判断哪个业务页面需要订阅此消息。
* publisher:发布方,后台自定义,前端可以不关注,也可以做校验,只有特定发布方才处理。
* cmdType:指令类型,字符串,提供给前端业务页面做判断需要执行的事件
* uniqueKey:唯一键,用于前端业务页面判断,只处理自己这个业务数据的场景,如:教室课堂页面,应该只处理自己这个教室课堂ID的消息
* data:业务数据
*/
IM.receiveSession((payload) => payload && subscriberEmit(payload))
})
} catch (error) {
console.error(error)
}
}
}
/**
* @description: 创建新连接
* @param {*} params
* @return {*}
*/
const createConnect = (params) => {
const { options } = Config.BSConfig?.MQTT_USER_CONFIG
try {
const token = cache.getCache('token','local')
const clientId = `topic_web_session_${token}` + '_' + Date.now()
const defaultTopic = `_${token}`
const oo = {...options,...{
will:{
topic:'error',
payload:'Connection Closed abnormally...!',
retain: false,
qos: 0
}
}}
IM.connect(getURL(), {
clientId,
defaultTopic,
...oo
}).then(() => {
// 开始订阅当前会话
IM.subscribe(defaultTopic)
// 开始监听消息--会话消息【通知】
IM.receiveSession((payload) => payload && subscriberEmit(payload))
})
} catch (error) {
console.error(error)
}
}
export default {
register,
createConnect,
IMConfig
};
核心解读:
1、IM配置这个一般很少用,很多配置都在系统级的配置里面,通过 Config.BSConfig?.MQTT_USER_CONFIG
就可以获取到
2、createConnect 创建新连接,这个需要看业务场景,因为目前系统就一个会话级客户端,进行不同消息类型传递。
3、IM初始化
从配置中获取IM配置
BaseStore.app
,判断是否开启
MQTT客户端ID clientId ,创建主题
const token = cache.getCache('token','local')
const clientId = `topic_web_session_${token}` + '_' + Date.now()
const defaultTopic = `topic_web_session_${token}`
连接成功后,开始订阅默认主题会话
IM.connect
IM.subscribe(defaultTopic)
接收到消息后,推送给前端的事件管理器
/**
* @description: 接收信息-内部消息中心执行
* @param {*} payload
* @return {*}
*/
const subscriberEmit = (payload) => {
payload.subscriber && SocketEmitter.emit(payload.subscriber, payload)
}
IM.receiveSession((payload) => payload && subscriberEmit(payload))
SocketEmitter.emit()
订阅使用的是 payload.subscriber
4、业务订阅使用
import { SocketEmitter } from '@basic-utils'
const subscriber = '业务类别' // 需要与服务端进行约定业务类别
SocketEmitter.on(subscriber,()=>{
...
})
设计完毕,期望你的指点....
针对Loader管理器的一系列,请看下方:
业务侧-Loader设计
业务侧-系统应用加载器-AppLoader
业务侧-登录管理器-LoginLoader
业务侧-异常拦截管理器-ErrorLoader
业务侧-路由管理器-RouterLoader