因为要完成一个聊天的项目,所以借鉴了goim,第一篇分析打算半原版,先摘抄//www.greatytc.com/p/8bd96a9a473d他的一些理解,写这些还是为了让自己更好的理解这个项目,如果有什么不对的,请大家给我留言。
Comet 角色定位
GOIM整体架构
在整个架构中,系统被分成 Comet, Logic, Job, Router 四大模块,各个模块通过 RPC 通信,参考官方中文文档,Comet 程序是连接层,暴露给公网,所有的业务处理推给 Logic 模块,通过 RPC 通信。这样设计的好处在于,长连接逻辑很少变动,稳定的保持公网连接,而后端 Logic, Router 模块经常变动,重启不会影响连接层。
几个重要的结构体
做为典型代码即注释的开源项目,goim 基本无太多阅读障碍,几个逻辑点梳理下很快就会明白。
Bucket: 每个 Comet 程序拥有若干个 Bucket, 可以理解为 Session Management, 保存着当前 Comet 服务于哪些 Room 和 Channel. 长连接具体分布在哪个 Bucket 上呢?根据 SubKey 一致性 Hash 来选择。
Room: 可以理解为房间,群组或是一个 Group. 这个房间内维护 N 个 Channel, 即长连接用户。在该 Room 内广播消息,会发送给房间内的所有 Channel.
Channel: 维护一个长连接用户,只能对应一个 Room. 推送的消息可以在 Room 内广播,也可以推送到指定的 Channel.
Proto: 消息结构体,存放版本号,操作类型,消息序号和消息体。
多协议支持
Goim 支持 Tcp, Http, WebSocket, TLS WebSocket. 非常强大,底层原理一样,下面的分析都是基于 Tcp 协议。
Bucket
先来看看结构体的定义
Bucket结构体
定义很明了,维护当前消息通道和房间的信息,方法也很简单,加减 Channel 和 Room. 一个 Comet Server 默认开启 1024 Bucket, 这样做的好处是减少锁 ( Bucket.cLock ) 争用,在大并发业务上尤其明显。最新的版本增加了routineamout,routinesize这两个选项主要是用来做progress推送消息的作用,第一个是做消息推送协程个数,第二个是管道长度。比旧版buket取消了以前的roomoptions直接使用room的参数
Room
Room结构体
Room 结构体稍显复杂一些,不但要维护所属的消息通道 Channel, 还要消息广播的合并写,即 Batch Write, 如果不合并写,每来一个小的消息都通过长连接写出去,系统 Syscall 调用的开销会非常大,Pprof 的时候会看到网络 Syscall 是大头。新版本room改动很大,以前这么同学说辞不知道是否管用,但是从结构体定义来看,room增加了下个channel和一个在线状态。
消息广播
Logic Server 通过 RPC 调用,将广播的消息发给 Room.Push, 数据会被暂存在proto这个结构体 里,每个 Room 在初始化时会开启一个 groutine 用来处理暂存的消息,达到 Batch Num 数量或是延迟一定时间后,将消息批量 Push 到 Channel 消息通道。
Channel
Channel结构体
这是一个通道。Writer/Reader 就是对网络 Conn 的封装,cliProto 是一个 Ring Buffer,保存 Room 广播或是直接发送过来的消息体。
消息流转
这里只分析 Comet 代码,所以消息生成暂时不提
- Client 连接到 Comet Server, 握手认证
- 新建当前长连接的 Channel, 由于 Comet 服务不处理业务逻辑,需要 RPC 去 Logic Server 获取该 Channel 的订阅信息。同时 Channel 开启一个 dispatchTCP groutine, 阻塞等待 Ring Buffer 数据可用,发送到 Client。
- Logic 服务通过 RPC, 将消息写到 Room (广播)或是直接写到指定 Channel (单播)。注意这里,广播是有写合并 BatchWrite, 而单播没有,消息生成后立刻发送。
- Room 里的广播消息到达一定数量 Batch Num, 或是延迟等待一定时间后,将消息写到 Channel Ring Buffer。
小结
新版本跟以前那位同学的很不一样,第一版本由于时间关系,基本大多数是摘抄这位同学的,感觉新版本很多不对,但是阅读代码还没到那个程度,所以第一版就到这里,这位同学对goim大体架子分析的很清楚,细节在我后续章节会有,如果有不对的,请及时留言纠正
补充-来自毛剑
- bucket按照key的cityhash求余命中的,没有用一致性hash,因为这里不涉及迁移
- 私信发送其实也有合并的,和room合并不同的是,在ringbuffer取消息饥饿时候才会真正flush
- 还有一个优化可以改进,因为room有个特点大家消息可能都一样,所以在room提前合并成字节buffer,然后广播所有人,避免每个人都序列化一次,然后利用gc来处理这个buffer的释放,这样可以节省大量cpu,目前这个优化还没做