Skip to content

架构总览 ​

这页用一张图把 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

三个关键推论

  1. 自定义组件总能拿到 Cluster() / Discovery() / ActorSystem() —— 它们在自定义组件之前注册
  2. OnAfterInit() 里才能安全访问其它组件 —— Init() 阶段其它组件可能还没初始化
  3. 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(同节点也是这条)
EventPostEvent 订阅/发布
TimerActor 的 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()

下一步 ​