|
1 | 1 | --- |
2 | 2 | title: '如何进行二次开发' |
3 | 3 | sidebar_position: 6 |
4 | | - |
5 | 4 | --- |
6 | 5 |
|
7 | | - |
8 | 6 | # 如何进行二次开发 |
9 | | -- 如果您需要基于OpenIM开发新特性,首先要确定是针对业务侧还是即时通讯核心逻辑。 |
10 | | -- 由于OpenIM系统本身已经做好了比较多的抽象,大部分聊天的功能已经具备了,不建议修改IM本身。 |
11 | | -- 如果需要增加IM的能力,可以参考以下流程,并提交PR,以保证未来代码统一性。 |
12 | 7 |
|
| 8 | +- 如果您需要基于 OpenIM 开发新特性,首先要确定是针对业务侧还是即时通讯核心逻辑。 |
| 9 | +- 由于 OpenIM 系统本身已经做好了比较多的抽象,大部分聊天的功能已经具备了,不建议修改 IM 本身。 |
| 10 | +- 如果需要增加 IM 的能力,可以参考以下流程,并提交 PR,以保证未来代码统一性。 |
| 11 | + |
| 12 | +# 服务器 |
| 13 | + |
| 14 | +> OpenIMServer 主要分为长短连接接口,长连接接口主要是 IM 消息的核心逻辑(逻辑入口位于/internal/msggateway),短连接接口主要是 IM 的 |
| 15 | +> 业务逻辑(逻辑入口位于/internal/api/),下面具体介绍如何在 IM 中加上新的业务功能。 |
| 16 | +
|
| 17 | +## 1. 开发前提 |
| 18 | + |
| 19 | +- 搭建环境 |
| 20 | + - 搭建 Go 环境,参考[Go 官方文档](https://golang.org/doc/install) |
| 21 | + - 搭建 grpc 环境,参考[grpc 官方文档](https://grpc.io/docs/languages/go/quickstart/) |
| 22 | + |
| 23 | +* fork OpenIMServer 依赖的外部仓库 protocol |
| 24 | + |
| 25 | + - clone 官方的后台协议仓库: [github.com/openimsdk/protocol](https://github.com/openimsdk/protocol) |
| 26 | + |
| 27 | + **注意**:IMServer 使用的 protobuf 协议以依赖仓库的形式在 `github.com/openimsdk/protocol` 中,如果需要修改协议,需要先 fork protocol 仓库, |
| 28 | + 然后在此仓库上增加新的接口协议,然后在 OpenIMServer 的 `go.mod` 中引用新的包路径,通过: |
| 29 | + |
| 30 | + `replace github.com/openimsdk/protocol => ./your_protocol_path` |
| 31 | + |
| 32 | + 其中 `your_protocol_path` 为你 fork 的 protocol 仓库所在的本地路径。 |
| 33 | + |
| 34 | +## 2. Protobuf 协议增加与生成 |
| 35 | + |
| 36 | +下面以 Go 为例,介绍如何完整的生成一个新的接口协议。 |
| 37 | + |
| 38 | +### 编写 proto 文件 |
| 39 | + |
| 40 | +- 首先根据业务需求,定义一个新的模块。本文以 `hello` 为例,创建对应的 module 文件夹和 proto 文件,如 `hello/hello.proto`。 |
| 41 | +- 编写 proto 文件,定义新的接口方法,如: |
| 42 | + |
| 43 | +```proto |
| 44 | +syntax = "proto3"; |
| 45 | +
|
| 46 | +package openim.hello; |
| 47 | +
|
| 48 | +// 定义生成的 go 语言包名,通常为 github.com/openimsdk/protocol/<module>,其中 module 为具体的模块名称。 |
| 49 | +option go_package = "github.com/openimsdk/protocol/hello"; |
| 50 | +
|
| 51 | +// 定义 Hello 的请求参数 |
| 52 | +message HelloRequest { |
| 53 | + string name = 1; |
| 54 | +} |
| 55 | +
|
| 56 | +// 定义 Hello 的响应参数 |
| 57 | +message HelloResponse { |
| 58 | + string message = 1; |
| 59 | + UserInfo user = 2; // 引用自定义的 message 结构 |
| 60 | +} |
| 61 | +
|
| 62 | +// 自定义 message 结构 |
| 63 | +message UserInfo { |
| 64 | + string name = 1; |
| 65 | + int32 age = 2; |
| 66 | +} |
13 | 67 |
|
14 | | -## 服务器 |
15 | | -> OpenIMServer主要分为长短连接接口,长连接接口主要是IM消息的核心逻辑(逻辑入口位于/internal/msggateway),短连接接口主要是IM的 |
16 | | -业务逻辑(逻辑入口位于/internal/api/),下面具体介绍如何在IM中加上新的业务功能。 |
| 68 | +// 定义一个 Hello 模块的 RPC 服务 |
| 69 | +service HelloService { |
| 70 | + // 定义一个 SayHello 的 RPC 方法 |
| 71 | + rpc SayHello(HelloRequest) returns (HelloResponse); |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +这里面分别定义了一个请求参数 `HelloRequest`,一个响应参数 `HelloResponse`,一个自定义的结构体 `UserInfo`,以及一个 RPC 服务 `HelloService`,其中包含的 RPC 方法 `SayHello`。 |
| 76 | + |
| 77 | +上面这个主要的关注点为: |
| 78 | +定义生成的 go_package 路径 -> 定义 RPC 方法的请求和响应 message 结构,如果有需要定义的通用结构可以单独定义 message 结构(如 `UserInfo`)-> 定义 RPC 服务和方法。 |
| 79 | + |
| 80 | +### 生成 Go 代码 |
| 81 | + |
| 82 | +下面介绍如何在编写 proto 文件后,生成对应的 Go 的 pb 代码。 |
| 83 | + |
| 84 | +- 安装执行命令的工具 mage,执行 `go install github.com/magefile/mage@latest` 即可安装。 |
| 85 | +- 在对应仓库中执行 `mage InstallDepend`,安装 Go 所需的依赖。 |
| 86 | +- proto 编辑完毕后,在克隆的 protocol 仓库中直接执行 `mage GenGo` 即可生成对应的 go 代码。 |
| 87 | +- 更多内容,具体参考[用 mage 生成 PB 文件](https://github.com/openimsdk/protocol/blob/main/mage-README.md)。 |
17 | 88 |
|
| 89 | +## 3. API 功能添加 |
18 | 90 |
|
19 | | -### 1、开发前提 |
20 | | - - 搭建环境 |
21 | | - - 搭建Go环境,参考[Go官方文档](https://golang.org/doc/install) |
22 | | - - 搭建grpc环境,参考[grpc官方文档](https://grpc.io/docs/languages/go/quickstart/) |
23 | | - + fork OpenIMServer依赖的外部仓库 |
24 | | - - clone官方的后台协议仓库: github.com/openimsdk/protocol |
| 91 | +添加新的 API 功能,包括路由定义和接口定义。 |
25 | 92 |
|
26 | | - **注**:IMServer使用的protobuf协议以依赖仓库的形式在github.com/openimsdk/protocol中,如果需要修改协议,需要先fork protocol仓库, |
27 | | - 然后在此仓库上增加新的接口协议,然后在OpenIMServer的go.mod中引用新的协议通过: |
| 93 | +### API 路由定义 |
28 | 94 |
|
29 | | - `replace github.com/openimsdk/protocol => ./your_protocol_path` |
30 | | -### 2、协议增加与生成 |
31 | | - - 编写proto文件,定义新的接口协议 |
32 | | - - 生成go代码 |
33 | | - - proto编辑完毕后在克隆的protocol仓库中直接执行`gen.cmd`或者`gen.sh`即可生成go代码 |
34 | | -### 3、api功能添加 |
35 | | - - 在/internal/api/router.go文件中增加新的接口包括定义路由,如果增加的接口属于一个路由组,可直接增加到对应的路由组文件中,否则模仿创建新的路由组文件。 |
36 | | - - 在/internal/api/xxx.go中如果api的json请求和rpc的request请求一致,可以直接调用a2r.Call函数,否则需要自己解析json请求,然后调用grpc接口(可模仿SendMessage接口)。 |
37 | | -### 4、rpc功能添加 |
| 95 | +- 定义路由的文件在 `/internal/api/router.go`,我们需要在 `newGinRouter` 函数中定义对应的路由,如: |
| 96 | + 例如我们要定义一个 Friend 模块的 `AddFriendCategory` 接口,我们可以在 `newGinRouter` 函数中增加如下代码: |
| 97 | + |
| 98 | +```go |
| 99 | + // friend routing group |
| 100 | + { |
| 101 | + f := NewFriendApi(relation.NewFriendClient(friendConn)) |
| 102 | + friendRouterGroup := r.Group("/friend") |
| 103 | + friendRouterGroup.POST("/delete_friend", f.DeleteFriend) |
| 104 | + // ...... |
| 105 | + |
| 106 | + // 新增 AddFriendCategory 接口的路由 |
| 107 | + friendRouterGroup.POST("/add_friend_category", f.AddFriendCategory) |
| 108 | + } |
38 | 109 | ``` |
39 | | - 举例:在/internal/rpc/group/group.go中增加新的rpc函数,使用groupServer结构体实现对应的grpc的server接口,然后编写主体业务逻辑,其中涉及db更新 |
40 | | - 插入操作需要下发sdk实时通知,可直接模仿 g.notification.GroupApplicationAgreeMemberEnterNotification这种类型的通知下发函数(sdk对应需要处理新的通知) |
| 110 | + |
| 111 | +如果增加的接口属于一个路由组,可直接增加到对应的路由组文件中,否则模仿创建新的路由组文件。 |
| 112 | + |
| 113 | +### API 接口定义 |
| 114 | + |
| 115 | +根据上面的路由定义,我们需要在 `/internal/api/friend/friend.go` 中增加对应的接口定义。 |
| 116 | +如果 API 的 JSON 请求与 RPC 的 Request 请求一致,可以直接调用 `a2r.Call` 函数,否则需要自己解析 JSON 请求,然后调用 gRPC 接口(可参考 Message 模块的 `SendMessage` 接口)。 |
| 117 | +例如: |
| 118 | + |
| 119 | +```go |
| 120 | + // 如果 API 的 Request 与 JSON 请求一致 |
| 121 | + func (o *FriendApi) AddFriendCategory(c *gin.Context) { |
| 122 | + // AddFriendCategory 为在 RPC 定义的方法 |
| 123 | + a2r.Call(c,relation.FriendClient.AddFriendCategory, o.client) |
| 124 | + } |
| 125 | + |
| 126 | + // 如果 API 的 Request 与 JSON 请求不一致,需要自己解析 JSON 请求 |
| 127 | + func (o *FriendApi) AddFriendCategory(c *gin.Context) { |
| 128 | + var req apistruct.AddFriendCategoryReq{} |
| 129 | + |
| 130 | + if err := c.BindJSON(&req); err != nil { |
| 131 | + apiresp.GinError(c,errs.ErrArgs.WithDetail(err.Error()).Wrap()) |
| 132 | + return |
| 133 | + } |
| 134 | + |
| 135 | + resp, err := o.client.AddFriendCategory(c, &req) |
| 136 | + if err != nil { |
| 137 | + apiresp.GinError(c,err) |
| 138 | + return |
| 139 | + } |
| 140 | + |
| 141 | + apiresp.GinSuccess(c, resp) |
| 142 | + } |
41 | 143 | ``` |
42 | | -### 5、存储层接口增加 |
| 144 | + |
| 145 | +## 4. 添加 RPC 方法 |
| 146 | + |
| 147 | +在对应模块的 Server 结构体,新增相应的 gRPC 方法来实现 Server 接口。然后编写主体的业务逻辑。 |
| 148 | +其中涉及 DB 更新、插入操作需要下发 SDK 实时通知,可直接模仿 `g.notification.GroupApplicationAgreeMemberEnterNotification` 这种类型的通知下发函数。(sdk 对应需要处理新的通知) |
| 149 | + |
| 150 | +### 添加新的 RPC 方法 |
| 151 | + |
| 152 | +在 `internal/rpc/relation/friend/friend.go` 中增加新的 rpc 方法 `AddFriendCategory`,并编写主体的业务逻辑。 |
| 153 | + |
| 154 | +```go |
| 155 | + |
| 156 | +// AddFriendCategory 添加好友分组 |
| 157 | + |
| 158 | +func (s *friendServer) AddFriendCategory(ctx context.Context, req *relation.AddFriendCategoryReq) (*relation.AddFriendCategoryResp, error) { |
| 159 | + |
| 160 | + // 实现具体的业务逻辑 |
| 161 | + // ... |
| 162 | + |
| 163 | + // 调用 DB 操作 |
| 164 | + if err := s.db.AddFriendCategory(req.UserId, req.CategoryName); err != nil { |
| 165 | + return nil, err |
| 166 | + } |
| 167 | + |
| 168 | + // 调用 sdk 下发通知(如果有对应的 DB 操作) |
| 169 | + s.notification.FriendCategoryAddNotification(req.UserId, req.CategoryName) // 仅举例,具体通知函数需要根据业务需求实现 |
| 170 | + |
| 171 | + return &relation.AddFriendCategoryResp{}, nil |
| 172 | +} |
| 173 | + |
| 174 | +``` |
| 175 | + |
| 176 | +对应的通知下发函数 `FriendCategoryAddNotification` 应在 `internal/rpc/relation/notification.go` 中实现。 |
| 177 | + |
| 178 | +```go |
| 179 | +func (f *FriendNotificationSender) FriendCategoryAddNotification(ctx context.Context, userID string, categoryName string) { |
| 180 | + tips := sdkws.FriendCategoryAddTips{UserID: userID, CategoryName: categoryName} |
| 181 | + f.Notification(ctx, mcontext.GetOpUserID(ctx), userID, constant.FriendCategoryAddNotification, &tips) |
| 182 | +} |
| 183 | + |
| 184 | +``` |
| 185 | + |
| 186 | +## 5. 添加存储层接口 |
| 187 | + |
43 | 188 | > 存储层主要分为三层 |
44 | | - - controller:主要用于数据库事务处理和cache整合的逻辑控制层 |
45 | | - - cache:主要为db的数据缓存 |
46 | | - - database:数据持久化层,用于业务逻辑的存储 |
| 189 | +> |
| 190 | +> - controller:主要用于数据库事务处理和 cache 整合的逻辑控制层 |
| 191 | +> - cache:主要为 db 的数据缓存 |
| 192 | +> - database:数据持久化层,用于业务逻辑的存储 |
| 193 | +
|
| 194 | +### 添加 controller 层接口 |
| 195 | + |
| 196 | +在 `pkg/common/storage/controller` 中,增加新的接口,实现对应的接口,提供给 RPC 逻辑层调用。 |
| 197 | + |
| 198 | +例如我们定义的 `AddFriendCategory` 接口,需在 `pkg/common/storage/controller/friend.go` 中增加如下代码: |
| 199 | + |
| 200 | +```go |
| 201 | + |
| 202 | +type friendDatabase struct { |
| 203 | + friend database.Friend |
| 204 | + cache cache.FriendCache |
| 205 | +} |
| 206 | + |
| 207 | +type FriendDatabase interface { |
| 208 | + CheckIn(ctx context.Context, user1, user2 string) (inUser1Friends bool, inUser2Friends bool, err error) |
| 209 | + // ... |
| 210 | + |
| 211 | + // 新增 AddFriendCategory 接口 |
| 212 | + AddFriendCategory(ctx context.Context, userID, categoryName string) error |
| 213 | +} |
| 214 | + |
| 215 | +// 实现 AddFriendCategory 接口 |
| 216 | + |
| 217 | +func (f *FriendDatabase) AddFriendCategory(ctx context.Context, userID, categoryName string) error { |
| 218 | + // 实现对应的业务逻辑,如数据转换等。 |
| 219 | + |
| 220 | + if err := f.friend.AddFriendCategory(ctx, userID, categoryName); err != nil { |
| 221 | + return err |
| 222 | + } |
| 223 | + |
| 224 | + return f.cache.AddFriendCategory(ctx, userID, categoryName) |
| 225 | +} |
| 226 | + |
| 227 | +``` |
| 228 | + |
| 229 | +### 添加 cache 层接口 |
| 230 | + |
| 231 | +在 `pkg/common/storage/cache` 中增加新的接口,在 `pkg/common/storage/cache/cachekey` 中实现对应的 Key,并实现对应的接口,提供给 controller 层调用。 |
| 232 | + |
| 233 | +例如我们定义的 `AddFriendCategory` 接口,需在 `pkg/common/storage/cache/cachekey/friend.go` 中实现其前缀和对应的 Get 函数, |
| 234 | +在 `pkg/common/storage/cache/friend.go` 定义供 controller 层调用的接口,并在 `pkg/common/storage/cache/redis/friend.go` 实现对应的缓存逻辑。 |
| 235 | + |
| 236 | +**cachekey/friend.go** |
| 237 | + |
| 238 | +```go |
| 239 | + |
| 240 | +const ( |
| 241 | + FriendCategoryKey = "FRIEND_CATEGORY:" |
| 242 | +) |
| 243 | + |
| 244 | +func GetFriendCategoryKey(userID, categoryName string) string { |
| 245 | + return FriendCategoryKey + userID + "-" + categoryName |
| 246 | +} |
47 | 247 | ``` |
48 | | - - 在pkg\common\storage\controller中增加新的接口,实现对应的接口,提供给rpc逻辑层调用。 |
49 | | - - 在pkg\common\storage\cache中增加新的接口,(pkg\common\storage\cache\cachekey中存储了缓存所有的key前缀)实现对应的接口, |
50 | | - 提供给controller层调用。 |
51 | | - - 在pkg\common\storage\model中可定义数据库的model结构体,pkg\common\storage\database中增加新的接口,实现对应的接口,提供给cache层整合。 |
| 248 | + |
| 249 | +**cache/friend.go** |
| 250 | + |
| 251 | +```go |
| 252 | +type FriendCache interface { |
| 253 | + BatchDeleter |
| 254 | + CloneFriendCache() FriendCache |
| 255 | + // ... |
| 256 | + |
| 257 | + // 新增 AddFriendCategory 接口 |
| 258 | + AddFriendCategory(ctx context.Context, userID, categoryName string) error |
| 259 | +} |
52 | 260 | ``` |
53 | 261 |
|
54 | | -## 客户端 |
| 262 | +**cache/redis/friend.go** |
55 | 263 |
|
| 264 | +```go |
| 265 | +func (f *FriendCacheRedis) AddFriendCategory(ctx context.Context, userID, categoryName string) error { |
| 266 | + // 实现对应的缓存逻辑 |
| 267 | + key := cachekey.GetFriendCategoryKey(userID, categoryName) |
| 268 | + return f.redis.Set(ctx, key) |
| 269 | +} |
| 270 | +``` |
56 | 271 |
|
| 272 | +### 添加 database 层接口 |
57 | 273 |
|
| 274 | +- 在 pkg/common/storage/model 中可定义数据库的 model 结构体,pkg/common/storage/database 中增加新的接口,实现对应的接口,提供给 cache 层整合。 |
58 | 275 |
|
| 276 | +# 客户端 |
59 | 277 |
|
| 278 | +``` |
| 279 | +
|
| 280 | +``` |
0 commit comments