写点什么

即时通讯开源项目 OpenIM 配置离线推送全攻略

作者:Geek_1ef48b
  • 2025-02-08
    湖南
  • 本文字数:6349 字

    阅读完需:约 21 分钟

如何进行二次开发如果您需要基于 OpenIM 开发新特性,首先要确定是针对业务侧还是即时通讯核心逻辑。由于 OpenIM 系统本身已经做好了比较多的抽象,大部分聊天的功能已经具备了,不建议修改 IM 本身。如果需要增加 IM 的能力,可以参考以下流程,并提交 PR,以保证未来代码统一性。服务器 OpenIMServer 主要分为长短连接接口,长连接接口主要是 IM 消息的核心逻辑(逻辑入口位于/internal/msggateway),短连接接口主要是 IM 的 业务逻辑(逻辑入口位于/internal/api/),下面具体介绍如何在 IM 中加上新的业务功能。


  1. 开发前提​搭建环境搭建 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 依赖的外部仓库 protocolclone 官方的后台协议仓库: github.com/openimsdk/protocol 注意:IMServer 使用的 protobuf 协议以依赖仓库的形式在 github.com/openimsdk/protocol 中,如果需要修改协议,需要先 fork protocol 仓库, 然后在此仓库上增加新的接口协议,然后在 OpenIMServer 的 go.mod 中引用新的包路径,通过:replace github.com/openimsdk/protocol => github.com/<your-username>/protocol 其中 your_protocol_path 为你 fork 的 protocol 仓库所在的本地路径。


  1. 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}


  1. 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)}


  1. 添加 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 = fromUserIDtips.FromToUserID.ToUserID = toUserIDf.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;}


  1. 添加存储层接口​存储层主要分为三层 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 调用变量:// relationvar(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.loginUserIDreturn 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 方法来实现。

用户头像

Geek_1ef48b

关注

还未添加个人签名 2021-08-25 加入

还未添加个人简介

评论

发布
暂无评论
即时通讯开源项目OpenIM配置离线推送全攻略_Geek_1ef48b_InfoQ写作社区