一、需求
需要实现直播间的以下功能:
- 群发消息(文本、图片、推荐商品)
- 点对点私发消息(文本、图片、推荐商品)
- 单个用户禁言
- 全体用户禁言
- 撤回消息
- 聊天记录持久化
二、技术实现
服务端消息中心采用netty实现,
微站、小程序使用websocket与消息中心通信,
安卓端使用netty与消息中心通信。
服务器端每过一定时间会给客户端推送一条ping消息,客户端收到ping消息后回复pong消息,通过心跳验证存活客户端,定时断开未回复pong消息的链接,剔除服务端连接会话信息。
客户端因网络等问题断开链接后,客户端需要实现定时重连机制,先设定断开后每5秒尝试一次重连,这个时间后续可能会修改。
服务端链接地址:ws://{ip}:{端口}/websocket?liveId=123&userId=00b084ea98e24e80a7f8be3c4b8a64d0
liveId为直播id,userId为用户id
1.消息格式
消息为json格式字符串,有如下属性:
字段名 | 类型 | 含义 | 客户端是否必填 |
---|---|---|---|
id | string | uuid,服务端生成 | 不填 |
liveId | string | 直播id | 必填 |
code | int | 系统消息类型 | 必填 |
type | int | 业务消息类型 | 业务消息必填,心跳消息不填 |
msg | string | 消息内容 | 必填,心跳消息不填 |
sendUserId | string | 发送人用户id | 必填 |
sendUserName | string | 发送人用户名 | 不填,服务器端返回 |
sendUserHeadImg | string | 发送人头像 | 不填,服务器端返回 |
receiveUserId | string[] | 接收人用户id数组 | code为群发时无需填写,私聊需要填写 |
sendTime | string | 发送时间,服务端生成,格式:yyyy-MM-dd HH:mm:ss | 不填 |
ext | string | 扩展信息 | 选填 |
消息code定义
code | 含义 |
---|---|
1 | 点对点 |
2 | 群发 |
3 | ping消息,服务器端心跳发送到客户端 |
4 | pong消息,客户端收到ping消息后回应pong消息 |
消息type定义
type | 含义 |
---|---|
1001 | 普通文本消息 |
1002 | 图片消息 |
1003 | 推荐商品消息 |
1004 | 单个用户禁言消息 |
1005 | 全体用户禁言消息 |
1006 | 撤回用户发言消息 |
1007 | 打赏消息 |
pong消息示例:
{
"code":4,
"liveId":"asfasda",
"sendUserId":"sfasdasdasd"
}
a.普通文本消息示例:
{
"id":"fasdasdasd",
"liveId":"fasdasdas",
"code":2,
"msg":"你好啊",
"sendUserId":"这是发送人的用户id",
"sendUserName":"asfasdas",
"sendUserHeadImg":"fasdasdasdsad.jpg",
"receiveUserId":"这是接收人的用户id",
"type":1001,
"sendTime":133548798798,
"ext":"{}"
}
b.带表情的普通文本消息示例:
{
"id":"fasdasdasd",
"liveId":"fasdasdas",
"code":10001,
"msg":"你好啊[微笑]",
"sendUserId":"这是发送人的用户id",
"sendUserName":"asfasdas",
"sendUserHeadImg":"fasdasdasdsad.jpg",
"receiveUserId":"这是接收人的用户id",
"type":1,
"sendTime":133548798798,
"ext":"{}"
}
备注:[微笑]为微笑表情图片的字符串标识,客户端收到这条消息后需要把表情标识替换为图片显示到聊天窗口。
c.图片消息示例:
先把图片文件进行客户端压缩,尽量控制到1M以内,然后调用上传图片接口得到图片相对路径,如果上传成功,组装websocket消息把路径放入msg:
{
"id":"fasdasd",
"liveId":"fasdasdas",
"code":2,
"msg":"/static/img/chat/2020-02-11/xxx.jpg",
"sendUserId":"这是发送人的用户id",
"sendUserName":"asfasdas",
"sendUserHeadImg":"fasdasdasdsad.jpg",
"receiveUserId":"这是接收人的用户id",
"type":1002,
"sendTime":133548798798,
"ext":"{}"
}
d.推荐商品消息示例:
推荐商品的msg字段为商品信息json
{
"id":"asfasdsdads",
"liveId":"fasdasdas",
"code":2,
"msg":"{"productId":"asdasfas","productType":1,"originPrice":100,"currentPrice":100,"productName":"sfasd", "coverImg":"asfasdasd.jpg"}",
"sendUserId":"这是发送人的用户id",
"sendUserName":"asfasdas",
"sendUserHeadImg":"fasdasdasdsad.jpg",
"receiveUserId":"这是接收人的用户id",
"type":1003,
"sendTime":133548798798
}
e.打赏消息示例:
{
"id":"fasdasd",
"liveId":"fasdasdas",
"code":2,
"msg":"/static/img/chat/2020-02-11/xxx.jpg",
"sendUserId":"这是发送人的用户id",
"receiveUserId":"",
"type":1007,
"sendTime":133548798798,
"ext":"{"name":"被打赏用户的用户名", "price":100}"
}
f.单用户禁言消息示例:
{
“id”:”fasdasd”,
“liveId”:”fasdasdas”,
“code”:2,
“msg”:”被禁言用户的id”,
“sendUserId”:”这是发送人的用户id”,
“receiveUserId”:”这是接收人的用户id”,
“type”:1004,
“sendTime”:133548798798,
“ext”:"{'name':'被禁言的用户名'}”
}
f.单用户取消禁言消息示例:
{
“id”:”fasdasd”,
“liveId”:”fasdasdas”,
“code”:2,
“msg”:”被取消禁言用户的id”,
“sendUserId”:”这是发送人的用户id”,
“receiveUserId”:”这是接收人的用户id”,
“type”:1010,
“sendTime”:133548798798,
“ext”:"{'name':'被禁言的用户名'}”
}
f.撤回消息示例:
{
“id”:”fasdasd”,
“liveId”:”fasdasdas”,
“code”:2,
“msg”:”被撤销的消息id”,
“sendUserId”:”这是发送人的用户id”,
“receiveUserId”:”这是接收人的用户id”,
“type”:1006,
“sendTime”:133548798798,
“ext”:””
}
2.消息交互流程
接收消息步骤:
a.当用户打开直播介绍页面时,向注册中心发起建立连接请求,监听消息中心推送过来的消息。
b.当消息中心推送过来的消息触发了监听事件函数,判断type是普通文本消息、系统消息、推荐商品消息其中的某一种,然后执行对应的逻辑处理与展示。
发送消息步骤:
a.当用户打开直播介绍页面时,向注册中心发起建立连接请求,监听消息中心推送过来的消息。
b.如果是普通聊天文本消息或系统消息按约定好的消息格式组装好消息json,如果是图片消息则先调用图片上传接口得到url并把url组装到消息json,调用websocket或者netty sdk向消息中心发送消息。
撤回消息步骤:
a.助教端、ibos点击撤回时,组装一条type为撤回、code为群发类型、msg字段为需要撤回的消息id的消息,发送到消息中心。
b.消息中心收到这条消息后,将mongodb中的消息状态改为撤回,并广播撤回消息到所有客户端。
c.被广播的客户端收到这条消息后,按消息id隐藏对应的消息。
单个用户禁言步骤
a.助教端、ibos点击用户禁言时,组装一条type为禁言、code为点对点私聊类型、msg字段为需要禁言的用户id的消息,发送到消息中心。
b.消息中心收到这条消息后,将用户的禁言状态持久化到数据库,并在缓存中记录直播和用户的禁言关系。如果这个用户的客户端还保持会话连接,点对点推送这个被禁言用户的客户端。
c.被禁言用户客户端收到消息后,文本框禁用并提示用户已被禁言。
备注:
当用户刷新页面时,会从数据库中获取到最新的禁言状态并禁用/启用文本框。极端情况用户发出消息,消息经过消息中心时会查询一下缓存中是否有直播用户禁言关系,有的话该消息不予推送。
取消单个用户禁言的交互步骤和以上步骤相同,只是消息的type为取消禁言。
全体用户禁言步骤
a.助教端、ibos点击全体用户禁言时,将直播的禁言状态持久化到数据库,并在缓存中记录直播的禁言状态。组装一条type为全体禁言、code为广播的消息,发送到消息中心。
b.消息中心将消息广播到观看该直播的所有客户端。
c.客户端收到消息后,文本框禁用。
备注:
当用户刷新页面时,会从数据库中获取到最新的直播禁言状态并禁用/启用文本框。极端情况用户发出消息,消息经过消息中心时会查询一下缓存中是否有直播禁言状态,有的话该消息不予推送。
取消全体用户禁言的交互步骤和以上步骤相同,只是消息的type为取消全体禁言。
3.消息存储
在消息中心所在服务器的本地内存加一个队列作为缓冲区,经过消息中心的聊天记录会追加到缓冲区。开启异步任务定时检查缓冲区是否达到阈值,达到缓冲区阈值后批量存储到mongodb聊天记录表。
mongodb表结构
字段名 | 含义 |
---|---|
id | 主键 |
zbId | 直播id |
msgId | 消息id |
sendUserId | 发送人id |
receiveUserId | 接收人id,多个逗号分隔 |
sendTime | 发送时间 |
code | 消息发送类型 |
type | 消息类型 |
msg | 消息内容 |
ext | 消息扩展内容 |
createTime | 保存时间 |
4.鉴权
在消息中心对用户的发言状态做验证。
5.扩展
如果需要支持热部署和扩容,需要解决如下的坑:
1.消息中心的客户端会话管理代码需要改造成分布式会话管理,例如使用redis做存储,但是连接数过大时网络请求传输的数据量也会增大,不妥。
2.集群需要解决负载均衡问题,目前只能做客户端负载均衡,无法像nginx转发http请求一样实现服务端负载均衡。
3.目前市面上没有成熟标准的解决以上问题的方案,需要自己结合一些零散的思路做一些尝试。
网上找到的消息服务集群思路:
如果从自己编程方面考虑socket集群,那么是有困难的。告诉你一个我曾使用过的架构模型。
1、HTTP服务做集群。
2、socket服务器启用后直接访问HTTP服务,主动告知有一个新的socket服务,socket服务状态用中间缓存层保存,具体服务状态可以使用HTTP心跳轮询检测,此部分为socket服务的主动发现、装载服务、卸载服务。
3、客户端请求HTTP服务,HTTP服务分析保存在其上的各个socket服务的存活状态和负载情况,然后返回给客户端最优的socket服务地址。
4、客户端获得最优负载的socket服务地址后连接对应的socket服务。
5、各服务之间的数据交换,可以添加一台socket服务作为socket服务的中转站,这种方式不太可靠,强依赖于中转服务的存活状态。
6、各socket服务的数据必须能保证全局共享,用于客户端之间数据的共通性,使用户在感知上就像完全连接在一台socket服务之上。
下一篇为netty实现消息推送的代码实现:netty实现消息中心(二)基于netty搭建一个聊天室