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:在线
ChatUnit、ChatChannel、ChatSceneHelper.Distribution;不直接对公网。 - Map:登录时记下
ChatUnitRouteId;事件里ChatHelper.SendChatMessage,不在业务服再实现一套频道广播。
上下行怎么记
- 上行:
C2Chat_SendMessageRequest→ Gate →Other2Chat_ChatMessage→ Chat。 - 下行:Chat 组装
Chat2G_ChatMessage或Chat2C_ChatMessage→ Gate → 客户端上的Chat2C_Message。
跨服触发与玩家发言在 Chat 侧都会落到同一套 Distribution,差别只在于包从 Gate 进来还是从 Map 经内网进来。
| 关注点 | 要点 |
|---|---|
| 高并发 | 世界/地图类走频道批量推;SendTime、缓冲池等控刷屏与分配压力 |
| 状态一致 | 登录阶段把 Chat、Map 等路由写进 Gate;断线由 Gate 统一驱动各服回收 |
| 扩展 | 多 Chat 节点时换路由目标;新节点类型主要扩工厂与客户端渲染 |
Gate 侧:Unit、Flag、Route
GateUnitManageComponent:Id与UserName双索引;GateUnit.Routes为SceneType → 各服 Unit RunTimeId。GateUnitFlagComponent:绑在Session上,断开即可用GateUnitId走统一下线。RouteComponent:AddAddress登记,Send/Call按目标服类型转发;聊天回包要么进GateScene扇出,要么进指定GateSession。
登录顺序(与实现顺序等价):建 GateUnit → G2Chat_LoginRequest 换 ChatRouteId → G2M_LoginRequest 携带 ChatUnitRouteId → G2C_LoginResponse 带回各侧路由。平时聊天请求只带树,由已登记映射送到 Chat。
消息树:频道、目标与节点
| 对象 | 角色 | 复习时抓的重点 |
|---|---|---|
ChatInfoTree |
整包载荷 | ChatChannelType / ChatChannelId / Target / Nodes 次序即展示次序 |
ChatInfoNode |
单段内容 | ChatNodeType 管形态,ChatNodeEvent 管点击后行为,Data 带结构化参数 |
- 链式
AddText、AddLink等是拼装体验;双端枚举、ChatNodeFactory类扩展要保持一致。 - 文中频道枚举带
Flags语义,可同时勾选多类展示用途;解析端对Nodes分支渲染即可。
Chat 服:单元、频道与分发
ChatUnit:存GateRouteId、已加入Channel字典,SendTime可做按频道冷却。ChatChannelCenterComponent:Apply/Disband;ChatChannel维护成员,Send内向各成员GateRouteId发内网包。- 上线重复
UnitId时更新GateRouteId;Dispose退干净频道。 - 世界广播常打 Gate 服场景
RouteId,由ForEachUnitSession扇出;私聊等用Chat2C带目标GateSessionRunTimeId。
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_ChatMessage 和 Chat2C_ChatMessage 分别在什么时候用?Gate 怎么处理?
题目
世界频道和私聊回程为什么用两种消息形态?
深入解析
Chat2G_ChatMessage:目标常是 Gate 场景或配置的 Gate 服RouteId,由GateUnitManageComponent遍历所有在线GateSession扇出,适合世界、跑马灯等全员可见。Chat2C_ChatMessage:带一批目标GateSessionRunTimeId,框架只投递这些会话,适合私聊、小队等窄广播。- 客户端最终都收到
Chat2C_Message载荷;成本差在 Gate 是全量扇出还是按列表投递,避免私聊也扫全场。
答题示例
全员收用
Chat2G,打到 Gate 场景再遍历所有 GateSession。只给部分人用Chat2C,带上目标会话 ID,避免无差别广播浪费带宽和 CPU。
参考文章
- 2.聊天系统Gate服设计
- 4.聊天系统Chat服设计
2. Map 服为什么必须记住 ChatUnitRouteId?Other2Chat_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