如何进行二次开发
- 如果您需要基于 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
- clone 官方的后台协议仓库: 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 仓库所在的路径。
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。