33

IM服务器设计-基础

 4 years ago
source link: https://www.tuicool.com/articles/qEjiUfY
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

IM做为非常经典的服务器系统,其设计时候的考量具备代表性,所以这一次花几个篇幅讨论其相关设计。

整体架构

Bvqq2iR.png!web

以上架构图中,分为几个部分:

  • 客户端:支持IOS、Android系统。
  • 接入层:负责维护与客户端之间的长连接。
  • 逻辑层:负责IM系统中各逻辑功能的实现。
  • 存储层:存储IM系统相关的数据,主要包括Redis缓存系统(用于保存用户状态及路由数据)、消息数据。

上图中几部分的交互如下:

  • 客户端通过gate接入IM服务器。在这里,客户端与gate之间保持TCP长连接,客户端使用DNS查询域名返回最近的gate地址进行连接。
  • Gate的作用:保持与客户端之间的长连接,将请求数据转发给后面的逻辑服务LogicServer。LogicServer最上面是一个消息路由服务Router,根据请求的类型转发到后面具体的逻辑服务器。其中c代表客户端,s代表服务器,g代表群组,因此比如c2c服务就是处理客户端之间消息的服务器,而auth服务是处理客户端登录请求的服务器。
  • 逻辑类服务器与存储层服务打交道,其中:redis用于存储用户在线状态、用户路由数据(用户路由数据就是指用户在哪个gate服务上维护长连接),而Mysql用户存储用户的消息数据。
  • 以上的接入层、逻辑层由于本身不存储状态,因此都可以进行横向扩展。看似Gate维护着长连接,但是即使一个Gate宕机,客户端检测到之后可以重新发起请求接入另一台Gate服务器。

数据存储

  • 路由数据:存放在Redis中,格式为(UID,客户端在哪个gate登录)。
  • 消息数据:存储在DB中,部分也会缓存在缓存中方便查询,这部分做为下一部分文章的重点来讲解,不在这部分展开讨论。

核心交互流程

统一登录系统

登录授权(auth)

umaayqU.png!web

  1. 客户端通过统一登录系统验证登录密码等。
  2. SSO验证客户端用户名密码之后,生成登录token并返回给客户端。
  3. 客户端使用UID和返回的token向gate发起授权验证请求。
  4. gate同步调用logic server的验证接口。
  5. logic server请求SSO系统验证token合法性。
    • SSO向auth系统返回验证token结果。
    • 如果验证成功,auth系统在redis中存储客户端的路由信息,即客户端在哪个gate上登录。
  6. auth系统向gate返回验证登录结果。
  7. gate向客户端返回授权结果。

登出(logout)

zaM3ae3.png!web

  1. 客户端向gate发出logout请求。
  2. gate设置客户端UID对应的peer无效,然后应答客户端登出成功。
  3. gate向logic server发出登录请求。
  4. 处理该类请求的c2s服务器,清除redis中的客户端路由信息。

踢人(kickout)

用户请求授权时,可能在另一个设备(同类型设备,比如一台苹果手机登录时发现一台安卓手机也在登录这个账号)开着软件处于登录状态。这种情况需要系统将那个设备踢下线。

biyeqmi.png!web

新的客户端登陆流程同上面的登陆认证流程,只不过在auth模块完成认证之后,会做如下的操作:

  • 根据UID到redis中查询路由数据,如果不存在说明前面没有登陆过,那么就像登陆流程一样返回即可。
  • 否则说明前面已经有其他设备登陆了,将向前面的gate发送踢人请求,然后保存新的路由信息到redis中。
  • gate接收到踢人请求,踢掉客户端之后断掉与客户端的连接。

客户端上报消息(c2s消息)

qANFbq2.png!web

  1. 客户端向gate发送c2s消息数据。
  2. gate应答客户端。
  3. gate向逻辑服务器发送c2s消息。
  4. logic server的c2s模块,将消息发送到MQ消息总线中。
  5. appserver消费MQ消息做处理。

应用服务器推送消息(s2c消息)

VJbmUbF.png!web

  1. 业务服务器向逻辑服务器发送s2c消息。
  2. 逻辑服务器的s2c模块从redis中查询UID的路由数据,知道该用户在哪个gate上面登陆。
  3. 逻辑服务器向gate发送s2c消息。
  4. gate服务器向客户端发送s2c消息。
  5. 客户端收到之后向gate ack消息。
  6. gate向逻辑服务器ack s2c消息。

单对单聊天(c2c消息)

