OpenIM源码分析

我需要的仅仅是基础的IM聊天功能。不想花那么多钱的情况下,OpenIM是一个不错的选择.

OpenIM源码分析
Photo by Mikhail / Unsplash

介绍OpenIM

研究这个框架,源于公司一个项目对于即时通讯的需要,之前涉及到这个需求,都采用商业方案,比如腾讯云提供的IM服务,环信等等,商业的云服务稳定,强大,要价高,但有的时候,我需要的仅仅是基础的IM聊天功能。不想花那么多钱的情况下,开源社区就是我最强大的后盾。OpenIM是由前微信技术专家打造的开源即时通讯解决方案,提供完整组件和一键部署能力,支持私有化部署和数据隐私保护,具备高性能、微服务、集群化特性,适用于企业级即时通讯需求。实际测试下,OpenIM对服务器的资源需求,真的不算大,官方介绍,核心服务仅需要4G内存。

部署结构分析

一、依赖服务安装

OpenIM已经准备好的依赖组件的Docker服务配置,位于项目open-im-server下。

$ git clone https://github.com/openimsdk/open-im-server && cd open-im-server

组件类别

核心依赖
运维组件
测试组件

组件清单

  • mongodb

  • redis

  • kafka

  • MinIO

  • etcd

  • prometheus

  • alertmanager

  • grafana


admin-front

web-front

是否必选
必选
可选
可选

依赖启动

$ docker compose up -d // 启动核心组件和测试前端
$ docker compose --profile m up -d // 启动运维组件

二、文档补充说明

  1. Minio的访问配置:Minio主要用于存储用户对话过程中的文件,而这些请求是直接进minio的,因此需要配置域名,或用户可以访问的IP地址。若是内网,给Minio一个内网IP即可。端口号默认Docker启动Minio使用的端口,映射到了主体的10005,具体查看minio.yml配置确认。
  2. Go的代理:golang语言没限制,不过golang依赖库的机制对github有非常强的依赖,大部分的依赖库都在github上,源码运行,需要配置go依赖的代理。

三、核心服务启动

在上面的准备基础完成之后,使用openim的指定的编译工具,mage start就可以直接启动核心服务了,这里可参考官网原文,不赘述。

如果自己的系统需要IM,那么到此为止,便可以基于OpenIM,来进行二次开发了。但如果你想直接上手试一下消息,就需要下面的AppServer演示服务接口。

四、AppServer演示接口服务

核心服务中,有USER的逻辑,但是却没有账号体系,因此上面的web-front和admin-front项目,可以打开,但却没办法登录,这需要另外的实现账号机制的后端的API服务。这个服务即AppServer,需要另外部署。

  • 服务获取:
$ git clone https://github.com/openimsdk/chat&& cd chat

如果您是在本地部署,那么admin-front的默认端口应该是11002,浏览器打开http://localhost:11002即可。

系统初始的管理员账号与密码是:chatAdmin/chatAdmin。

五、核心服务结构

  • 主服务:
    • openim-api:对客户端曝露的核心服务,http协议,信息查询,与消息发送请求。
    • openim-msggateway:对客户端曝露的核心服务,ws协议,处理客户端实时消息发送与监听。
    • openim-cmdutil
    • openim-crontask
    • openim-msgtransfer
    • openim-push
  • 底层服务
    • openim-rpc-auth
    • openim-rpc-conversation
    • openim-rpc-friend
    • openim-rpc-group
    • openim-rpc-msg
    • openim-rpc-third
    • openim-rpc-user

主服务与底层服务之间的关系如下图:

六、SDK结构

OpenIM的SDK支持多平台,核心使用golang编写,封装了对API服务请求,与长链接的处理逻辑。而各端的封装主要完成了客户端参数配置,与桥接Golang的部分。

用户认证机制

一、概括:

客户端交互的接口都在API服务中以http接口方式实现,而消息收发都在msggateway中以websocket协议实现。以下API定义在OpenIMAPI服务中,OpenIMAPI本身只起到接口转接的作用,具体的逻辑定义在下面的RPC服务,user&auth中。

二、接口

  • 用户接口
