架构总览
这页用一张图把 Cherry 的整体结构讲清楚。建议在读完关于 Cherry之后先看这页,后面的具体页面都是在展开这里的某一块。
四条执行链
框架的代码可以按四条链路来理解,每条链路职责独立:
| 链路 | 关键路径 | 职责 |
|---|---|---|
| 应用装配 | cherry.go → application.go | 组件注册、生命周期、启动 |
| Actor 执行 | net/actor/ | 每个 Actor 一个 goroutine、串行邮箱、本地/远程/事件分发 |
| 集群通信 | net/cluster/ + net/nats/ + net/discovery/ | 跨节点 RPC、NATS 传输、成员发现 |
| 前端接入 | net/parser/ + net/connector/ | 协议编解码、session、Agent Actor、WebSocket/TCP |
┌──────────────────────────────┐
│ 你的业务代码 │
│ Actor / Component / 配表 │
└──────────────┬───────────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
┌───────▼────────┐ ┌─────────▼─────────┐ ┌─────────▼────────┐
│ 应用装配 │ │ Actor 执行 │ │ 集群通信 │
│ cherry.go │ │ net/actor/ │ │ net/cluster/ │
│ application.go │◀──────────▶│ 每个 Actor 一个 │◀─────────▶│ net/nats/ │
│ │ │ goroutine + 串行 │ │ net/discovery/ │
│ 组件注册/生命周期 │ │ 邮箱(4 条通路) │ │ 跨节点 RPC/发现 │
└───────┬────────┘ └─────────▲─────────┘ └──────────────────┘
│ │
│ ┌─────────┴─────────┐
└────────────────────▶│ 前端接入 │
│ net/parser/ │
│ net/connector/ │
│ 编解码/Agent 连接 │
└─────────▲─────────┘
│
tcp / websocket
│
客户端启动顺序
理解启动顺序能解释很多「为什么这里拿不到那个组件」的问题。
1. cherry.Configure(...) 创建 Application,读 profile
2. AppBuilder.Startup()
├─ 集群模式:自动注册 cluster_component
├─ 集群模式:自动注册 discovery_component
├─ 自动注册 actor_component
├─ 注册你通过 app.Register() 添加的组件
└─ Application.Startup()
├─ 若设置了 netParser,把它的 connector 注册为组件
├─ 所有组件按注册顺序执行 Set → Init → OnAfterInit
└─ 阻塞等待退出信号
3. 收到 SIGINT/SIGTERM 或调用 Shutdown()
└─ 组件逆序执行 OnBeforeStop → OnStop三个关键推论
- 自定义组件总能拿到
Cluster()/Discovery()/ActorSystem()—— 它们在自定义组件之前注册 OnAfterInit()里才能安全访问其它组件 ——Init()阶段其它组件可能还没初始化Registration只在启动前有效 ——Startup()之后调用Register()不会生效
前端节点必须设置解析器
isFrontend = true 但没有调用 SetNetParser(...) 会导致启动时 panic。这是框架刻意的检查——前端节点不接收客户端连接是没有意义的。
一条消息的完整路径
以客户端发一条业务消息为例:
① 客户端
│ ws/tcp 字节流
▼
② connector(net/connector/)
│ 接受连接,交给 parser
▼
③ parser(net/parser/pomelo/)
│ ① 解 package 头(type + length)
│ ② 解 message 头(flag + mid + route)
│ ③ 交给 onDataRoute
▼
④ 路由决策
├─ 目标 nodeType == 本节点 → PublishLocal(进程内,不序列化)
└─ 否则 → 经 NATS 发到目标节点
▼
⑤ Actor 邮箱
│ 投递到 Local 队列
▼
⑥ Actor goroutine
│ ① 反序列化参数(此时才 Unmarshal)
│ ② 按 funcName 找到注册的处理函数
│ ③ 调用
▼
⑦ 处理函数
│ Response / Push / Call 其它 Actor
▼
⑧ 回程:序列化 → parser 编码 → 客户端几个容易误解的点:
- 第 ③ 步不做反序列化。body 一路以原始
[]byte传到第 ⑥ 步的调用处才Unmarshal。所以「反序列化失败」的报错出现在 Actor 调用处,不是网络层。 - 第 ④ 步的「本地」指进程内。同一节点内 Actor 之间的
Call走的是 remote 队列,不是 local。 - 第 ⑥ 步是串行的。同一个 Actor 的所有处理函数依次执行,不会并发——这就是 Actor 内部字段不用加锁的原因。
协议字节格式详见 Pomelo 协议规范。
Actor 执行模型
┌──────────────── 一个 Actor ────────────────┐
│ │
┌─────▼─────┐ ┌──────────┐ ┌─────────┐ ┌────────┐
│ local │ │ remote │ │ event │ │ timer │ 四条通路
│ queue │ │ queue │ │ queue │ │ ch │ 各自 FIFO
└─────┬─────┘ └────┬─────┘ └────┬────┘ └───┬────┘
│ │ │ │
└─────────────┴──────┬──────┴───────────┘
│
┌────────▼────────┐
│ 单个 goroutine │ ← 串行处理,无需加锁
│ select 循环 │
└────────┬────────┘
│
┌────────▼────────┐
│ Actor 内部状态 │ userMap / 计数器 / 缓存…
└─────────────────┘| 通路 | 触发方式 |
|---|---|
| Local | 客户端消息经路由投递,PostLocal |
| Remote | 其它 Actor 的 Call / CallWait(同节点也是这条) |
| Event | PostEvent 订阅/发布 |
| Timer | Actor 的 p.Timer().Add(...) |
不要在 Actor 里做阻塞操作
一个 Actor 只有一个 goroutine。在回调里做同步 HTTP、等锁、长时间计算,会卡住整个 Actor 的后续消息。需要阻塞操作时用 Call 交给别的 Actor,或用 cutils.Try 包一层。
Child Actor 与父 Actor 共享同一个 goroutine,所以一个卡住的子 Actor 会阻塞父 Actor。
Actor 的详细 API 见 Actor 模型。
节点与集群
一个 Cherry 进程 = 一个节点,节点有类型(nodeType)和 id(nodeID)。
┌─────────────────┐
│ NATS 服务器 │ ← 所有节点间的通信总线
└────────┬────────┘
┌──────────────┬────┴────┬──────────────┐
│ │ │ │
┌─────▼─────┐ ┌─────▼────┐ ┌─▼──────┐ ┌────▼─────┐
│ master │ │ gate │ │ game │ │ web │
│ 发现服务 │ │ 网关 │ │ 逻辑服 │ │ HTTP │
└───────────┘ └──────────┘ └────────┘ └──────────┘
↑ 注册/心跳 ↑ 客户端连接 ↑ 玩家 Actor ↑ 浏览器| 概念 | 说明 |
|---|---|
| 节点类型 | 由 profile 中 node 段的键决定,如 gate / game / web |
| 节点 id | 每个节点的唯一标识,节点靠它在 profile 里查出自己的配置 |
| 发现服务 | 维护「集群里有哪些节点」,支持 default / nats / etcd 三种后端 |
| 集群通信 | 固定走 NATS,框架封装好 subject 与请求应答 |
| Actor 路径 | 节点id.ActorID 或 节点id.ActorID.子id,跨节点寻址靠它 |
节点间通信与发现服务的细节见 通信 & 集群。
全局状态与限制
框架有若干包级全局变量,因此:
一个进程只能有一个 Application
这些是包级单例,多个 Application 会互相冲突:
| 包 | 全局状态 |
|---|---|
profile/ | profilePath、jsonConfig、env、debug、printLevel |
logger/ | DefaultLogger |
net/nats/ | connectPool、roundIndex |
net/discovery/ | discoveryMap(组件注册表) |
需要多个实例请拆成多个进程。
关键约定
| 约定 | 说明 |
|---|---|
| Actor 路径 | {nodeID}.{actorID} 或 {nodeID}.{actorID}.{childID},. 开头表示本节点 |
| 组件名唯一 | Name() 在一个 Application 内必须唯一 |
| 注册时机 | Register() 只在 Startup() 之前有效 |
| Actor 系统组件 | 框架自动注册,不要手动注册 actor_component |
| 集群组件 | 集群模式下框架自动注册 cluster_component + discovery_component |
Startup() 会阻塞 | 它不是「初始化后返回」,而是阻塞到进程退出 |
| 子 Actor 限制 | 只能由父 Actor 创建,子 Actor 不能再创建孙 Actor |
| 消息对象池 | Message 来自对象池,投递失败时调用方负责 Recycle() |
下一步
- 组件管理 —— 应用装配链路的展开
- Actor 模型 —— Actor 执行链路的展开
- 通信 & 集群 —— 集群通信链路的展开
- 网络数据包 —— 前端接入链路的展开
- 编写一个 Cherry 服务器 —— 动手跑起来