4.聊天系统Chat服设计

4.聊天系统Chat服设计


4.1 知识点

设计思路

Chat服务器作为聊天中枢,专注消息分发频道管理序列化,解耦网络路由与业务逻辑。

  • 单元管理:通过 ChatUnitManageComponent 缓存并维护在线聊天单元(ChatUnit)。
  • 频道中心:由 ChatChannelCenterComponent 负责申请、解散、查找频道,并协调成员加入、退出。
  • 消息树解析:接收并反序列化 ChatInfoTree,按节点类型分发:广播(世界、地图、广播等)、频道(队伍、公会等)或私聊。
  • 双端协议:与客户端共享 ChatInfoTreeChatInfoNodeChatNodeFactory 和序列化组件,保证协议一致性。
  • 回程路由:分发结果包装为 Chat2G_ChatMessage(全服广播)或 Chat2C_Message(局部/私聊),通过 Gate 自动转发至客户端。

此设计使 Chat 专注业务逻辑,依赖 Gate 提供的网络转发,支持动态热更与横向扩展。

Chat服主要组件

classDiagram
  class ChatScene {
    +Start()
    +OnInnerRoute()
  }

  class ChatUnitManageComponent {
    -Dictionary Units
    +Add(unitId, userName, gateRouteId): ChatUnit
    +Remove(unitId)
    +TryGet(unitId, out ChatUnit): bool
  }

  class ChatUnit {
    +long Id
    +string UserName
    +long GateRouteId
    +Dictionary Channels
    +Dictionary SendTime
    +Dispose()
  }

  class ChatChannelCenterComponent {
    -Dictionary Channels
    +Apply(channelId): ChatChannel
    +Disband(channelId)
    +TryGet(channelId, out ChatChannel): bool
  }

  class ChatChannel {
    +long Id
    -HashSet Units
    +Send(tree)
    +JoinChannel(chatUnitId): bool
    +ExitChannel(chatUnitId)
  }

  class SerializerComponent {
    +ISerialize Serialize
    +MemoryStreamBufferPool BufferPool
    +Initialize()
  }

  ChatScene --> ChatUnitManageComponent : 挂载
  ChatScene --> ChatChannelCenterComponent : 挂载
  ChatScene --> SerializerComponent : 挂载
  ChatScene --> NetworkMessagingComponent : 引用
  ChatChannelCenterComponent --> ChatChannel : 管理
  ChatUnitManageComponent --> ChatUnit : 管理
  ChatChannel --> ChatUnit : 通知/广播

ChatScene

  • 方法

    • Start():初始化组件并注册内网路由处理器(Other2Chat_ChatMessageHandlerG2Chat_LoginRequestHandler 等)。
    • OnInnerRoute():接收来自 Gate 或 Map 的聊天相关路由消息。

SerializerComponent

  • 字段/方法

    • Serialize:选用 ProtoBuf 等序列化器。
    • BufferPool:内存流缓冲池,复用 MemoryStream 提升性能。
    • Initialize():在 Scene 启动时注册序列化器。

ChatUnitManageComponent

  • 方法

    • Add(unitId, userName, gateRouteId):创建或更新 ChatUnit,并返回用于后续路由。
    • Remove(unitId):移除 ChatUnit,释放资源。
    • TryGet(unitId, out ChatUnit):查询是否在线。

ChatUnit

  • 字段

    • Id, UserName, GateRouteId:对应 GateSession 的运行时 ID。
    • Channels:此用户所加入的频道引用。
    • SendTime:各频道上次发言时间,防止刷屏。
  • 方法

    • Dispose():退出所有频道并清理自身引用。

ChatChannelCenterComponent

  • 方法

    • Apply(channelId):获取或创建频道。
    • Disband(channelId):销毁频道,释放资源。
    • TryGet(channelId, out ChatChannel):查询指定频道。

ChatChannel

  • 字段/方法

    • Units:频道成员 ID 列表。
    • Send(tree):广播消息给所有成员,内部调用 NetworkMessagingComponent.SendInnerRoute
    • JoinChannel(chatUnitId):成员加入。
    • ExitChannel(chatUnitId):成员退出,空频道时自动 Dispose()

Chat 服上下线及频道管理流程梳理

ChatUnit 管理中心:上线/下线流程

ChatUnit 管理中心流程
“ChatUnit 管理中心”维护了所有在线聊天单元的生命周期:

  1. 上线

    • 检查 ChatUnitManageComponent.Units 中是否已有该 UnitId
    • 存在:更新已有 ChatUnitGateRouteId(支持断线重连或重复登录),并刷新 UserName 等信息;
    • 不存在:调用 Add(unitId, userName, gateRouteId) 创建新的 ChatUnit 实体,加入到 Units
  2. 下线

    • 检查缓存中是否有该 ChatUnit
    • 存在:调用 Remove(unitId),先执行 ChatUnit.Dispose()(会退出所有频道),再从 Units 中移除;
    • 不存在:无操作。

此流程确保 ChatUnit 的“在线”与“离线”状态始终与 Gate 层及客户端连接状态保持一致,并自动清理无用实体。