u := NewUserApi(user.NewUserClient(userConn), client, cfg.Discovery.RpcService)  
{      
  userRouterGroup := r.Group("/user")     
  userRouterGroup.POST("/user_register", u.UserRegister)      
  userRouterGroup.POST("/update_user_info", u.UpdateUserInfo)      
  userRouterGroup.POST("/update_user_info_ex", u.UpdateUserInfoEx)      
  userRouterGroup.POST("/set_global_msg_recv_opt", u.SetGlobalRecvMessageOpt)      
  userRouterGroup.POST("/get_users_info", u.GetUsersPublicInfo)      
  userRouterGroup.POST("/get_all_users_uid", u.GetAllUsersID)      
  userRouterGroup.POST("/account_check", u.AccountCheck)      
  userRouterGroup.POST("/get_users", u.GetUsers)      
  userRouterGroup.POST("/get_users_online_status", u.GetUsersOnlineStatus)
  userRouterGroup.POST("/get_users_online_token_detail", u.GetUsersOnlineTokenDetail)      
  userRouterGroup.POST("/subscribe_users_status", u.SubscriberStatus)    
  userRouterGroup.POST("/get_users_status", u.GetUserStatus)      
  userRouterGroup.POST("/get_subscribe_users_status", u.GetSubscribeUsersStatus)      
  userRouterGroup.POST("/process_user_command_add", u.ProcessUserCommandAdd)      
  userRouterGroup.POST("/process_user_command_delete", u.ProcessUserCommandDelete)      
  userRouterGroup.POST("/process_user_command_update", u.ProcessUserCommandUpdate)      
  auserRouterGroup.POST("/process_user_command_get", u.ProcessUserCommandGet)      
  userRouterGroup.POST("/process_user_command_get_all", u.ProcessUserCommandGetAll)      
  userRouterGroup.POST("/add_notification_account", u.AddNotificationAccount)      
  userRouterGroup.POST("/update_notification_account", u.UpdateNotificationAccountInfo)      
  userRouterGroup.POST("/search_notification_account", u.SearchNotificationAccount)  
}
  • 认证相关
a := NewAuthApi(pbAuth.NewAuthClient(authConn))  
authRouterGroup := r.Group("/auth") 
authRouterGroup.POST("/get_admin_token", a.GetAdminToken) 
authRouterGroup.POST("/get_user_token", a.GetUserToken) 
authRouterGroup.POST("/parse_token", a.ParseToken) 
authRouterGroup.POST("/force_logout", a.ForceLogout) 

三、认证逻辑

四、Token说明

  • TokenKey In Redis
// 前缀:用户ID:登录平台
UID_PID_TOKEN_STATUS:4834513665:Web
  • TokenValue说明:{Token:TokenFlag}

    TokenFlag取值如下,也就是说,TokenValue将以JsonMap的形式保存,最里可以看到最多4种情况

    • NormalToken  = 0
    • InValidToken = 1
    • KickedToken  = 2
    • ExpiredToken = 3

五、AuthDatabase Controller

database controller是openim对数据层的封装,底层可能是对mongodb或是redis的数据服务。

认证服务Auth是openim底层服务,在启动时,会实例化AuthDatabase,Auth服务实现的4四认证相关接口,便是依赖authDatabase实现认证数据的管理。

authDatabase实现的四个方法如下,即操作上面Token的方法:

- GetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error)
- CreateToken(ctx context.Context, userID string, platformID int) (string, error)  
- BatchSetTokenMapByUidPid(ctx context.Context, tokens []string) error  
- SetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error

消息类型说明MsgGateway提供了一个WebSocket服务,监听客户端的ws链接,并负责消息监听与处理。

一、OpenIM五种系统消息类型

  • TextMessage(Ping/Pone Message)
  • BinaryMessage(Chat Message)
  • CloseMessage(客户端关闭连接)
  • PingMessage
  • PongMessage

二、BinaryMessage(Req/Resp)

type Req struct {
  ReqIdentifier int32  `json:"reqIdentifier" validate:"required"`      
  Token         string `json:"token"`      
  SendID        string `json:"sendID"        validate:"required"`      
  OperationID   string `json:"operationID"   validate:"required"`      
  MsgIncr       string `json:"msgIncr"       validate:"required"`      
  Data          []byte `json:"data"`  
}
type Resp struct {      
  ReqIdentifier int32  `json:"reqIdentifier"`      
  MsgIncr       string `json:"msgIncr"`      
  OperationID   string `json:"operationID"`      
  ErrCode       int    `json:"errCode"`      
  ErrMsg        string `json:"errMsg"`      
  Data          []byte `json:"data"`  
}

三、ReqIdentifier

每一个消息类型,对应底层rpc服务msg都有对应的处理方法。一般用户的消息,则对应着WSSendMsg这个类型。

WSGetNewestSeq        = 1001 
WSPullMsgBySeqList    = 1002
WSSendMsg             = 1003  // 一般聊天消息
WSSendSignalMsg       = 1004  
WSPullMsg             = 1005  
WSGetConvMaxReadSeq   = 1006  
WsPullConvLastMessage = 1007  
WSPushMsg             = 2001  
WSKickOnlineMsg       = 2002  
WsLogoutMsg           = 2003  
WsSetBackgroundStatus = 2004  
WsSubUserOnlineStatus = 2005  
WSDataError           = 3001

花了两天时间,反复翻了几遍源码,结合以上核心的概念说明,OpenIM的基本结构已经相对清晰,后面有深入的分析再续。