goim源码剖析

goim源码剖析

Comet

  • Bucket: 每个 Comet 程序拥有若干个 Bucket, 可以理解为 Session Management, 保存着当前 Comet 服务于哪些 Room 和 Channel. 长连接具体分布在哪个 Bucket 上呢?根据 SubKey 一致性 Hash 来选择。

  • Room: 可以理解为房间,群组或是一个 Group. 这个房间内维护 N 个 Channel, 即长连接用户。在该 Room 内广播消息,会发送给房间内的所有 Channel.

  • Channel: 维护一个长连接用户,只能对应一个 Room. 推送的消息可以在 Room 内广播,也可以推送到指定的 Channel.

  • Proto: 消息结构体,存放版本号,操作类型,消息序号和消息体。

goim源码剖析_第1张图片
comet websocket protoc

comet websocket protoc

goim源码剖析_第2张图片
comet websocket protoc

comet tcp protoc

  • 客户端首先连接到comet服务,comet调用logic来校验用户的合法性,logic会返回一个subKey给comet,该subKey成为该连接的唯一标示;
  • 客户端接下来可以发心跳包给comet,同时,job服务将MQ-Kafka的消息转发到对应comet,comet再将其转发到对应的客户端

主要逻辑代码分析

bucket

定义很明了,维护当前消息通道和房间的信息,方法也很简单,加减 Channel 和 Room. 一个 Comet Server 默认开启 1024 Bucket, 这样做的好处是减少锁 ( Bucket.cLock ) 争用,在大并发业务上尤其明显。

type BucketOptions struct {
    ChannelSize   int 
    RoomSize      int   // 房间(Bucket.rooms)的初始化个数
    RoutineAmount int64 // Bucket.routines数组大小
    RoutineSize   int   // 房间信道通信(proto.BoardcastRoomArg)缓冲区的大小
}

// Bucket is a channel holder.
type Bucket struct {
    cLock    sync.RWMutex        // protect the channels for chs
    chs      map[string]*Channel // map sub key to a channel
    boptions BucketOptions
    // room
    rooms       map[int32]*Room // bucket room channels
    routines    []chan *proto.BoardcastRoomArg // 节点房间的总信道数
    routinesNum uint64 // 处理routines信道的goroutine个数,用于消费房播的信道消息
}
goim源码剖析_第3张图片
image

房间信息推送流程:

  1. job通过rpc的方式发送msg到comet
  2. comet内部将消息推送到至bucket.routines,bucket.routines以轮询的方式选出一个proto.BoardcastRoomArg信道进行消息转发
  3. bucket.routines's worker 消费消息,根据msg'roomid从roomsMap(bucket.rooms)选出该room的所有客户端Channels,再将消息一一转发至client'Channel
  4. dispatchWorker消费client'Channel消息,通过websocket或者tcp协议发送到客户端

单播推送流程:

单播相对于”房播(群播)”,简化了房间检索的步骤。

  1. job通过rpc的方式发送msg到comet
  2. comet内部根据消息的subKey,从buckets中找出对应的channel,将消息转发到对应的Channel
  3. dispatchWorker消费client'Channel消息,通过websocket或者tcp协议发送到客户端

注:
​subKey的生成采用city32的hash算法,bucket是一个大小为n的hashMap slice,其主要目的是将数据切分成更小的块,从而降低资源的竞争。 bucket按照key的cityhash求余命中的,没有用一致性hash,因为这里不涉及迁移

多播与单播推送流程类似,广播也类似。

room

type Room struct {
    id     int32        // 房间号
    rLock  sync.RWMutex // 锁
    next   *Channel     // 该房间的所有客户端的Channel  是一个双向链表,复杂度为o(1),效率比较高。
    drop   bool         // 标示房间是否存活
    Online int          // 房间的channel数量,即房间的在线用户的多少
}

Logic Server 通过 RPC 调用,将广播的消息发给 Room.Push, 数据会被暂存在 vers, ops, msgs 里,每个 Room 在初始化时会开启一个 groutine 用来处理暂存的消息,达到 Batch Num 数量或是延迟一定时间后,将消息批量 Push 到 Channel 消息通道。

Channel

总是觉得起名叫 Session 更直观,并且不和语言层面的 "channel" 冲突。Writer/Reader 就是对网络 Conn 的封装,SvrProto 是一个 Ring Buffer,保存 Room 广播或是直接发送过来的消息体。

type Channel struct {
    Room     *Room      //每个 Channel 都有一个 Room
    CliProto Ring           //客户端 proto
    signal   chan *proto.Proto  
    Writer   bufio.Writer    
    Reader   bufio.Reader
    Next     *Channel
    Prev     *Channel
}