多网关消息回传:ChatUnit → Gate → 客户端

多网关转发流程
当 ChatSession 分发完成后,可能需要通过多个 Gate 节点将消息发送给所有客户端:

  1. Chat 调用 NetworkMessagingComponent.SendInnerRoute(gateRouteId, message),将消息发给 Gate1 与 Gate2 等。
  2. 各个 GateSession 收到时,依旧通过其各自的 RouteComponent 或直接 session.Send() 转发给对应的 ClientSession。
  3. 如果是广播消息或者世界消息,可能调用的是 networkMessagingComponent.SendInnerRoute(gateSceneConfig.RouteId, Chat2GMessage),拿的是配置里Gate服的RouteId,这样GateScene再遍历自己GateUnitManageComponent中的所有GateSession发送消息给各个客户端。
  4. 这种多网关并行转发的机制,保证了跨机房、多网段环境下的高可用与低延迟,且不丢失任一在线客户端。

聊天频道中心:创建/加入/解散

聊天频道中心操作流程
ChatChannelCenterComponent 提供了频道的“三大基石操作”:

  1. 申请/创建频道

    • 接收 channelId,若不存在则:

      • Entity.Create<ChatChannel>(scene, channelId, ...) 新建频道实体;
      • 加入 Channels 字典并返回。
  2. 加入频道

    • 通过 Apply(channelId).JoinChannel(chatUnitId)

      • ChatChannel.Units 中添加 chatUnitId
      • 在对应的 ChatUnit.Channels 字典中保存频道引用。
  3. 解散频道

    • 调用 Disband(channelId)

      • Channels 字典移除该 ChatChannel
      • 自动触发 Dispose(),清理所有成员引用。

这些方法确保频道的生命周期(申请→使用→解散)流畅、无残留。

聊天频道消息与成员增减

聊天频道消息发送与成员管理流程
ChatChannel 实现了广播与动态成员管理:

  1. 发送消息

    • Send(tree) 遍历 Units 中的每个 chatUnitId

      • 取出对应 ChatUnit,获取 GateRouteId
      • 调用 NetworkMessagingComponent.SendInnerRoute(gateRouteId, chat2cMessage)
  2. 加入频道

    • JoinChannel(chatUnitId):将 chatUnitId 加入 Units,并在 ChatUnit.Channels 中记录该频道。
  3. 退出频道

    • ExitChannel(chatUnitId)

      • Units 中移除 chatUnitId
      • ChatUnit.Channels 中移除频道引用;
      • Units.Count == 0,则调用 Dispose() 自动解散频道。

通过清晰的增删流程与自动销毁机制,频道始终保持正确的成员列表与生命周期管理。

Chat服核心流程图

sequenceDiagram
  autonumber
  participant GateSession
  participant ChatSession

  GateSession->>ChatSession: 1. G2Chat_LoginRequest { UserName, UnitId, GateRouteId }
  ChatSession->>SerializerComponent: 2. Initialize if needed
  ChatSession->>ChatUnitManageComponent: 3. Add(UnitId, UserName, GateRouteId)
  ChatSession-->>GateSession: 4. Chat2G_LoginResponse { ChatRouteId }

  GateSession->>ChatSession: 5. Other2Chat_ChatMessage { ChatInfoTree }
  ChatSession->>SerializerComponent: 6. Deserialize ChatInfoTree
  ChatSession->>ChatUnitManageComponent: 7. Get chatUnit by UnitId
  ChatSession->>ChatSceneHelper: 8. Distribution(chatUnit, tree)
  alt Broadcast 全服
    ChatSceneHelper ->>ChatChannelCenterComponent: Apply(WorldChannelId)
    ChatChannelCenterComponent ->>ChatChannel: Send(tree)
  else 私聊 / 频道
    ChatSceneHelper ->>ChatChannel: Join/Channel/Private(tree)
  end
  ChatChannel-->>ChatSession: 9. Chat2G_ChatMessage or Chat2C_ChatMessage
  ChatSession-->>GateSession: 10. 回传消息 { ChatInfoTree, TargetList? }

