【计算机网络实战】简易IM(四)私聊和群聊的全套逻辑
前言
这一篇主要分析与KIM的聊天逻辑相关的业务代码。我认为这部分核心的模块有三个:
- 通信协议实体的定义
- 通信对象的定义
- 聊天逻辑的handler部分
它们都在主目录下的services包下。
需要说明的是,因为笔者并不是通信专业出身的,所以我全文所说的“顶层”、“底层”是从软件思维上考虑的:被高度封装的就是顶层,需要一点点实现的是底层。
例如handler里面这段代码:
···
// 1. 解包
var req pkt.MessageReq
if err := ctx.ReadBody(&req); err != nil {
_ = ctx.RespWithError(pkt.Status_InvalidPacketBody, err)
return
}
// 2. 获取接收方的位置信息
receiver := ctx.Header().GetDest()
loc, err := ctx.GetLocation(receiver, "")
if err != nil && err != kim.ErrSessionNil {
_ = ctx.RespWithError(pkt.Status_SystemException, err)
return
}
···
像这类内容就是我说的“顶层”,因为ReadBody()、GetLocation()这些都已经被封装好了,顾名即可思义,就算没有注释也不妨碍对代码的理解。在这部分中,程序员只需要关注大体的实现逻辑,而不需要关注函数内部的具体实现细节。
但是我觉得对于通信专业的同学而言,“顶层”指的应该是物理意义上的、最靠近云端的协议。
大家都没有错,只是思考的出发点不一样哈。
通信协议实体
// services/service/database/model.go
type MessageIndex struct {
ID int64 `gorm:"primarykey"`
AccountA string `gorm:"index;size:60;not null;comment:队列唯一标识"`
AccountB string `gorm:"size:60;not null;comment:另一方"`
Direction byte `gorm:"default:0;not null;comment:1表示AccountA为发送者"`
MessageID int64 `gorm:"not null;comment:关联消息内容表中的ID"`
Group string `gorm:"size:30;comment:群ID,单聊情况为空"`
SendTime int64 `gorm:"index;not null;comment:消息发送时间"`
}
type MessageContent struct {
ID int64 `gorm:"primarykey"`
Type byte `gorm:"default:0"`
Body string `gorm:"size:5000;not null"`
Extra string `gorm:"size:500"`
SendTime int64 `gorm:"index"`
}
在一个package中,Message.Req前面会被加上一个Header,在这里KIM的作者只定义了type、body、extra三部分,可根据实际情况删减。
// wire/proto/protocol.proto
// chat message
message MessageReq {
int32 type = 1;
string body = 2;
string extra = 3;
}
message MessageResp {
int64 messageId = 1;
int64 sendTime = 2;
}
message MessagePush {
int64 messageId = 1;
int32 type = 2;
string body = 3;
string extra = 4;
string sender = 5;
int64 sendTime = 6;
}
...
通信对象的定义
type User struct {
Model
App string `gorm:"size:30"`
Account string `gorm:"uniqueIndex;size:60"`
Password string `gorm:"size:30"`
Avatar string `gorm:"size:200"`
Nickname string `gorm:"size:20"`
}
type Group struct {
Model
Group string `gorm:"uniqueIndex;size:30"`
App string `gorm:"size:30"`
Name string `gorm:"size:50"`
Owner string `gorm:"size:60"`
Avatar string `gorm:"size:200"`
Introduction string `gorm:"size:300"`
}
// GroupMember GroupMember
type GroupMember struct {
Model
Account string `gorm:"uniqueIndex:uni_gp_acc;size:60"`
Group string `gorm:"uniqueIndex:uni_gp_acc;index;size:30"`
Alias string `gorm:"size:30"`
}
其中Model是一个通用的结构体:
type Model struct {
ID int64 `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
}
聊天逻辑的handler部分
以单聊为例,下面我将以图片说明它做的几件事情:
先暂存后判断的设计能较大程度地保证信息不丢失。
推送的消息实际上是一堆协议字段及body内容:
// 4. 如果接收方在线,就推送一条消息过去。
if loc != nil {
if err = ctx.Dispatch(&pkt.MessagePush{
MessageId: msgId,
Type: req.GetType(),
Body: req.GetBody(),
Extra: req.GetExtra(),
Sender: ctx.Session().GetAccount(),
SendTime: sendTime,
}, loc); err != nil {
_ = ctx.RespWithError(pkt.Status_SystemException, err)
return
}
}
群聊的逻辑比单聊多了一步: 读取成员列表
membersResp, err := h.groupService.Members(ctx.Session().GetApp(), &rpc.GroupMembersReq{
GroupId: group,
})
if err != nil {
_ = ctx.RespWithError(pkt.Status_SystemException, err)
return
}
var members = make([]string, len(membersResp.Users))
for i, user := range membersResp.Users {
members[i] = user.Account
}
并把单聊中获取单个成员的位置信息和进行单条推送改为批量方式
注意消息推送是同时推的,不是for遍历列表然后一条条推的。不然如果列表数量过大,群成员之间收到消息的时间相差较大,是一个很严重的失误。
关于阅读开源代码的一点碎碎念
以上提到的内容虽然很简单、也很好理解,但实际上如果不借助任何作者本人提供的详尽资料,要在短时间内彻底捋顺别人写的代码是很困难的,举个例子: 前面提到在handler中已经封装好了所有的逻辑,放眼一看,大多和context下的内容有关。但是当你跳转进去看的时候,又会感觉像套娃一样。
有的是在主目录下的context.go中,有的是在storage.go中,跳转一层不能解决问题,往往要跳转很多层,跳着跳着又会跳到一些引用的外部库中,跳到最后都忘记最初是因为什么才跳过来了。
作为局外人很难理解作者那种跳跃的思维:ta为什么要这么设计?这段代码为什么要放在那里?
看别人的代码真是比自己写代码要痛苦一百倍不止的事情。
但也许只有跳出舒适圈,才能有真正的进步吧。
转载自:https://juejin.cn/post/7250283546367393852