Server Development
Extend OpenIMServer through API, RPC, and storage layers.
Extend OpenIMSDK
- Before developing new capabilities for OpenIMSDK, determine whether the requirement belongs to the business extension layer or core IM logic.
- OpenIMSDK provides comprehensive abstractions for standard instant messaging features; modifying core implementations directly is generally discouraged.
- When you do need to extend core IM capabilities, follow the workflow described below and submit a pull request to the upstream repository to facilitate long-term maintenance.
OpenIMServer
OpenIMServer handles real-time message pipelines over persistent WebSocket connections (entry point in/internal/msggateway) and REST API business logic over short-lived HTTP connections (entry point in/internal/api/).
The following walkthrough uses adding anAddFriendCategoryfeature as an example to explain how to modify the API, RPC, and Storage layers in sequence.
Prerequisites
- Prepare your development environment:
- Go version 1.22 or later. Refer to the official Go installation guide.
protoc-gen-goversion 1.36.1 or later, andprotoc-gen-go-grpcversion 1.5.1 or later. Refer to the official gRPC documentation.protocversion 5.29.2 or later, with the binary accessible in your systemPATH. Refer to the Protocol Buffers download page.- Fork the protocol repository required by OpenIMServer:
- Official repository: github.com/openimsdk/protocol
OpenIMServer imports protobuf protocol definitions from github.com/openimsdk/protocol. When you need to modify protocol definitions, fork this repository, add the new schema definitions, and point OpenIMServer to your fork in go.mod:
replace github.com/openimsdk/protocol => github.com/<your-username>/protocolAdd and generate Protobuf protocols
Define the Proto schema
The following example adds AddFriendCategory to relation/relation.proto in the Friend module:
syntax = "proto3";
package openim.relation;
option go_package = "github.com/openimsdk/protocol/relation";
message AddFriendCategoryReq {
string ownerUserID = 1;
string friendUserID = 2;
int32 category = 3;
}
message AddFriendCategoryResp {
}
service Friend {
rpc AddFriendCategory(AddFriendCategoryReq) returns (AddFriendCategoryResp);
}This definition includes the request parameters, an empty response, and the new RPC method.
Generate Go code
- Install Mage. OpenIMServer standardizes development commands with Mage to eliminate cross-platform shell script discrepancies:
go install github.com/magefile/mage@latest- Run
mage InstallDependin the protocol repository to install required dependencies. - Modify your proto files, then run
mage GenGoto generate Go code. - For additional details, refer to Generating PB files with Mage.
Add parameter validation
OpenIMServer avoids using proto tags and reflection plugins for parameter validation. Instead, add a Go file in the generated protobuf directory and implement a Check method on the request struct:
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
}Add API endpoints
Define routes
Routes are defined in /internal/api/router.go. Register the new endpoint in the friend route group inside newGinRouter:
{
f := NewFriendApi(relation.NewFriendClient(friendConn))
friendRouterGroup := r.Group("/friend")
friendRouterGroup.POST("/delete_friend", f.DeleteFriend)
// ...
friendRouterGroup.POST("/add_friend_category", f.AddFriendCategory)
}If the endpoint belongs to an existing group, append it directly; otherwise, create a new route group file modeled after existing modules.
Implement the API handler
Implement the handler in /internal/api/friend/friend.go. If the incoming JSON payload perfectly matches the RPC request struct, invoke a2r.Call directly; otherwise, manually parse the request and invoke the gRPC client (refer to SendMessage in the Message module as a reference):
func (o *FriendApi) AddFriendCategory(c *gin.Context) {
a2r.Call(c, relation.FriendClient.AddFriendCategory, o.client)
}Add RPC methods
Implement the new gRPC method on the corresponding module's Server struct. When database updates or inserts require real-time synchronization with OpenIMClientSDK, follow the notification pattern used in s.notificationSender.FriendsInfoUpdateNotification.
Implement business logic
Implement AddFriendCategory in internal/rpc/relation/friend/friend.go:
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
}
if _, err := s.db.FindFriendsWithError(
ctx,
req.OwnerUserID,
[]string{req.FriendUserID},
); err != nil {
return nil, err
}
if err := s.db.AddFriendCategory(
ctx,
req.OwnerUserID,
req.FriendUserID,
req.Category,
); err != nil {
return nil, err
}
s.notification.FriendCategoryAddNotification(
ctx,
req.OwnerUserID,
req.FriendUserID,
)
return &relation.AddFriendCategoryResp{}, nil
}Implement the notification helper in 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,
)
}Register the notification type in constant/constant.go within your protocol repository:
const (
FriendApplicationApprovedNotification = 1201
// ...
FriendCategoryAddNotification = 1211
)Next, add the new field to FriendInfo in sdkws/sdkws.proto and regenerate sdkws/sdkws.pb.go:
message FriendInfo {
string ownerUserID = 1;
string remark = 2;
// ...
int32 category = 9;
}Add Storage layer interfaces
The OpenIMServer Storage layer is organized into three distinct tiers:
- Controller: Orchestrates transactions, cache invalidation, and domain model mapping.
- Cache: Caches high-frequency data to reduce database load.
- Database: Executes persistent data read and write operations.
Controller tier
Extend the interface and implement the method in pkg/common/storage/controller/friend.go:
type FriendDatabase interface {
CheckIn(
ctx context.Context,
user1,
user2 string,
) (inUser1Friends bool, inUser2Friends bool, err error)
// ...
AddFriendCategory(
ctx context.Context,
ownerUserID,
friendUserID string,
category int,
) error
}
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 tier
Declare cache interfaces in pkg/common/storage/cache, manage cache keys in pkg/common/storage/cache/cachekey, and provide the implementations required by the Controller tier. For this example, you can reuse the existing DeleteFriend cache method.
OpenIMServer follows a "delete on write, populate on read" caching strategy: invalidate the cached entry when the database updates, and re-cache data during subsequent reads.
Database tier
Add the new field to the database model in pkg/common/storage/model/friend.go:
type Friend struct {
ID primitive.ObjectID `bson:"_id"`
OwnerUserID string `bson:"owner_user_id"`
// ...
Category int `bson:"category"`
}Extend the interface in pkg/common/storage/database/friend.go:
type Friend interface {
UpdateRemark(
ctx context.Context,
ownerUserID,
friendUserID,
remark string,
) error
// ...
AddFriendCategory(
ctx context.Context,
ownerUserID,
friendUserID string,
category int,
) error
}Finally, implement the database query in pkg/common/storage/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},
)
}OpenIMClientSDK
The cross-platform foundation of OpenIMClientSDK is OpenIMClientSDK Core. It implements core client-side IM capabilities:
- Network connection lifecycle: Maintains stable, persistent WebSocket connections with OpenIMServer.
- Message receipt and storage: Ingests incoming messages and persists them in the local SQLite database.
- Relationship and group management: Manages friendship relations, blocklists, group profiles, and group member records.
- Cross-platform parity: Delivers identical core behavior across Android, iOS, Windows, macOS, and Web platforms.
Define Server API callers
When the new client method requires invoking an OpenIMServer endpoint, register the API declaration in server_api.
Declare the API client in pkg/api/api.go:
var (
AddFriend = newApi[
relation.ApplyToAddFriendReq,
relation.ApplyToAddFriendResp,
]("/friend/add_friend")
// ...
AddFriendCategory = newApi[
relation.AddFriendCategoryReq,
relation.AddFriendCategoryResp,
]("/friend/add_friend_category")
)relation.AddFriendCategoryReq is imported from the shared protocol repository; OpenIMClientSDK Core depends on the identical protocol definitions.
Add the caller method in 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)
}Implement SDK business logic
Implement the public SDK method in internal/relation/api.go. Requests and responses can use protobuf structs or custom structs with JSON tags:
func (r *Relation) AddFriendCategory(
ctx context.Context,
req *sdkpb.AddFriendCategoryReq,
) (*sdkpb.AddFriendCategoryResp, error) {
serverReq := &relation.AddFriendCategoryReq{
OwnerUserID: r.loginUserID,
FriendUserID: req.FriendUserID,
Category: req.Category,
}
if err := r.AddFriendCategory(ctx, serverReq); 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
}Handle OpenIMServer notifications
Process server push notifications in 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 &&
tips.FromToUserID.FromUserID == r.loginUserID {
return r.IncrSyncFriends(ctx)
}
}
return nil
}Because IncrSyncFriends writes updated data into the local database, map the newly introduced field in internal/relation/conversion.go:
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: info.Category,
}
}Update the local database
- Extend the interface in
pkg/db/db_interface/databse.gocalled by OpenIMClientSDK. For this example, reuseUpdateFriend. - Add the field to
LocalFriendinpkg/db/model_struct/data_model_struct.go:
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 int32 `gorm:"column:category" json:"category"`
}- Implement the persistence logic in
pkg/db/friend_model.go.
Export cross-language bindings
OpenIMClientSDK Core is built with Go. Once the core logic is complete, export bindings for target client languages:
open_im_sdk/: Function interface binding layer.open_im_sdk_callback/: Callback signature definition layer.
Android and iOS use gomobile to compile native AAR and xcframework artifacts. Refer to the OpenIMClientSDK Core repository for build instructions.
For example, export the method in open_im_sdk/relation.go:
func AddFriendCategory(
callback open_im_sdk_callback.Base,
operationID string,
req string,
) {
call(
callback,
operationID,
UserForSDK.Relation().AddFriendCategory,
req,
)
}OpenIMClientSDK Core also provides C bindings for other cross-platform integrations, maintained in the OpenIMClientSDK C++ repository. When integrating via C interfaces in other languages, refer to the wrapper patterns in that repository.