tcp

https://github.com/Terry-Mao/goim/blob/master/doc/proto.md

goim源码剖析_第4张图片
image

ring

ring是一个环形对象池,用于管理协议对象-proto,其内存结构如图所示,rp为可读游标,wp为可写游标,内存的大小为4的整数倍。

goim源码剖析_第5张图片
image

注 ​ 如果wp移动过快,会影响rp游标指向的数据,如图:

goim源码剖析_第6张图片
image

当程序运行到第3步的时候,wp已经又重新超过rp的索引了,这时候,第6个对象还没被rp读取过,但是它的数据已经被修改了,这样即使rp读取第6个对象,也是一个dirty对象。

Signal

信号处理模块,可用于在线加载配置,配置动态加载的信号为SIGHUP。

Logic

goim源码剖析_第7张图片
image

Logic是goim是主要业务处理模块,负责的内容有:

  • 注册/注销
  • 验证
  • 消息Push代理

协议

Push协议

https://github.com/Terry-Mao/goim/blob/master/doc/push.md

其中ensure参数是额外的参数,用于控制消息是否必达,为布偶值。

主要逻辑代码分析

Auth

用户验证模块

router

负责与Router Service交互,多个Router Service采用的是一致性hash算法,hash的输入为Router Serviceid,默认所有的Router Service权值是一样的,如果需要控制不同的权值,可以在配置router service的时候加多个端口实例或者同一个节点配置成多个serviceid标签。

注 ​ 一致性hash没有实现动态扩容,即没有自动平衡,所以不能够动态改变Router service映射配置,且每个Logic节点的Router Service配置项需保持一致。

rpc

LogicService用于心跳的检测以及客户端的注册/注销。

Job

Job负责消费kafka消息,然后转发至comet。

单/多播

[图片上传失败...(image-5e9c72-1539578576429)]

广播

[图片上传失败...(image-63e75-1539578576429)]

按房间推送

[图片上传失败...(image-f119fd-1539578576429)]

主要逻辑代码分析

comet

[图片上传失败...(image-d82023-1539578576429)]

type CometOptions struct {
    RoutineSize int64 // 每个comet rpc goroutine个数
    RoutineChan int   // 每个协议通道的缓冲大小
}
type Comet struct {
    serverId             int32                              // comet service id
    rpcClient            *xrpc.Clients                      // rpc连接对象
    pushRoutines         []chan *proto.MPushMsgArg          // 单/多播信道
    broadcastRoutines    []chan *proto.BoardcastArg         // 广播信道
    roomRoutines         []chan *proto.BoardcastRoomArg     // 群播-房播信道
    pushRoutinesNum      int64                              // 单/多播协议信道=》用于循环
    roomRoutinesNum      int64                              // 房播-群播协议信道=》
    broadcastRoutinesNum int64                              // 广播协议信道=》
    options              CometOptions
}
room
type RoomBucket struct {
    roomNum int 
    rooms   map[int32]*Room
    rwLock  sync.RWMutex
    options RoomOptions
    round   *Round
}
type RoomOptions struct {
    BatchNum   int              //汇总阈值
    SignalTime time.Duration
}

room模块用于接收kafka的push模块发送的消息,每个room都有一个协程,其协程的信道缓冲区的大小为RoomOptions.BatchNum的两倍。

注:
​ 1、room协程在接收到的消息条数>=BatchNum*2或者timeout时,才会触发转发消息的行为(转发至broadcastRoutines),即其具有汇总操作。

2、房间的消息使用了Libs/bytes.Writer进行汇总缓存。

push/kafka

push/kafka模块用于预处理消息,消息从kafka集群流出,经过kafka模块转发至push模块,push模块对消息预处理/过滤/分类,然后发至不同的comet 信道中,具体使用请参照Job的前3章图.

消息的分类:

  • KAFKA_MESSAGE_MULTI=>多播
  • KAFKA_MESSAGE_BROADCAST=>广播
  • KAFKA_MESSAGE_BROADCAST_ROOM=>群播/房播
Round/RoundOptions

时钟管理器

Router

主要逻辑代码分析

session

type Session struct {
    seq     int32                     // 序列号自增标记器
    servers map[int32]int32           // seq:server
    rooms   map[int32]map[int32]int32 // roomid:seq:server with specified room id
}

客户端会话信息管理,以用户id为单位,即每个用户有且拥有一个session,session包含了用户每个连接的comet service信息,以及每个连接所属的roomid。

