8.聊天系列总结

8.总结


8.1 知识点

系列从总览架构出发:客户端只连 Gate;Chat 专管在线单元、频道与分发;Map 等服在业务里把系统类或玩法触发的聊天推到 Chat。载荷统一用消息树表达频道、定向目标与富文本节点。后半部分单独讲离线私聊的拉取策略、ACK 与合并 ACK,以及群聊里存储冗余、lastMessageId 进度、ACK 风暴与批量 ACK。


8.2 核心要点速览

分层与消息主路径

各进程分工一句话:客户端拼树发包;Gate 会话与路由;Chat 反序列化后分发并组回程;Map 等服通过已登记的 Chat 路由 ID 把消息树推到 Chat。

  • 客户端:UI、构造 ChatInfoTree、KCP/TCP 只连 Gate;序列化组件与各服对齐。
  • Gate:鉴权、心跳、断线;GateUnit + RouteComponent 维护到 Chat/Map 等的 RunTimeId。
  • Chat:在线 ChatUnitChatChannelChatSceneHelper.Distribution;不直接对公网。
  • Map:登录时记下 ChatUnitRouteId;事件里 ChatHelper.SendChatMessage,不在业务服再实现一套频道广播。

上下行怎么记

  1. 上行:C2Chat_SendMessageRequest → Gate → Other2Chat_ChatMessage → Chat。
  2. 下行:Chat 组装 Chat2G_ChatMessageChat2C_ChatMessage → Gate → 客户端上的 Chat2C_Message

跨服触发与玩家发言在 Chat 侧都会落到同一套 Distribution,差别只在于包从 Gate 进来还是从 Map 经内网进来。

关注点 要点
高并发 世界/地图类走频道批量推;SendTime、缓冲池等控刷屏与分配压力
状态一致 登录阶段把 Chat、Map 等路由写进 Gate;断线由 Gate 统一驱动各服回收
扩展 多 Chat 节点时换路由目标;新节点类型主要扩工厂与客户端渲染

Gate 侧:Unit、Flag、Route

  • GateUnitManageComponentIdUserName 双索引;GateUnit.RoutesSceneType → 各服 Unit RunTimeId
  • GateUnitFlagComponent:绑在 Session 上,断开即可用 GateUnitId 走统一下线。
  • RouteComponentAddAddress 登记,Send/Call 按目标服类型转发;聊天回包要么进 GateScene 扇出,要么进指定 GateSession

登录顺序(与实现顺序等价):建 GateUnitG2Chat_LoginRequestChatRouteIdG2M_LoginRequest 携带 ChatUnitRouteIdG2C_LoginResponse 带回各侧路由。平时聊天请求只带树,由已登记映射送到 Chat。

消息树:频道、目标与节点

对象 角色 复习时抓的重点
ChatInfoTree 整包载荷 ChatChannelType / ChatChannelId / Target / Nodes 次序即展示次序
ChatInfoNode 单段内容 ChatNodeType 管形态,ChatNodeEvent 管点击后行为,Data 带结构化参数
  • 链式 AddTextAddLink 等是拼装体验;双端枚举、ChatNodeFactory 类扩展要保持一致。
  • 文中频道枚举带 Flags 语义,可同时勾选多类展示用途;解析端对 Nodes 分支渲染即可。

Chat 服:单元、频道与分发

  • ChatUnit:存 GateRouteId、已加入 Channel 字典,SendTime 可做按频道冷却。
  • ChatChannelCenterComponentApply / DisbandChatChannel 维护成员,Send 内向各成员 GateRouteId 发内网包。
  • 上线重复 UnitId 时更新 GateRouteIdDispose 退干净频道。
  • 世界广播常打 Gate 服场景 RouteId,由 ForEachUnitSession 扇出;私聊等用 Chat2C 带目标 GateSession RunTimeId。

Distribution 里先做频控与内容校验,再按类型走 Broadcast / Channel / Private。回程先想清楚:要全员扇出还是只打名单里的会话。

Map 与跨服

业务服标准写法:建树,再调 SendChatMessage。与正文一致的调用骨架如下(语义摘自系列示例,收发细节仍以工程实现为准):

