Skip to main content

如何进行二次开发

  • 如果您需要基于 OpenIM 开发新特性,首先要确定是针对业务侧还是即时通讯核心逻辑。
  • 由于 OpenIM 系统本身已经做好了比较多的抽象,大部分聊天的功能已经具备了,不建议修改 IM 本身。
  • 如果需要增加 IM 的能力,可以参考以下流程,并提交 PR,以保证未来代码统一性。

服务器

OpenIMServer 主要分为长短连接接口,长连接接口主要是 IM 消息的核心逻辑(逻辑入口位于/internal/msggateway),短连接接口主要是 IM 的 业务逻辑(逻辑入口位于/internal/api/),下面具体介绍如何在 IM 中加上新的业务功能;

下面通过增加一个API 为例,介绍如何完整的从api => rpc => storage代码的增加修改。

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 依赖的外部仓库 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 仓库所在的路径。

2. Protobuf 协议增加与生成

编写 proto 文件

  • 首先根据业务需求,定义一个新的功能。本文以在 Friend 模块添加一个 AddFriendCategory 为例,我们需要在 Friend 模块的 proto 文件,添加对应的功能,文件在 relation/relation.proto
  • 编写 proto 文件,定义新的 AddFriendCategory 接口方法,如:
syntax = "proto3";

package openim.relation;

option go_package = "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

生成 Go 代码

下面介绍如何在编写 proto 文件后,生成对应的 Go 的 pb 代码。

  • 安装执行命令的工具 mage(openim使用mage执行各种命令从而避免使用脚本带来的跨平台兼容性问题),执行 go install github.com/magefile/mage@latest 即可安装。
  • 在对应仓库中执行 mage InstallDepend,安装 Go 所需的依赖。
  • proto 编辑完毕后,在克隆的 protocol 仓库中直接执行 mage GenGo 即可生成对应的 go 代码。
  • 更多内容,具体参考用 mage 生成 PB 文件

添加校验函数

API请求参数的校验,通常需要通过插件生成并且在proto文件中直接定义,openim并没有使用这种方式,而是在生成的pb协议文件目录中增加了一个文件,通过实现check函数的方式,定义实现每个参数的校验逻辑,这样更加直观,而且避免使用tag反射语法,性能更佳。

例如我们定义的 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
}

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

4. 添加 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;
}

5. 添加存储层接口

存储层主要分为三层

  • controller 层:负责数据库事务管理和缓存整合的业务逻辑控制层。它协调数据库操作和缓存系统,确保数据一致性和事务处理的正确性。
  • cache 层:作为数据库的缓存层,缓存热点数据,以减少频繁的数据库访问,提高系统性能。通过缓存策略和有效的缓存更新机制,确保数据访问的高效性。
  • database 层:负责持久化存储,处理业务逻辑中的数据存储需求。它通过数据库管理系统(如关系型数据库或 NoSQL)确保数据的持久性和一致性。

添加 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,该层全面负责 IM 系统的核心功能,包括但不限于:

  • 网络连接管理:实现与服务器的稳定、高效的 WebSocket 长连接,确保实时数据传输和通信流畅性。
  • 消息转发与存储:负责接收、转发和存储用户之间的即时消息,确保消息的准确、及时传递以及数据的持久化管理。
  • 好友与群组管理:提供灵活的好友管理与群组管理功能,支持好友添加、删除、群组创建与管理等操作,为社交交互提供完整的基础设施。
  • 跨平台支持:该 SDK 能够在多个平台(如 Android、iOS、Windows、Mac 等)上无缝运行,确保客户端在不同设备和操作系统上具有一致的表现和用户体验。

1、定义 Server API 接口

如果新增的方法需要调用服务端的接口,需要在 server_api 中定义对应的接口方法。

例如我们定义的 AddFriendCategory 接口,需添加对应内容:

  • pkg/api/api.go 中定义对应的 Server API 调用变量:

其中的relation.AddFriendCategoryReq为服务器上proto定义的协议文件,sdk-core也需要引用

// relation
var(
AddFriend = newApi[relation.ApplyToAddFriendReq, relation.ApplyToAddFriendResp]("/friend/add_friend")
// ...
// 定义 AddFriendCategory 接口
AddFriendCategory = newApi[relation.AddFriendCategoryReq, relation.AddFriendCategoryResp]("/friend/add_friend_category")
)
  • relation/server_api.go 中添加对应的server_api调用器:
func (r *Relation) AddFriendCategory(ctx context.Context, req *relation.AddFriendCategoryReq) error {
// 实现对应的逻辑和数据转换
req.OwnerUserID = r.loginUserID
return api.AddFriendCategory.Execute(ctx, req)
}

2、实现 SDK 接口业务逻辑

我们需要在 internal/relation/api.go 中实现对应的逻辑方法:

其中的req sdkpb.AddFriendCategoryReq和 sdkpb.AddFriendCategoryResp结构体可以使用pb,也可以自定义结构体,有json tag即可

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
}

3、处理 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,
}
}

4、处理本地 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 方法来实现。

5、导出层增加接口提供给其他语言使用

OpenIM SDK Core使用go语言开发,步骤2的逻辑增加完毕后,需要将相应的接口导出给到其他语言调用:

以Android和iOS为例,导出层的接口主要位于:

  • open_im_sdk/(函数接口层)
  • open_im_sdk_callback/(callback定义层,使用callback进行通信交互)

然后使用gomobille将其编译成Android的aar和iOS的xcframework库文件供其调用;具体关于OpenIM SDK Core如何利用gomobile编译成为Android的aar和iOS的xcframework可以参考仓库文档。

上面的例子将这个接口定义到 open_im_sdk/relation.go 中,以便提供给Android/iOS 调用。

func AddFriendCategory(callback open_im_sdk_callback.Base, operationID string, req string){
call(callback, operationID, UserForSDK.Relation().AddFriendCategory, req)
}

注:OpenIM SDK Core除开gomobile提供给Android/iOS调用,还提供c语言接口以便于更多语言进行跨平台调用,其主要实现在OpenIM SDK CPP这个仓库中,如果需要在其他语言利用c接口调用,可以参考该仓库的实现封装SDK。