bucket

type Bucket struct {
   bLock             sync.RWMutex
   server            int                       // session server map init num
   session           int                       // bucket session init num
   sessions          map[int64]*Session        // userid->sessions, 一个可能同时多处登陆
   roomCounter       map[int32]int32           // roomid->count
   serverCounter     map[int32]int32           // server->count
   userServerCounter map[int32]map[int64]int32 // serverid->userid count
   cleaner           *Cleaner                  // bucket map cleaner
}

cleaner

lru对象管理器,只负责管理,不负责触发GC,GC交给Runtime处理。

主要应用于客户端的session管理,定时处理掉一些过期的session对象。

1、数据结构使用map和双向列表,map用于快速检索;

2、双向链表用于快速剔除数据:因为从map中剔除数据,map的结构会实时改变,每剔除一个都得再次从起点开始遍历map,而使用链表不用重新遍历,时间复杂度为O(logN)

Libs

缓冲io-bufio

Reader

type Reader struct {
    buf []byte
    rd  io.Reader
    r, w int
    err error
}

Reader是一个具有缓存的可读IO。

goim源码剖析_第8张图片
image

主要函数

func (b *Reader) Reset(r io.Reader)
重置IO,可读游标重置为0

func (b *Reader) ResetBuffer(r io.Reader, buf []byte)
重置IO,可读游标重置为0,且b.buf变为buf

func (b *Reader) Peek(n int) ([]byte, error)
窥探缓存的n个字节,可读游标维持不变,可写游标可能会改变;

如果可窥探的数据少于n,则调用b.fill()尝试读取远端数据用于填充b.buf.

func (b *Reader) Pop(n int) ([]byte, error)
返回[b.r:b.r+n]处的数据,该函数会调用b.Peek,而且会改变可读游标和科协游标

func (b *Reader) Discard(n int) (discarded int, err error)
丢弃n个数据;如果缓冲区的可读数据小于n,则一致调用b.fill()尝试读取远程的数据,直到b.buf的可读缓冲区大于或者等于n或者出现网络异常为止

func (b *Reader) Read(p []byte) (n int, err error)
读取缓冲区数据;

如果缓冲区为空,为了提高效率,避免应用层的数据拷贝(kernel net stack=>b.buf==>p),直接将kernel net stack拷贝到p []byte,并且调用f.fill()整理缓冲区.

func (b *Reader) fill()

![](http://oat9hwijg.bkt.clouddn.com/goim/bufio-reader-fill.png)
fill读取远程新块数据到本地缓冲区的b.buf

fill会有数据的移动,如图所示。

Writer

type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}

Writer是一个具有缓存的可写IO.