// 地图等非 Chat 场景:用登录阶段存下的 chatUnitRouteId 把树送到对应 ChatUnit
ChatHelper.SendChatMessage(scene, chatUnitRouteId, tree);
// 内部等价于对 chatUnitRouteId 发 Other2Chat_ChatMessage,由 Chat 侧 Handler 调 Distribution

chatUnitRouteId 来自 Gate 写进 gateUnit.Routes 再带进 G2M_LoginRequest。Chat 进程内禁止再调 ChatHelper,避免递归扇出。

离线私聊:交互与可靠性

私聊离线可以概括成「按接收方落库 + 上线拉取」;系列里演进顺序是:减少拉取次数 → 控制单包体积 → 避免「发了就删」丢消息 → 用 ACK 换可靠 → 合并 ACK 省往返

阶段 做法 代价或风险
朴素 按好友多次拉 好友多则 RPC 次数爆炸
按接收者一次拉 receiveId 单查,客户端再分组 数据量极大时单包暴涨
分页 固定 page size 若返回即删,Crash 可能「删了但未送达」
应用层 ACK 确认后再删库 每页多一次 RTT
合并 ACK 拉第 N 页顺带 ACK 第 N−1 页 ID 多数流程只多一次收尾确认

服务端若以 DB 记录为「是否清离线」的依据,删除应发生在对端确认收到之后;合并 ACK 是在不坏可靠性的前提下削往返。

离线群聊:冗余、进度与风暴

  • 朴素:每条 GroupMessage 给每个离线成员插 GroupOfflineIndex,群里人一多索引行数按人头放大。
  • 收紧实体:内容只存一份;可先退到「索引只存 messageId」,再进一步用 GroupMember.lastMessageId / lastMessageTime 表示读进度,拉取 messageId 大于进度即可,旧索引表可废弃。
  • 在线推与离线拉都要 ACK,否则重启、丢包、客户端崩溃都会出现洞;ACK 可能丢导致重复下发,客户端按 messageId 去重。
  • 大群每人每消息一次 ACK 会形成风暴,用按条数或按时间窗批量 ACK 把波峰摊平。

8.3 面试题精选

基础题

1. 为什么聊天链路里客户端只连 Gate,不直连 Chat?

题目

客户端能否直接连 Chat 发聊天?本系列里为何统一走 Gate?

深入解析
  • 公网面:Chat 负责扇出与频道,再对玩家开端口要重复一套安全、限流与审计;Gate 已是统一入口。
  • 会话回指:客户端只连 Gate,ChatUnit 上记 GateRouteId,回程能回到原会话;直连 Chat 还要额外维护玩家当前挂在哪个 Gate。
  • 扩容:Gate 与 Chat 可各自横向扩,客户端配置只填网关。
答题示例

不直连。Gate 做公网入口和路由,Chat 在内网做分发。客户端一条连接进 Gate,Chat 回包用 GateRouteId 就能回到对应 GateSession,安全与运维边界也清晰。

参考文章
  • 1.聊天系统架构设计总览
  • 2.聊天系统Gate服设计

2. 玩家从客户端发聊天,和 Map 里调 ChatHelper 发聊天,Chat 里是不是同一套分发?

题目

要不要为跨服聊天单独写一套广播、私聊逻辑?

深入解析
  • 客户端链路:包先进 Gate,再转成 Other2Chat_ChatMessage 进 Chat。
  • Map 等服:SendChatMessage 封同样的协议,用 SendInnerRoute(chatUnitRouteId, …) 直达目标 ChatUnit,跳过 Gate 不等于换业务框架。
  • Chat 内:Handler 归并为落到 ChatUnit,再 Distribution入口不同,分发与频道规则只维护一份,避免双份改频道类型时改漏。
答题示例

是同一套。两边最后都是带树的 Other2Chat 进 Chat,路由到具体 ChatUnit 后走 Distribution。差别在谁打包、从 Gate 还是从业务服内网进来,Chat 里不搞两套广播私聊实现。

参考文章
  • 4.聊天系统Chat服设计
  • 5.聊天系统跨服调用

进阶题

1. Chat2G_ChatMessageChat2C_ChatMessage 分别在什么时候用?Gate 怎么处理?

题目