Chat服核心流程图说明

  1. G2Chat 登陆请求
    GateSession 收到客户端登录后的内部路由,从 Gate 转发过来的 G2Chat_LoginRequest,包含三个关键信息:

    • UserName:玩家的用户名,用于在 Chat服创建或查找对应的 ChatUnit。
    • UnitId:GateUnit 在 Gate 服内存中的唯一运行时 ID,用作 ChatUnit 的标识。
    • GateRouteId:GateSession 的 RunTimeId,ChatUnit 用它回传消息时,Gate 能准确地转回到原始客户端。
  2. SerializerComponent 初始化
    ChatSession 的上下文中,首先检查并初始化 SerializerComponent

    • 如果尚未初始化,调用 Initialize() 绑定 ProtoBuf 等序列化器。
    • 确保后续对 ChatInfoTree 等复杂对象的序列化和反序列化都能借助缓冲池和高效的序列化组件完成。
  3. ChatUnit 注册/更新
    调用 ChatUnitManageComponent.Add(UnitId, UserName, GateRouteId)

    • 如果 Units 字典中已存在该 UnitId,则更新其 GateRouteId(应对断线重连场景);
    • 否则,创建新 ChatUnit,赋值 IdUserNameGateRouteId,并加入到 Units
    • 返回该 ChatUnit,后续分发逻辑中会使用其 ChannelsSendTime 等状态。
  4. ChatRouteId 回传 Gate
    ChatSession 构造 Chat2G_LoginResponse { ChatRouteId = chatUnit.RunTimeId },并回传给 GateSession:

    • GateSession 收到后将该 ChatRouteId 保存到自身的 RouteComponent.addressMap 中,键为 SceneType.Chat
    • GateRouteId 与 ChatRouteId 的映射确保后续所有从 Gate 发往 Chat 的请求都正确路由到对应的 ChatSession。
  5. Other2Chat 聊天消息接收
    GateSession 接收到客户端发来的 C2Chat_SendMessageRequest 后,通过 RouteComponent.Send(SceneType.Chat, message) 转发为 Other2Chat_ChatMessage { ChatInfoTree } 给 ChatSession:

    • 这里 Gate 不干预消息内容,只做透明转发;
    • ChatSession 的路由处理器 Other2Chat_ChatMessageHandler 被触发。
  6. 反序列化 ChatInfoTree
    Other2Chat_ChatMessageHandler 中,调用 SerializerComponent.Deserialize<ChatInfoTree>(bytes)

    • 使用缓冲池内的 MemoryStream 读取二进制数据;
    • 重建 ChatInfoTree 对象,包括其 Nodes 列表,每个 ChatInfoNodeContentDataNodeTypeEventType 都被还原。
  7. 获取 ChatUnit 实例
    根据请求携带的 chatUnitId(即 GateRouteId 对应的 ChatUnit.RunTimeId),通过 ChatUnitManageComponent.TryGet(chatUnitId, out chatUnit)

    • 验证该用户是否仍在线;
    • 获取用户所在的所有频道信息和上次发送时间等状态,便于后续分发与频率限制。
  8. 消息分发决策
    调用 ChatSceneHelper.Distribution(chatUnit, tree) 进行核心逻辑分发:

    • 检查发送频率与内容合法性:使用 chatUnit.SendTime 确保各频道间隔不低于配置;计算文本长度和道具数量,防止刷屏或滥发;

    • 根据 ChatChannelType 决定分发方式

      • Broadcast:调用 Broadcast(scene, tree)
      • Channel(队伍/公会/地图):执行 Channel(chatUnit, tree)
      • Private:走 Private(chatUnit, tree) 私聊分支;
    • 返回分发结果码,决定后续组装哪种回传消息。

  9. 生成并回传消息
    分发函数根据分发类型生成两种可能的消息对象:

    • Chat2G_ChatMessage:当需要向所有客户端广播时(世界聊天、跑马灯等),消息发送目标是 GateScene(SceneType.Gate), 调用 networkMessagingComponent.SendInnerRoute(gateSceneConfig.RouteId, Chat2GMessage),拿的是配置里Gate服的RouteId;
    • Chat2C_ChatMessage:当是私聊或局部频道时,Chat服直接指定目标 ChatUnit 的 GateRouteId 列表,调用 NetworkMessagingComponent.SendInnerRoute(gateRouteId, chat2cMessage),gateRouteId就是登录时知道的GateSessionRunTimeId,这样在框架层被包装为发送到对应 GateSession。
  10. Gate 转发至客户端
    GateSession 收到 Chat2G_ChatMessageChat2C_ChatMessage 后:

    • 对于 Chat2G_ChatMessage,收到消息的是GateScene,执行 GateUnitManageComponent.ForEachUnitSession(),遍历所有在线 GateSession 并调用 session.Send(chat2cMessage),发送消息给所有客户端;
    • 对于 Chat2C_ChatMessage,收到消息的是GateSession,无需特殊处理,框架层自动回把这个消息给客户端;
    • 客户端最终收到 Chat2C_Message { ChatInfoTree },立刻解析并在 UI 渲染。

这样,Chat服在整条消息流中只聚焦“反序列化 → 分发决策 → 重序列化回传”,所有网络转发、分发目标的维护均由 Gate 的 RouteComponent 与各 FlagUnitManageComponent 协作完成,使得 Chat 逻辑干净、职责单一、易于测试与扩展。

总结

  1. 解耦路由:Chat 仅处理业务分发,依赖 Gate 提供统一路由。
  2. 组件化管理:单元、频道、序列化各司其职,易于维护与扩展。
  3. 灵活分发:同一套 ChatInfoTree 能覆盖世界、频道、私聊等所有聊天场景。
  4. 性能优化:内存缓冲池、发送冷却(SendTime)与及时销毁策略,确保高并发下稳定性。
  5. 双端一致:客户端与服务器共享协议定义、序列化组件与节点工厂,保证格式一致、无缝升级。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