func (b *Writer) Reset(w io.Writer) {
重置IO,可写游标重置为0,句柄变为w

func (b *Writer) ResetBuffer(w io.Writer, buf []byte)
重置IO,可写游标重置为0,句柄变为w,缓冲区变为buf

func (b *Writer) flush() error
刷新缓冲区,将本地缓冲区的数据发送至内核网络栈;

游标被重置(不一定会被重置为0,可能为其他值,因为本地缓冲的数据大于内核可写缓冲区,这时还会造成数据的搬迁)。
![](http://oat9hwijg.bkt.clouddn.com/goim/bufio-writer.png)


func (b *Writer) Available() int
可写字节数

func (b *Writer) Buffered()
已写缓冲大小

func (b *Writer) Write(p []byte) (nn int, err error)
将p []byte写到缓冲区或者直接写到网络内核栈(此时缓冲区已满),可能造成可写游标的移动

func (b *Writer) WriteRaw(p []byte) (nn int, err error)
和b.Write()类似

func (b *Writer) Peek(n int) ([]byte, error)
窥探可写缓冲区剩余值,如果可写缓冲区的剩余值小于n,会调用flush,可写游标相应可能会改变

timer

timer是一个最小堆算法实现的时钟对象,串行执行时钟任务,所以时钟任务应该尽量小,timer不适合太耗时的任务,当然用户可以控制时钟任务的并发。

bytes

Pool

type Buffer struct {
    buf  []byte
    next *Buffer // next free buffer
}
type Pool struct {
    lock sync.Mutex
    free *Buffer
    max  int
    num  int
    size int
}

pool内存组织如下,pool是一个链式存储的栈,数据从栈顶出,同时数据也从栈顶回收。

goim源码剖析_第9张图片
image
func (p Pool) Get() (b Buffer)
获取一个缓冲区

func (p Pool) Put(b Buffer)
归还一个缓冲区

func (p *Pool) grow()
重置Pool对象

注:pool 是一个不限大小的内存池,如果栈没有数据了,那么pool会调用glow()重新生成数据,所以最后可能造成的内存架构如下图所示

goim源码剖析_第10张图片
image

如果租借的速度大于归还的速度,会造成内存的溢出。

Writer

type Writer struct {
    n   int     //游标
    buf []byte
}

writer是一个具有缓冲的可写IO。

func (w *Writer) Size()
缓冲区的大小

func (w *Writer) Reset()
重置缓冲区,游标重置为0

func (w *Writer) Buffer() []byte
返回缓冲区的内容

func (w *Writer) Peek(n int) []byte
窥探缓冲区的n个字节,如果缓冲区的剩余空间小于n,则会调用w.grow()自增长数据缓冲区,游标会移动

func (w *Writer) grow(n int)
按照2倍的大小增长缓冲区,会发生数据的移动

net

xrpc

根据原生的rpc封装的,其调用采用异步的方式,具有重连功能。

xrpc并没有做负载均衡的工作,只是简单做一下容灾而已,相对来时不是很灵活,用户可以稍微修改一下,就支持负载均衡了。

同时xrpc也没有在代码层面上实现[host:por]连接池,即同一个[host:port]配置只会有一个rpc长链接,除非增加多配置。

proto
主要的rpc协议说明:

job
type KafkaMsg struct {
    OP       string   `json:"op"`               //操作类型
    RoomId   int32    `json:"roomid,omitempty"` //房间号
    ServerId int32    `json:"server,omitempty"` //comet id
    SubKeys  []string `json:"subkeys,omitempty"`
    Msg      []byte   `json:"msg"`
    Ensure   bool     `json:"ensure,omitempty"` //是否强推送(伪强推)
}
logic

客户端上线:

// 用于comet发送客户端的校验信息
type ConnArg struct {
    Token  string // Token
    Server int32  // comet id
}
// logic 校验应答
type ConnReply struct {
    Key    string   // subKey
    RoomId int32    // 房间号
}

客户端下线:

// 用于comet发送客户端连接下线
type DisconnArg struct {
    Key    string   // subKey
    RoomId int32    // 房间号
}
// 应答客户端下线
type DisconnReply struct {
    Has bool
}
comet
Push RPC模块

心跳:

type NoArg struct {
}
type NoReply struct {
}

Job---->comet

// 单播
type PushMsgArg struct {
    Key string  //subKey
    P   Proto
}
type NoReply struct {
}
// 把某条消息推送给多个subKey
type MPushMsgArg struct {
    Keys []string
    P    Proto
}
type MPushMsgReply struct {
    Index int32
}
// 多播
type MPushMsgsArg struct {
    PMArgs []*PushMsgArg
}
type MPushMsgsReply struct {
    Index int32
}
// 广播
type BoardcastArg struct {
    P Proto
}
// 吧某条消息推送给某个房间的所有channels
type BoardcastRoomArg struct {
    RoomId int32
    P      Proto
}
type RoomsReply struct {
    RoomIds map[int32]struct{}
}
router

增加用户:

type PutArg struct {
    UserId int64
    Server int32
    RoomId int32
}
type PutReply struct {
    Seq int32   // 序列号
}

移除用户:

type DelArg struct {
    UserId int64
    Seq    int32
    RoomId int32
}
type DelReply struct {
    Has bool    //  是否存在目标用户
}

其他:

// 剔除comet server
type DelServerArg struct {
    Server int32
}
// 获取用户信息
type GetArg struct {
    UserId int64
}
// 获取Router的所有信息
type GetReply struct {
    Seqs    []int32
    Servers []int32
}
type GetAllReply struct {
    UserIds  []int64
    Sessions []*GetReply
}
// 获取多个用户信息
type MGetArg struct {
    UserIds []int64
}
type MGetReply struct {
    UserIds  []int64
    Sessions []*GetReply
}
// 返回所有连接个数
type CountReply struct {
    Count int32
}
// 获取特定房间的所有连接
type RoomCountArg struct {
    RoomId int32
}
type RoomCountReply struct {
    Count int32
}
// 获取所有房间的连接个数
type AllRoomCountReply struct {
    Counter map[int32]int32
}
// 获取所有的comet server个数
type AllServerCountReply struct {
    Counter map[int32]int32
}
// 获取所有的用户个数
type UserCountArg struct {
    UserId int64
}
type UserCountReply struct {
    Count int32
}
推送协议

参照官网

你可能感兴趣的:(goim源码剖析)