世界频道和私聊回程为什么用两种消息形态?

深入解析
  • Chat2G_ChatMessage:目标常是 Gate 场景或配置的 Gate 服 RouteId,由 GateUnitManageComponent 遍历所有在线 GateSession 扇出,适合世界、跑马灯等全员可见。
  • Chat2C_ChatMessage:带一批目标 GateSession RunTimeId,框架只投递这些会话,适合私聊、小队等窄广播。
  • 客户端最终都收到 Chat2C_Message 载荷;成本差在 Gate 是全量扇出还是按列表投递,避免私聊也扫全场。
答题示例

全员收用 Chat2G,打到 Gate 场景再遍历所有 GateSession。只给部分人用 Chat2C,带上目标会话 ID,避免无差别广播浪费带宽和 CPU。

参考文章
  • 2.聊天系统Gate服设计
  • 4.聊天系统Chat服设计

2. Map 服为什么必须记住 ChatUnitRouteIdOther2Chat_ChatMessage 如何落到正确 ChatUnit

题目

跨服发聊天时 ChatHelper 为什么要传 chatUnitRouteId

深入解析
  • ChatUnitRouteId 是 Chat 侧该玩家 ChatUnit 的 RunTimeId;Map 只认地图实例不认这个 ID,就分不清「这条要以哪个聊天身份」进 Distribution
  • 登录时 G2M_LoginRequest 写入该 ID;事件里 SendInnerRoute(chatUnitRouteId, Other2Chat_ChatMessage),Chat 按 RunTimeId 绑定到 ChatUnit
  • 业务上 Handler 返回非 0 要有日志或回执,避免频控、频道不存在被静默吞掉。
答题示例

Map 必须存 ChatUnit 的 RunTimeId,否则内网不知道交给哪个聊天单元。Gate 在登录链上把这个 ID 带给 Map,SendInnerRoute 用同一个 ID 把包送进正确的 ChatUnit 再分发。

参考文章
  • 1.聊天系统架构设计总览
  • 5.聊天系统跨服调用

深度题

1. 离线私聊里「分页 + 先删后发」有什么问题?ACK 与合并 ACK 各自换来什么?

题目

为什么要应用层 ACK?合并 ACK 省的是什么?

深入解析
  • 先删后发:客户端尚未确认,库记录已删,进程 Crash 或中途丢包会变成永久丢失;可靠路径是「对端确认后再删」。
  • 纯分页 + ACK:每页多拉一次确认,RTT 近似翻倍;合并策略把「拉第 N 页」与「ACK 第 N−1 页 id 列表」合成一次,通常只剩最后一轮纯 ACK。
  • 工程上要约定尾页是否单独 ACK、超时重试幂等等,避免边界卡死。
答题示例

先删后发在故障下容易丢消息。ACK 保证送达再删,但交互变多。合并 ACK 用「拉下一页顺带确认上一页」把往返压掉一半量级感知,一般只多一次收尾确认。

参考文章
  • 6.聊天系统离线私聊消息设计

2. 群离线从「每人每条索引」演进到 lastMessageId,解决什么、带来什么新成本?

题目

为什么要砍 GroupOfflineIndex 膨胀?ACK 风暴和重复消息怎么办?

深入解析
  • 冗余:同一条群消息对 N 个离线成员写 N 行索引,存储与写入放大;实体单份 + lastMessageId 后,按 messageId > lastMessageId 追赶即可,可废弃逐条索引表。
  • 可靠性:在线推与离线拉都要 ACK 后再挪进度,否则和私聊一样会出现「以为到了其实没进客户端」的中间态洞。
  • 新成本:大群 × 高频下每人每消息 ACK 会爆炸,用批量 ACK(按条数或时间窗);ACK 丢失导致重复,客户端 messageId 去重兜底。
答题示例

每人每条索引在大群里会把 DB 写爆。消息只存一份,用成员上的 lastMessageId 表示读到哪里,上线往后拉。ACK 保进度但会带来 QPS 风暴,所以批量 ACK;ACK 丢会重复,客户端本地按 messageId 去重。

参考文章
  • 7.聊天系统离线群聊消息设计


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

×

喜欢就点赞,疼爱就打赏