5.总结
5.1 知识点
系列按「总览 → Gate → 数据模型 → Mail 服实现」排布:先定分层与消息走向,再写网关会话与路由,接着把 Mail / MailBox 与枚举钉死,最后用组件与流程把登录领取、列表、开信领附件、发送和两类过期清理串起来。
5.2 核心要点速览
业务边界与部署形态
邮件系统承接的是玩法与运营侧「道具 + 文案」在多人、多进程之间的可靠投递:全服公告式投放、按维度圈人、点名、离线暂存、带附件结算,同时满足审计和扩展。工程上默认 Mail 不外网、客户端只连 Gate,把暴面、会话和路由收口在一层。
常见部署:
| 模式 | 做法 | 注意 |
|---|---|---|
| 无分区 | 多 Mail 实例;playerId % mailServerCount 或按负载选机 |
同一大世界吞吐高,要避开单节点过热 |
| 分区服 | 每区一枚 Mail | 运维简单;跨区投递需另设计 |
与配图一致的几条硬事实:客户端只连 Gate;Gate 再连 Map、其它业务服、邮件服;邮件服与其它服一样走「公网进网关、业务只在内网」;必要时邮件服也可向 Map 等节点发内网消息做联动,玩家可见协议仍经 Gate。
Gate:会话、鉴权、透明路由
Gate 只做三件事:会话生命周期、账号与会话绑定、把邮件 RPC 指到正确的 MailUnit。和「聊天网关」同类,邮件业务不在 Gate 里展开。
组件与职责
| 对象 | 作用 | 和邮件链的关系 |
|---|---|---|
GateScene |
启服、接入会话、OnSessionAccept / OnSessionDisconnect |
每个外网连接一条会话线 |
GateAccountFlagComponent |
Session 上挂 Account(含 MailRouteId),销毁时走下线 |
断线必须通知 Mail 落库、卸 MailUnit |
RouteComponent |
RouteType.MailRoute → MailUnitRouteId;Send / Call 走内网 |
客户端以为在打 Mail,实际是 Gate 转发 |
GateLoginHelper.Online |
G2Mail_LoginRequest,回包带 MailUnitRouteId 后 AddAddress |
之后 C2Mail_* 才有确定的 Mail 侧落点 |
GateLoginHelper.Offline |
G2Mail_ExitRequest |
与 Mail 侧 Exit 成对 |
登录与邮件 RPC 走向
登录链:
C2G_LoginRequest
→ Gate 挂 GateAccountFlagComponent、RouteComponent
→ GateLoginHelper.Online → G2Mail_LoginRequest
← Mail2G_LoginResponse(含 MailUnitRouteId)
→ RouteComponent.AddAddress(MailRoute, MailUnitRouteId)
→ G2C_LoginResponse
邮件请求:C2Mail_* → RouteComponent.Call(MailRoute, 请求) → 命中对应 MailUnit → 响应沿原会话返回客户端。
数据模型:内容与投递分离
Mail 实体
玩家邮箱里的「一封」,管展示与状态。
| 字段 | 用途 |
|---|---|
OwnerId |
归属玩家,对齐账号或单元 |
Title / Content |
标题与正文 |
CreateTime / ExpireTime |
单封生命周期,驱动清理 |
Money / Items |
附件;Item 可先只做 Name 再扩展 |
MailState |
未读 / 已读 / 未领 / 已领,驱动红点与按钮 |
MailType |
系统 / 奖励 / 玩家互发 |
MailBox 实体
「一批怎么投、投给谁」的载体,内嵌一份 Mail,适合全服一类信共用一份正文。
| 字段 | 用途 |
|---|---|
Mail |
嵌套的 Mail,内容与附件 |
CreateTime / ExpireTime |
袋子级过期,可与内层 Mail 解耦 |
MailBoxType |
投递策略,见下表 |
AccountId |
目标玩家集合 |
Received |
已领取集合,防重领;全员领完可整袋回收 |
SendAccountId |
发件人,审计用 |
枚举
| 枚举 | 记住这几档 |
|---|---|
MailBoxType |
Specify 点名;Online 仅在线;All 全服;AllToDate 按注册时间与邮件创建时间筛人 |
MailRemoveActionType |
手删、超时删、超量淘汰 |
数据量上来后可在 ExpireTime、AccountId 等字段做索引;再大再谈分片或热点缓存。
Mail 服:在线单元、离线箱、统一发送口
组件分工
| 组件 | 一句话 | 关键点 |
|---|---|---|
MailUnitManageComponent |
MailScene 上管所有 MailUnit |
Init 载入;Online 建单元、挂 MailComponent、触发离线投递 |
MailUnit |
运行期玩家单元 | 含 GateRouteId,推送经 Gate 回到客户端 |
MailBoxManageComponent |
离线邮件「物流中心」 | 加载未过期 MailBox;GetHaveMail 把符合条件的信灌进玩家 MailComponent;定时 Timeout 清过期箱 |
MailComponent |
在线个人邮箱 | Add / Remove / OpenMail / Receive / CheckTimeOut;超量可删最老 |
MailHelper |
外部唯一发送入口 | Send 判断本机或跨服;InnerSend 按 MailBoxType 分支 |
两套过期,别混
| 谁负责 | 清什么 |
|---|---|
MailBoxManageComponent.Timeout |
尚未进个人邮箱的 MailBox,整袋过期 |
MailComponent.CheckTimeOut |
已在玩家邮箱里的单封 Mail,列表请求前扫一遍可避免客户端看到过期残留 |
登录灌信与拉列表
Online里会GetHaveMail(mailUnit, sync: true),合流全服、限时、指定等待,多次MailComponent.Add并推Mail2C_HaveMail。- 之后
C2Mail_GetHaveMailRequest:handler 里先CheckTimeOut(),再从当前MailComponent填MailSimplifyInfo,**不必再整表扫MailBox**。
开信、领附件、删信:RouteRPC 进 MailUnit,对应 OpenMail、Receive(内部调用货币与道具服务)、Remove(..., Remove, sync: true),状态用 Mail2C_MailState 等同步。
发送:C2Mail_SendMailRequestHandler 构建 MailBox 后 MailHelper.Send;Specify 在线立刻推、离线可落箱;Online 只打在线;All / AllToDate 进箱管理、登录批量下发。活动服等统一调 Send 即可。
5.3 面试题精选
基础题
1. 玩家登录成功后,Gate 如何把后续的 C2Mail_* 请求送到正确的 MailUnit?
题目
请按时间顺序说明:从 C2G_LoginRequest 到客户端能发 C2Mail_*,Gate 侧必须完成哪些步骤?MailUnitRouteId 起什么作用?
深入解析
Gate 自己不认识「邮件业务」,只认会话和路由表。登录时 GateLoginHelper.Online 把账号信息发到 MailScene,Mail 创建或定位 MailUnit 后,用 Mail2G_LoginResponse 带回 **MailUnitRouteId**。Gate 必须把它登记进 RouteComponent,即 AddAddress(RouteType.MailRoute, MailUnitRouteId),这样后续 RouteComponent.Call(MailRoute, rpc) 才能在内网打到那一颗 MailUnit。若只做鉴权却不登记路由,客户端会话有了、C2Mail_* 却会无处投递。下线时对称地 Offline / G2Mail_ExitRequest,避免 Mail 侧仍向已断会话推送。
答题示例
登录时 Gate 调
Online,Mail 回MailUnitRouteId。Gate 把它写进RouteComponent的 Mail 路由映射里。之后客户端发
C2Mail_*,Gate 用Call(MailRoute, 请求)转到对应的MailUnit,响应再走同一条会话返回。没这一步,MailUnitRouteId只在登录回包里出现过一次、没进路由表,邮件 RPC 就找不到人。
参考文章
- 1.邮件系统架构设计总览
- 2.邮件系统Gate服设计
进阶题
1. 为何把 Mail 与 MailBox 拆开?MailBoxType 如何改变投递路径?
题目
从存储与逻辑两方面说明:Mail 与 MailBox 各解决什么问题?结合 Specify / Online / All / AllToDate,配合 InnerSend / MailBoxManageComponent / MailComponent 简述典型走向。
深入解析
- 拆分动机:
Mail是「一封信」的状态与附件,挂在玩家侧列表里;MailBox是「一次投放任务」和多目标领取进度,内嵌一份Mail做 payload。全服同类邮件共用一箱,靠AccountId/Received维护谁该领、谁已领,避免把同一内容复制成 N 份独立Mail再分别过期。 - 类型与路径:
MailHelper.InnerSend按MailBoxType分支。Specify对名单:在线则MailComponent.Add即时推送,离线进箱等人上线。Online只遍历当前在线MailUnit。All/AllToDate通常持久化在MailBoxManageComponent,以GetHaveMail在登录时批量灌入个人邮箱;AllToDate额外用注册时间与邮件创建时间过滤「晚于发信注册」的玩家。
答题示例
MailBox管一批人怎么领、谁领过,里面嵌一个Received防重领。
Specify可在线直达邮箱、离线先进箱;Online只打在线;All和AllToDate多进离线箱,AllToDate还要按注册时间筛谁能领。具体分支在InnerSend里走箱管理或个人MailComponent。
参考文章
- 3.邮件系统邮件数据结构设计
- 4.邮件系统Mail服设计
深度题
1. MailBoxManageComponent.Timeout 与 MailComponent.CheckTimeOut 如何分工?怎样避免「登录灌信」和「拉邮件列表」重复扫同一批离线数据?
题目
两类定时/触发清理分别删除哪一层数据?Online → GetHaveMail 与 C2Mail_GetHaveMailRequestHandler 各自该做多少事?
深入解析
- 清理对象:
MailBoxManageComponent管理未并入个人邮箱的MailBox;Timeout到期整袋Remove,清缓存与库。MailComponent管已进入某MailUnit的Mail集合;CheckTimeOut删单封过期信并同步状态给客户端。一个是「仓库里的箱子」,一个是「玩家手里的信」。 - 职责边界:
Online中的GetHaveMail(sync: true)负责把离线侧应得的信批量Add进MailComponent,并推Mail2C_HaveMail,属于数据迁移 + 通知。之后客户端若只拉简略列表,GetHaveMail请求 handler 应 **CheckTimeOut+ 从内存中的MailComponent序列化MailSimplifyInfo**,而不是再次遍历全局MailBox。否则每次打开邮箱都要 O(离线池) 扫表,延迟与锁竞争都会放大。
答题示例
箱级过期归
MailBoxManageComponent,清的是还没灌进玩家邮箱的MailBox。灌进去之后,单封过期归MailComponent.CheckTimeOut。登录时
GetHaveMail把该领的离线信一次性加进MailComponent。以后拉列表只读内存里的邮箱字典,先过期扫描再返回简略信息,不要重复全库扫MailBox。
参考文章
- 1.邮件系统架构设计总览
- 4.邮件系统Mail服设计
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com