NnqmqqM.png!web

  1. 客户端向gate发送c2c消息。
  2. gate向逻辑服务器发送c2c消息。
  3. 逻辑服务器的c2c模块保存消息到消息存储中,此时会将该消息的未读标志置位表示未读。
  4. 逻辑服务器应答gate,说明已经保存了该消息,即客户端发送成功。
  5. gate应答客户端,表示c2c消息发送成功。
  6. 逻辑服务器的c2c模块,查询redis服务看该c2c消息的目标客户端的路由信息,如果不在线就直接返回。
  7. 否则说明该消息的目的客户端在线,向所在gate发送c2c消息。
  8. gate向客户端转发c2c消息。
  9. 客户端向gate应答收到c2c消息。
  10. gate向逻辑服务器应答客户端已经收到c2c消息。
  11. 逻辑服务器的c2c模块,在消息存储中清空该消息的未读标志表示消息已读。

注意第7步中,逻辑服务器的c2c模块在向gate转发c2c消息之后,需要加上定时器,如果在指定时间没有收到最后客户端的应答,需要重发。尝试几次重发都失败则放弃,等待下次用户登录了拉取离线消息。

群聊消息(c2g消息)

zANrqy7.png!web

  1. 客户端A向gate发送c2g消息。
  2. gate向逻辑服务器发送c2g消息。
  3. 逻辑服务器的c2g模块将消息保存到SendMsg DB中,这部分消息将根据消息的发送者ID水平扩展。
  4. c2g模块从cache中查询该群组的用户ID,如果查不到会到存放群组信息的DB中查询。
  5. 遍历获取到的群组ID,保存消息到RecvMsg DB中,这部分消息将根据接受者ID水平扩展。
  6. 查询redis,知道哪些群组用户当前在线。
  7. 向当前在线的用户所在gate发送c2g消息。
  8. gate转发给客户端c2g消息。
  9. 客户端应答gate c2g消息。
  10. gate应答逻辑服务器的c2g模块用户已经收到c2g消息。
  11. c2g模块修改发送消息库该消息已读。

协议设计

协议格式

remyErm.png!web

协议分为包头和包体两部分,其中包体为固定的大小,包括:

  • version(4字节):协议版本号。
  • cmd(4字节):协议类型。
  • seq(4字节):序列号。
  • timestamp(8字节):消息的时间戳
  • body length(4字节):包体大小。

其中,包体部分使用protobuf来定义,以下介绍不同命令的包体格式。

认证(auth)

message AuthRequest {
  string token = 1; // 从SSO服务器返回的登录token,登录之后保存在客户端
  srting uid = 2;   // 用户ID
}

message AuthResponse {
  int32 status = 1; // 应答状态码,0表示成功,其他表示失败
  string err_msg = 2; // 错误描述信息
}

登出(logout)

message LogoutRequest {
  string token = 1; // 从SSO服务器返回的登录token,登录之后保存在客户端
  srting uid = 2;   // 用户ID
}
message LogoutResponse {
}

踢人(kickout)

message KickoutRequest {
  enum Reason {
    OTHER_LOGIN = 1; // 其他设备登录
  }
  int32 reason = 1; // 踢人原因
}
message KickoutResponse {
}

心跳包

无包体

单对单消息(c2c)

// 发送者发送消息的协议
message C2CSendRequest {
  string from = 1; // 发送者
  string to = 2; // 接收者
  string content = 3; // 消息内容
}

message C2CSendResponse {
  int64 msgid = 1; // 落地的消息ID
}

// 推送给接收者的协议
message C2CPushRequest {
  string from = 1;
  string content = 2;
  int64 msgid = 3;
}

message C2CPushResponse {
  int64 msgid = 1;  // 消息id,服务器收到这个id可以去置位这个消息已读
}

群聊(c2g)

// 发送者发送群消息协议
message C2GSendRequest {
  string from = 1; // 发送者
  string group = 2; // 群
  string content = 3; // 消息内容
}
message C2GSendResponse {
  int64 msgid = 1; // 落地的消息ID
}

// 推送给其他群成员消息协议
message C2GPushRequest {
  string from = 1; // 发送者
  string group = 2; // 群
  string content = 3; // 消息内容
  int64 msgid = 4; // 落地的消息ID
}

message C2GPushResponse {
  int64 msgid = 1; // 落地的消息ID
}

拉离线消息(pull)

message C2SPullMessageRequest {
  string uid = 1;
  int64 msgid = 2;  // 拉取该消息id以后的离线消息,为0由服务器自行判断
  int32 limit = 3; //  单次拉取离线消息的数量
}

message PullMsg {
  string from = 1;  // 发送者
  int64 group = 2;  // 目的群
  string content = 3; // 消息内容
  int64 msgid = 4;  // 消息编号
  int64 send_time = 5;  // 服务器接收消息时间
}

message C2SPullMessageResponse {
  repeated PullMsg msg = 1; // 离线消息数组
}

参考资料


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK