我们继续讲
结合goim然后如何去做一个私信系统
其实就是一个im工具怎么去做
我们先了解一些基本概念
就是在聊天系统中
我们基本都会用聊天来做一些什么
像1v1就是我们所谓的私聊
然后群聊
以前像QQ还有这种所谓的在线状态是否在线
像Facebook的Message
应该就能看到是否在线
还有多端的多设备都可以同步
还有就是说我们如果离线的时候
苹果或者是小米华为手机可能会收到一些通知
这些都是它的基本的实现
我觉得要做一个聊天系统的话
基本上就是消息在用户不在线的时候
如何去存储这些新发的一些消息
然后用户上线的时候如何去同步所有的消息
我觉得这个是非常关键的
以及消息在线的时候如何转发到对应的人
这个就是一个聊天系统的一些核心关键了
它的通讯协议相对来说是比较简单
你用goim的 用TCP也好 用websocket也好
用HTTP长轮巡也好
定时轮巡也好
都可以
像QQ或者是微信的话可能是自己的私有协议
有可能是基于TCP的 有可能是基于UDP的
我印象中是微信应该是TCP的
然后QQ好像是UDP的
不确定我以前好像是有调研过
很多人问我做im的时候
它完整系统应该怎么做
其实你把整个聊天系统划成很多个功能去看待
比方说我们的登录注册用户信息
什么公众号文章这些
它完完全全就是一个
普通的用HTTP暴露一些API出来
它就是一个这种微服务就可以了
把它做成就是所谓的无状态的服务
像消息的转发
群聊的消息转发
用户状态的获取
这种其实更适合通过实时通讯的方式
就是通过我们的im系统下发指令或者下发消息的方式去获取
那么还有第三方的
比方说小米华为推送苹果的APS这些属于第三方的基层
所以我们看一套系统它其实是分为第三方的
分为有状态的以及分为所谓的就是叫无状态的
那无状态的话就是我们的用户
通过我们的Loadbalance HTTP接口
那可以请求我们的像鉴权服务 群管理
用户的profile 用户的个人信息
那么有状态的话就是我们一个设备可以通过websocket或者TCP
连接到我们的chart-service就是我们的im的一个服务
包括还有第三方
那你系统这样划分以后
真正用到有状态的stateful的纯在线的实时通讯的这种其实就几个
就是你的消息的转发和用户状态
你一划分以后
你的im其实就清晰的多了
它不是所有的指令都是要通过chat-service来去call后端的下游微服务
你完全可以做个HTTP接口
比方说你进入一个群
你要去加个群或者是同意一个人或者是拒绝一个人
都可以做HTTP接口去设计一套流程
不一定全部要耦合到im的实时通讯的这种方式来做
所以在聊天系统里面goim
它主要就是realtime service
就是在线服务
那么实现是对连接和状态的管理
通过我们设计了多套API service
进行系统之间的解藕
这个API service其实我们刚刚说的
就包含像群管理用户身份信息什么登录验证这些
我们看看整套架构
用户其实是可以通过HTTP接口来请求API service
这些我们要全部展开化
我刚刚说的包含像登录注册修改信息这些
那么他通过websocket或者TCP
它接入到是realtime-service
那么这些其实就包含像我们的聊天
chat service聊天的转发
还有我们的在线服务
就presence service就是在线服务
底层的可能依赖一些kv存储或者是MySQL都可以
然后我们第三方的notify
其实做第三方的通知
苹果推送这些它其实是外挂的一些服务
你这样一拆分其实架构就变得清晰很多
我们举个例子
一对一的一个聊天怎么一个流程呢
比方说用户a为要发送消息给用户b
那么用户a第一步先去聊天服务
从chat service获取一个消息id
通过chat service可能会call内部的一个id generator
它可能是另外一个服务
拿到一个消息id
那么chat service其实你可以想象一下
goim的内部的logic那个模块
它可能会把这个消息push到这个消息队列
那么消息队列
一部分我就会把它异步落地到持久化到kv的一个存储里面去
我会把它落地到kv存储
落地持久化成功以后
我先是会尝试根据它的路由查到它应该去chat server2
我就把它推过去
如果成功的话
它可能我们对到用户b就能收到这条消息
但如果他不在线
其实他就会触发另外一个流程
就是offline
那么我时候会调用的就是
苹果的推送或者是小米华为的推送
告诉他有一个消息到达
对 所以它其实是在线状态服务还是蛮关键的
是否在线
当然其实这个在线也是个假在线
因为我们知道
TCP之间你最终是通过应用心跳来保活的
所以它其实是有一定的gap的
它是有一定的时差的
它不是说时时刻刻表示用户在线的
其实是很难做到的
如果用户是离线状态
那么它要做消息同步的时候
实际上它就有两种架构的实现
我们叫收件箱模式
它其实就是有两种概念
一种叫收件箱
一种叫发件箱
那么收件箱指的是用户
我作为一个个人我收到的一些消息
那么发现我发出去的消息
什么叫timeline模型
我要去同步消息肯定是我本地看到了哪一个id
比方说我们的消息
假设在我的收件箱里面
它都是有递增的唯一自增的 就是顺序自增的
那么我的收件箱里的如果id顺序自侦
那么我看到哪一条消息
我用id来做一个sync key
就是一个同步key
去我的收件箱
从我的服务器上大于等于这个key的全部拖回来不就好了吗
我这样其实就做了个消息同步
这种模式其实像微博也好
就是像这种信息流
其实基本都是类似的思路
比这个id大的之后的消息全部拖过来
然后做一些展现
那么在消息的存储实现里面
通常要么就是用关系型数据库
要么就是用NoSQL
我认为像做消息存储的话
可能用kv可能会更好一些
包括水平扩展延迟低 访问成本低
如果要用关系型数据库
可能存好友列表 群组消息
这些可能会更合适
但是如果你是单纯的私聊
可能用kv一可能是更合适的
那为什么
我们先看一下
像一对一的消息存储设计里面
其实最重要的就是说它的这张表是怎么设计的
我们看这message表
有一个message_id就是消息id
有一个message_from就是消息发送者的ID
就是谁发的
还有一个接收者的id
然后有个消息内容
那么有一个create_at
就是这个消息的发送时间
简单的一张消息表
如果我要把它放到收件箱里面
我就可以在kv上这么设计
比方说用message_to
message_to就是消息的接收者
你发给我的就发到我的收件箱里面
消息接受者做key下划线带上message_id
然后这个里面就是可以放outbox_message_key
可以反查对方的发件箱
我这边存的是收件箱引用的是发件箱的key
我这样的话做两次查找
就可以把整个消息给捞出来了
这是一种最简单的方式啊
这个主键message_id它一定要保证自增
因为我们知道像HBass或者是TiDB这种
或者像RocksDB这种
它是都支持range的一个搜索
比方说大于等于某一个id
它是可以走索引效率是相对比较高的
就有点类似于像MySQL大于等于id
它其实是走可以命中走索引的
因为kv的话一般是不支持secondary index
不支持二级索引
所以你只要在你的key的设计上
是让它连续的话
那么你走这样的一个模型也是可以搜索的
比方说我这个场景里面
message_to是我的接收者id
就是我的用户id下划线
如果我的message_id是自增的
我的搜索perfix的搜索
我的用户a下划线
然后某一个id开头的
我就可以做一个scan操作
把这个消息都同步拉回来
或者如果你要买MySQL设计的话
只要大于等于这个id where user_id等于我
然后你就可以全部把这些消息同步回来了
那这个是一个比较简单的
我觉得私聊的话基本上就是投递到对方的收件箱
这个是非常简单的
冗余一份就好了
你这个地方其实不一定非要去查发现一下
你即便是把整条消息冗余存进去也是没问题的
因为发件箱这种场景比较少
我们很少有一个业务场景里面可以查到自己发的内容
对吧
你只能点到聊天记录里面去
有你们之间的一个对话内容
可能都是本地的
像微信的话基本都是本地的
你需要备份聊天记录
所以我觉得可能收件箱会用的比较多
那么发件箱可能场景会比较少
那么在群聊的消息里面
我们再看一下群消息
所谓的群组id消息id
谁发的消息内容
因为群聊其实就不是针对谁去发的
所以它只有一个消息的一个发送id
然后消息创建时间
我们叫group_message
群聊就是两种实现
我们先看一下
叫读扩散和写扩散两种模型
什么叫读扩散
我假设有好多个群
我都去每一个群的group_message这张表里面
把这些消息给捞出来
假如我有100个群
我就要去查100次
把这100个群我分别看到哪里来了
把它们全部做一个同步对吧
这是一种模式
相当于这样的话
你查的是群组的时候
就像你有100个群就查100次
但是这样的话
如果你有1000个群
你的成本就非常高
还有一种就是叫写扩散是什么
假设群里面有一个人发了一条消息
那么这个群可能有500人 有1000人
那么就到这500个人里面的收件箱每人写一份
我的读的时候就简单了
我只需要读自己的收件箱就可以了
这是两种模式
就是两种设计
一种是读扩散
一种是写扩散
通常像微信这种架构
它的群聊的人数是有限制的
比方1000人
所以我觉得写扩散可能更合适
写扩张的话
这样的话每次发送一个消息
都需要把这个消息广播写入到所有在群里面的每一个人
发到他的收件箱里面去
所以群组比较小的时候
成本不是很高
你像微信群我刚才说的1000人500人的上线其实还好
但是它对群组的数量没有上限
如不管你加了多少个群
我只要同步自己的收件箱就可以了
非常简单 对吧
但是如果是读扩散的模式
你一定要控制群的上限
因为这样的话需要同时查询多个群
把消息聚合下来
你才能获取到各个各个群的一个进展
但是它的好处是它写成本比较低
每一个群只存储了自己的这一份发件箱的内容
就是每个群发自己的邮箱
我同步的时候去指定的那张表
拖一下不就好了吗
那么它的查询成本比较高
但是它写入成本比较低
从我来讲
我们现在大部分的数据库
无论是MySQL还是RocksDB
这种ls Mt的这些结构的这种存储
基本上如果你是用kv的话
LSMTree实现的话
都是对写友好的
那么写友好的话读扩散可能就不是那么好了
所以写扩散是更合适的
所以如果你是用LSMTree做了一个kv的存储引擎
那么我认为写扩散可能更合适
这个其实是要了解一下的
所以读写扩散
包括像我们如果做微博的这种信息流系统
其实也是需要你去考虑的
就是什么场景下应该用读扩散什么情况下要写扩散
我再举个例子
我们如果是要做微信的公众号
你觉得应该是走什么模式会比较好
这个问题我们可以抛出来想一想你觉得应该怎么去做
如果是微信的订阅的公众号
我们在长连接中结合goim
如果你要做消息的一个同步和广播
你如果想把消息通知给所有人
主要有两种模式
一种就是所谓的推
就是说发了一条消息
看一下这个群里面有什么人
然后分别发给他的comet的接入节点
然后他可以收到这个通知
那么还有一种叫拉模式
定期的用户他会来拉一下有没有新消息
这样的话其实消息延迟会比较厉害一些
但还有一种模型就是说是推拉结合的
像微信可能就是推拉结合的
就是说有新消息到达了
它会推一个新消息的通知给到我们的APP端或者是PC端
那么这个时候前端收到指令以后
它其实是根据本地的一个sync key
也就是说我现在收件箱有一个最小的id
然后它用这个请求到服务端做一个消息同步
所以我们看到第三张图
它是push你有个指令
然后你再来做一个消息同步把它这个消息拉回来
你就可以把最新的消息全部拉到本地了
你其实就收到了最新的消息
所以我们有时候自己可以观察一下
你切到微信它有个消息同步的一个转圈
然后你这个时候就收到一坨你的好友发给你的消息
还有群里面的一些消息
所以它其实是一种推拉结合的
通过push指令告诉你
有新消息到达
然后就自动触发
我觉得这种模式是相对比较简单的 就设计上
因为如果你纯走push模式的话
消息乱序是很有可能会产生的
你很难保证两次push谁先谁后
但是sync同步
sync同步的机制一定是消息顺序到达你客户端
并且你可以完整整排序的
我觉得这个可能会比较好一点
消息同步其实也不难
你核心就是理解每一个人都有一个id
这个id自增的
比方现在等于1
那么我现在产生了2345的消息
那么id大于等于1
id为2345的消息其实就会被我拖回来
我下次就可以用5来做同步的sync key
我就查比5大的消息同步回来
所以它就是一种很简单的单向同步的一个机制
这个就是结合个goim和消息存储
它其实就是个完整的im
那么goIm其实就是解决连接管理
那么在实时推送的场景里面用的比较多
就我刚才说的
你可以通过goim下发一个指令
告诉他有新消息到达
那么这个时候APP端就是要触发一个消息的同步
把这些消息全部给下载下来
整个消息存储我们介绍了
它其实是分为读扩散和写扩散
我认为在群聊并且群不是特别大的情况下
用写扩散是更合适的
并且kv基本上都是LSM的
LSM的其实它是对写入是比较友好的
因为它是顺序写的
读扩散的话其实是非常适合这种大v
非常适合大v这种大公众号
我们也可以想象
比方说你要自己去做个公众号
如何知道对方有消息送达了
其实推拉结合其实在很多业务场景里面都用的多
包括像微博的信息流
你们也可以去搜一下微博的信息流架构是怎么实现的
那带着一些疑问我们回头再来详细讨论一下
好
今天的整个内容基本上就到这里了