如何进行二次开发
如果您需要基于 OpenIM 开发新特性,首先要确定是针对业务侧还是即时通讯核心逻辑。
由于 OpenIM 系统本身已经做好了比较多的抽象,大部分聊天的功能已经具备了,不建议修改 IM 本身。
如果需要增加 IM 的能力,可以参考以下流程,并提交 PR,以保证未来代码统一性。
服务器
OpenIMServer 主要分为长短连接接口,长连接接口主要是 IM 消息的核心逻辑(逻辑入口位于/internal/msggateway),短连接接口主要是 IM 的 业务逻辑(逻辑入口位于/internal/api/),下面具体介绍如何在 IM 中加上新的业务功能。
- 开发前提
搭建环境
搭建 Go 环境,推荐 Go 版本 >= 1.22,参考Go 官方文档
搭建 grpc 环境,推荐 proto-gen-go >=1.36.1,protoc-gen-go-grpc >= 1.5.1 ,参考grpc 官方文档
搭建 proto 环境,推荐 protoc >= 5.29.2,将其二进制文件存放到环境变量,参考proto 官方文档
fork OpenIMServer 依赖的外部仓库 protocol
clone 官方的后台协议仓库: github.com/openimsdk/protocol
注意:IMServer 使用的 protobuf 协议以依赖仓库的形式在 github.com/openimsdk/protocol 中,如果需要修改协议,需要先 fork protocol 仓库, 然后在此仓库上增加新的接口协议,然后在 OpenIMServer 的 go.mod 中引用新的包路径,通过:
replace github.com/openimsdk/protocol => github.com//protocol
其中 your_protocol_path 为你 fork 的 protocol 仓库所在的本地路径。
- Protobuf 协议增加与生成
下面以 Go 为例,介绍如何完整的生成一个新的接口协议。
编写 proto 文件
首先根据业务需求,定义一个新的功能。本文以在 Friend 模块添加一个 AddFriendCategory 为例,我们需要在 Friend 模块的 proto 文件,添加对应的功能,文件在 relation/relation.proto。
编写 proto 文件,定义新的 AddFriendCategory 接口方法,如:
syntax = "proto3";
package openim.relation;
option go_package = "http://github.com/openimsdk/protocol/relation";
// 定义 AddFriendCategory 的请求参数
message AddFriendCategoryReq {
string ownerUserID = 1;
string friendUserID = 2;
int category = 3;
}
// 定义 AddFriendCategory 的响应参数
// 如果无需返回参数,则不需要添加定义。错误码和错误信息已经默认定义。
message AddFriendCategoryResp {
}
// 定义一个 Friend 模块的 RPC 服务
service Friend {
// 定义一个 AddFriendCategory 的 RPC 方法
rpc AddFriendCategory(AddFriendCategoryReq) returns (AddFriendCategoryResp);
}
这里面分别定义了一个请求参数 AddFriendCategoryReq,一个响应参数 AddFriendCategoryResp,以及一个 RPC 服务 Friend,其中包含的新增 RPC 方法 AddFriendCategory。
上面这个主要的关注点为:
定义 RPC 方法的请求参数 -> 定义 RPC 方法的响应参数 -> 在 RPC 服务内定义 RPC 方法。
生成 Go 代码
下面介绍如何在编写 proto 文件后,生成对应的 Go 的 pb 代码。
安装执行命令的工具 mage,执行 go install github.com/magefile/mage@latest 即可安装。
在对应仓库中执行 mage InstallDepend,安装 Go 所需的依赖。
proto 编辑完毕后,在克隆的 protocol 仓库中直接执行 mage GenGo 即可生成对应的 go 代码。
更多内容,具体参考用 mage 生成 PB 文件。
添加校验函数
如果需要对 RPC 函数的请求添加校验,同样在 protocol 仓库中添加。
例如我们定义的 AddFriendCategory 接口,需在 relation/relation.go 中增加如下代码:
func (x *AddFriendCategoryReq) Check() error {
if x.OwnerUserID == "" {
return errors.New("OwnerUserID is empty")
}
if x.FriendUserID == "" {
return errors.New("FriendUserID is empty")
}
if x.Category == 0 {
return errors.New("Category is empty")
}
return nil
}
- API 功能添加
添加新的 API 功能,包括路由定义和接口定义。
API 路由定义
定义路由的文件在 /internal/api/router.go,我们需要在 newGinRouter 函数中定义对应的路由,如: 例如我们要定义一个 Friend 模块的 AddFriendCategory 接口,我们可以在 newGinRouter 函数中增加如下代码:
// friend routing group
{
f := NewFriendApi(relation.NewFriendClient(friendConn))
friendRouterGroup := r.Group("/friend")
friendRouterGroup.POST("/delete_friend", f.DeleteFriend)
// ......
// 新增 AddFriendCategory 接口的路由
friendRouterGroup.POST("/add_friend_category", f.AddFriendCategory)
}
如果增加的接口属于一个路由组,可直接增加到对应的路由组文件中,否则模仿创建新的路由组文件。
API 接口定义
根据上面的路由定义,我们需要在 /internal/api/friend/friend.go 中增加对应的接口定义。
如果 API 的 JSON 请求与 RPC 的 Request 请求一致,可以直接调用 a2r.Call 函数。否则需要自己解析 JSON 请求,然后调用 gRPC 接口(可参考 Message 模块的 SendMessage 接口)。 例如:
// 当 API 的 Request 与 JSON 请求一致
func (o FriendApi) AddFriendCategory(c gin.Context) {
// AddFriendCategory 为在 RPC 定义的方法
a2r.Call(c,relation.FriendClient.AddFriendCategory, o.client)
}
- 添加 RPC 方法
在对应模块的 Server 结构体,新增相应的 gRPC 方法来实现 Server 接口。然后编写主体的业务逻辑。
其中涉及 DB 更新、插入操作需要下发 SDK 实时通知,可直接模仿 s.notificationSender.FriendsInfoUpdateNotification 这种类型的通知下发函数。(sdk 对应需要处理新的通知)
添加新的 RPC 方法
在 internal/rpc/relation/friend/friend.go 中增加新的 rpc 方法 AddFriendCategory,并编写主体的业务逻辑。
// AddFriendCategory 添加好友分组
func (s friendServer) AddFriendCategory(ctx context.Context, req relation.AddFriendCategoryReq) (*relation.AddFriendCategoryResp, error) {
// 实现具体的业务逻辑
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
return nil, err
}
_, err = s.db.FindFriendsWithError(ctx, req.OwnerUserID, []string{req.FriendUserID})
if err != nil {
return nil, err
}
// 调用 DB 操作
if err := s.db.AddFriendCategory(ctx,req.OwnerUserID, req.FriendUserID,req.category); err != nil {
return nil, err
}
// 调用 sdk 下发通知(如果有对应的 DB 操作)
s.notification.FriendCategoryAddNotification(ctx, req.OwnerUserID, req.FriendUserID) // 仅举例,具体通知函数需要根据业务需求实现
return &relation.AddFriendCategoryResp{}, nil
}
对应的通知下发函数 FriendCategoryAddNotification 应在 internal/rpc/relation/notification.go 中实现。
func (f *FriendNotificationSender) FriendCategoryAddNotification(ctx context.Context,fromUserID, toUserID string) {
tips := sdkws.FriendInfoChangedTips{FromToUserID: &sdkws.FromToUserID{}}
tips.FromToUserID.FromUserID = fromUserID
tips.FromToUserID.ToUserID = toUserID
f.setSortVersion(ctx, &tips.FriendVersion, &tips.FriendVersionID, database.FriendVersionName, toUserID, &tips.FriendSortVersion)
f.Notification(ctx, fromUserID, toUserID, constant.FriendCategoryAddNotification, &tips)
}
此处调用的 constant.FriendCategoryAddNotification 需要添加到 protocol 仓库下的 constant/constant.go 中定义。
const(
FriendApplicationApprovedNotification = 1201 // add_friend_response
// ...
// 新增 FriendCategoryAddNotification 常量
FriendCategoryAddNotification = 1211
)
并且需要更新 sdkws/sdkws.proto 中的对应字段。且在编写完后执行命令,重新生成对应的 sdkws/sdkws.pb.go 文件。
message FriendInfo {
string ownerUserID = 1;
string remark = 2;
// ...
// 新增 Category 字段
int32 category = 9;
}
- 添加存储层接口
存储层主要分为三层
controller:主要用于数据库事务处理和 cache 整合的逻辑控制层
cache:主要为 db 的数据缓存
database:数据持久化层,用于业务逻辑的存储
添加 controller 层接口
在 pkg/common/storage/controller 中,增加新的接口,实现对应的接口,提供给 RPC 逻辑层调用。
例如我们定义的 AddFriendCategory 接口,需在 pkg/common/storage/controller/friend.go 中增加如下代码:
type FriendDatabase interface {
CheckIn(ctx context.Context, user1, user2 string) (inUser1Friends bool, inUser2Friends bool, err error)
// ...
// 定义 Controller 层的 AddFriendCategory 接口
AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error
}
// 实现 AddFriendCategory 接口
func (f *FriendDatabase) AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error {
// 实现对应的业务逻辑,如数据转换等。
if err := f.friend.AddFriendCategory(ctx, ownerUserID, friendUserID, category); err != nil {
return err
}
return f.cache.DeleteFriend(ownerUserID, friendUserID).DelMaxFriendVersion(ownerUserID).ChainExecDel(ctx)
}
添加 cache 层接口
在 pkg/common/storage/cache 中增加新的接口,在 pkg/common/storage/cache/cachekey 中实现对应的 Key,并实现对应的接口,提供给 controller 层调用。
我们定义的 AddFriendCategory 接口,可以直接调用 cache 层已有的 DeleteFriend 接口即可。
Notice: cache 层通常是在更新时删除缓存,当获取数据时再去更新数据写入缓存。采用了写时删除,读时更新的策略。
添加 database 层接口
在 pkg/common/storage/model 中,定义对应数据库的 model 结构体,然后在 pkg/common/storage/database 中增加新的接口,并实现对应的接口,提供给 cache 层整合。
例如,我们定义的 AddFriendCategory 接口,需要在 pkg/common/storage/model/friend.go 中定义对应的 model 结构体添加对应字段, 然后在 pkg/common/storage/database/friend.go 中添加对应的接口供 cache 层整合,在 pkg/common/storage/database/mgo/friend.go 中实现对应的数据库操作。
model/friend.go
type Friend struct {
ID primitive.ObjectID bson:"_id"
OwnerUserID string bson:"owner_user_id"
// ...
Category int bson:"category"
// 新增 Category 字段
}
database/friend.go
type Friend interface {
UpdateRemark(ctx context.Context, ownerUserID, friendUserID, remark string) (err error)
// ...
// 定义 DB 层的 AddFriendCategory 接口
AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error
}
database/mgo/friend.go
func (f *FriendMgo) AddFriendCategory(ctx context.Context, ownerUserID, friendUserID string, category int) error{
return f.UpdateByMap(ctx, ownerUserID, friendUserID, map[string]any{"category": category})
}
客户端
客户端的主要核心是 OpenIM SDK Core,负责管理 WebSocket 长连接、提供事件的处理回调机制。
SDK Core 接口添加
定义 Server API 接口
如果新增的方法需要调用服务端的接口,需要在 server_api 中定义对应的接口方法。
例如我们定义的 AddFriendCategory 接口,需添加对应内容:
在 pkg/api/api.go 中定义对应的 Server API 调用变量:
// relation
var(
AddFriend = newApirelation.ApplyToAddFriendReq, relation.ApplyToAddFriendResp
// ...
// 定义 AddFriendCategory 接口
AddFriendCategory = newApirelation.AddFriendCategoryReq, relation.AddFriendCategoryResp
)
在 relation/server_api.go 中添加对应内容:
func (r Relation) AddFriendCategory(ctx context.Context, req relation.AddFriendCategoryReq) error {
// 实现对应的逻辑和数据转换
req.OwnerUserID = r.loginUserID
return api.AddFriendCategory.Execute(ctx, req)
}
将这个接口定义到 open_im_sdk/relation.go 中,以便下游 SDK 调用。
func AddFriendCategory(callback open_im_sdk_callback.Base, operationID string, req string){
call(callback, operationID, UserForSDK.Relation().AddFriendCategory, req)
}
定义 SDK 对应方法
在相应模块的 api.go 中定义对应的方法,如:
我们需要在 internal/relation/api.go 中实现对应的逻辑方法:
func (r Relation) AddFriendCategory(ctx context.Context, req sdkpb.AddFriendCategoryReq) (*sdkpb.AddFriendCategoryResp, error) {
// 调用 Server API 的接口
sReq:= &relation.AddFriendCategoryReq{ OwnerUserID: r.loginUserID, FriendUserID: req.friendUserID, Category: req.Category}
if err := r.AddFriendCategory(ctx,sReq) ; err != nil {
return nil, err
}
r.relationSyncMutex.Lock()
defer r.relationSyncMutex.Unlock()
if err := r.IncrSyncFriends(ctx); err != nil {
return nil, err
}
return &sdkpb.AddFriendCategoryResp, nil
}
处理 Server 下发通知
我们需要对 Server 下发的通知进行处理,需要在 internal/relation/notification.go 中实现对应的通知处理方法。
例如我们定义的 FriendCategoryAddNotification 接口,需在 internal/relation/notification.go 中增加如下代码:
func (r Relation) doNotification(ctx context.Context, msg sdkws.MsgData) error {
r.relationSyncMutex.Lock()
defer r.relationSyncMutex.Unlock()
switch msg.ContentType {
case constant.FriendRemarkSetNotification:
// ...
// 添加对应的通知处理
case constant.FriendCategoryAddNotification:
var tips sdkws.FriendCategoryAddTips // 定义对应的通知结构体
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID != nil {
if tips.FromToUserID.FromUserID == r.loginUserID {
// 包含回调的方法
return r.IncrSyncFriends(ctx)
}
}
}
}
在 IncrSyncFriends 的方法需要写入本地 DB 中,所以需要将更新转换函数的内容: 更新 internal/relation/conversion.go 中的 ServerFriendToLocalFriend 函数。
func ServerFriendToLocalFriend(info sdkws.FriendInfo) model_struct.LocalFriend {
return &model_struct.LocalFriend{
OwnerUserID: info.OwnerUserID,
FriendUserID: info.FriendUser.UserID,
Remark: info.Remark,
CreateTime: info.CreateTime,
AddSource: info.AddSource,
OperatorUserID: info.OperatorUserID,
Nickname: info.FriendUser.Nickname,
FaceURL: info.FriendUser.FaceURL,
Ex: info.Ex,
IsPinned: info.IsPinned,
// 新增 Category 字段
Category: info.Category,
}
}
处理本地 DB 层
如果涉及到 db 操作,需要调用 db 层的接口,更新本地的 db 数据。
在 pkg/db/db_interface/databse.go 添加接口方法 供 sdk 调用。
此处使用的是现有的 UpdateFriend 方法来实现。
更新 pkg/db/model_struct/data_model_struct.go对应的 LocalFriend 结构体
在 pkg/db/model_struct/data_model_struct.go 中的 LocalFriend 结构体中添加对应的字段:
type LocalFriend struct {
OwnerUserID string gorm:"column:owner_user_id;primary_key;type:varchar(64)" json:"ownerUserID"
FriendUserID string gorm:"column:friend_user_id;primary_key;type:varchar(64)" json:"userID"
Remark string gorm:"column:remark;type:varchar(255)" json:"remark"
// ...
// 添加 Category 字段
Category int32 gorm:"column:category" json:"category"
}
在 pkg/db/friend_model.go中,添加具体实现方法。
此处调用了已存在的 UpdateFriend 方法来实现。