1. 4.1 网关的职责与架构
第二、三章分别固定了帧解析与 Actor 语义;本节说明 zhenyi/zgate.Server 如何把 IChannel 上行收敛为 zmsg.Message 路由:先到 Gate 自身 handler,再 本地 Actor,再 跨进程,最后 无路由处理。实现以 gate.go 为准。
1.1. 4.1.1 网关是什么?
网关(Gate)是客户端和业务服务之间的桥梁:
客户端 → [网关 Gate] → 业务 Actor(IM / Match / Chat / ...)
如果没有网关,每个业务 Actor 都要直接管理连接——接收数据、协议解析、心跳检测、限流……这些和业务逻辑无关的事情重复实现。
网关把这些和业务无关的职责统一接手,让业务 Actor 只专注于业务逻辑。
1.2. 4.1.2 网关的职责
从代码看,zhenyi 的 Gate 做了这些事:
| 职责 | 说明 |
|---|---|
| 连接管理 | 接受连接、断开清理、在线人数统计 |
| 协议接入 | TCP / WebSocket / KCP,统一处理 |
| 心跳检测 | 超时断开连接 |
| 限流 | 单连接级令牌桶,防刷 |
| 消息路由 | 根据消息类型转发到对应 Actor |
| 回复转发 | 把业务 Actor 的响应发回客户端 |
| 会话管理 | channelId → authId 映射 |
| 性能监控 | RTT、QPS、在线人数、内存指标 |
业务 Actor 不需要关心以上任何一件事。
1.3. 4.1.3 Gate 本身也是一个 Actor
zhenyi 的 Gate 不是一个独立的服务,它本身就是 Actor,和其他业务 Actor 一起在 Group 中运行:
// 主干节选;另含 remoteStrategy、HTTP、TLS、payloadEncrypt、noRouteHandler 等
type Server struct {
*zactor.Actor
*SessionManager
server baseziface.IServer // ztcp/zws/zkcp 等实现的 IServer
router ziface.LocalRouter
metrics *ServerMetrics
// ...
}
(完整字段见 zhenyi/zgate/gate.go。)
这意味着 Gate 享受 Actor 模型的所有好处:
- 消息串行处理:不需要锁
- 自动重启:异常退出后 Group 自动拉起
- 热更新:运行时调整参数
- 统一监控:和其他 Actor 一起被 Group 监控
1.4. 4.1.4 消息流转的完整路径
一条客户端消息从发达到处理完成,经过了这些步骤:
1. 客户端发送消息
↓
2. 网络层接收(RingBuffer + 协议解析)
↓
3. Gate.OnRead() 收到消息
├── 限流检查
├── 心跳刷新
└── Push 到 Mailbox
↓
4. Gate Actor 从 Mailbox 取出消息(Run 主循环)
↓
5. Gate.HandleClientMessage()
├── 检查 Gate 自己能否处理(已注册的 handler)
├── 尝试本地路由(转发到同进程的其他 Actor)
├── 尝试远程路由(通过 NATS 转发到其他进程)
└── 无路由 → 回复错误
↓
6. 业务 Actor 处理消息,生成响应
↓
7. 响应回到 Gate(`Actor.Push`,`zhenyi/zactor/base.go`)
├── `IsResponse && !ToClient` → `SetReply`,不进邮箱
├── `IsResponse && ToClient` → Gate 实现 `IToClientFastPath` 时直达 `sendClient`,否则入邮箱
└── 其它 Cmd 类型 → 入邮箱
↓
8. `sendClient`:`GetChannel(SessionId)` → `Channel.Send`
1.4.1. 路由优先级
Gate 的路由有明确的优先级顺序:
1. Gate 自身处理 → 已注册的 handler(如登录、心跳等 Gate 层面的协议)
2. 本地 Actor → 同进程内的其他业务 Actor(零网络开销)
3. 远程 Actor → 其他进程的 Actor(通过 NATS)
4. 无路由 → `sendNoRouteError`(默认打日志;可注册 `OnNoRoute` 钩子决定回包)
Gate 自身优先:认证、心跳等可在入口闭环,避免无谓的跨 Actor 投递与序列化。
1.5. 4.1.5 Gate 和业务 Actor 的关系
从架构上看,Gate 是一个入口 Actor:
┌─────────────┐
客户端 ───→ Gate ──→│ IM Actor │
├─────────────┤
│ Match Actor │
├─────────────┤
│ Chat Actor │
└─────────────┘
特点:
- Gate 是唯一入口:所有客户端消息都先到 Gate,再由 Gate 路由
- 业务 Actor 不直接持有连接:下行时由 Gate 根据
zmsg.Message中的元数据(如SessionId)定位连接,业务不直接操作IChannel - Gate 仍持有会话相关状态:例如嵌入的
SessionManager(authId → actorType → actorId)以及底层BaseServer上的authId → Channel映射;路由决策主要依赖消息与路由表,但不要把 Gate 理解成“零状态进程”
1.5.1. 为什么不让业务 Actor 直接接收连接?
如果每个业务 Actor 都直接接收连接:
| 问题 | 说明 |
|---|---|
| 重复实现 | 每个业务 Actor 都要写限流、心跳、协议解析 |
| 连接分散 | 客户端要连不同端口访问不同服务 |
| 跨服务调用复杂 | 业务 Actor 之间要互相转发连接 |
统一走 Gate,这些问题都不存在。
1.6. 4.1.6 会话管理:channelId 与 authId
Gate 维护两个 ID 的关系:
| ID | 含义 | 谁分配 |
|---|---|---|
| channelId | 连接 ID,底层网络层分配 | 网络层自动递增 |
| authId | 业务用户 ID(如 userId) | 业务逻辑在登录时设置 |
连接刚建立时只有 channelId,没有 authId。用户登录成功后,业务 Actor 调用:
gate.SetSessionAuth(channelId, userId)
建立绑定关系。之后路由消息时,可以通过 authId 找到对应的连接。
绑定时机由业务决定。 zhenyi 没有规定"必须在登录时绑定",这取决于业务逻辑。比如有的系统允许匿名连接,只有特定操作才需要认证。
1.7. 4.1.7 HTTP 支持
除了长连接(TCP/WS/KCP),Gate 还可以同时启动 HTTP 服务:
gate.SetHTTPAddr(":8080")
zhenyi 的 HTTP 模块(zhttp)基于标准库 net/http,提供了一套路由 + 中间件能力,handler 直接接收 Gate Actor 引用。
1.7.1. 路由注册
func init() {
gate := ... // Gate 实例
http := gate.HTTP()
// 健康检查
http.GET("/ping", func(actor ziface.IActor, w http.ResponseWriter, r *http.Request) error {
w.Write([]byte("ok"))
return nil
})
// 分组路由(统一前缀 + 中间件)
api := http.Group("/api/v1")
api.Use(authMiddleware) // 给这个组加认证中间件
api.GET("/users/:id", getUserHandler)
api.POST("/config/update", updateConfigHandler)
}
handler 签名是 func(actor IActor, w, r) error,第一个参数直接是 Gate Actor。这意味着 HTTP handler 可以直接调用 Actor 的能力——发消息、查路由、访问 Actor 状态。
1.7.2. 中间件
func authMiddleware(next ziface.HttpHandlerFunc) ziface.HttpHandlerFunc {
return func(actor ziface.IActor, w http.ResponseWriter, r *http.Request) error {
token := r.Header.Get("Authorization")
if !verifyToken(token) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return nil
}
return next(actor, w, r)
}
}
中间件链式调用,和常见的 Web 框架用法一致。
1.7.3. 和长连接共享 Gate Actor
HTTP 和长连接在同一个 Gate Actor 进程里运行。HTTP handler 若要给长连接客户端下行,应使用 zactor.Actor 上已实现的发送 API(IActor 接口本身不包含这些方法,需对具体类型断言,例如 *zgate.Server 嵌入了 *zactor.Actor)。
推荐:回包路径——若业务上存在“对应的客户端请求信封”(同一会话、同一条链路的上下文),用 SendToClient 复制信封里的 SessionId(连接 id / channelId)、SrcActor、SeqId 等:
func notifyClient(actor ziface.IActor, w http.ResponseWriter, r *http.Request) error {
gate := actor.(*zgate.Server)
var clientReq *zmsg.Message // 业务侧保存的最近一次该用户请求信封(示意)
var payload ziface.IMessage // 已实现 GetMsgId + MarshalToVT 的 protobuf 消息
gate.SendToClient(clientReq, payload)
return nil
}
主动推送(无现成信封)时,zgate.Server.sendClient 使用 msg.SessionId 调用 GetChannel(SessionId),因此 zmsg.Message.SessionId 必须是目标连接的 channelId,不能简单用 userId/authId 代替。需由业务维护「用户 → channelId」或在 Gate 侧通过 BaseServer.GetChannelByAuthId 等能力解析后再投递。
func pushByChannelId(actor ziface.IActor, channelId uint64, data ziface.IMessage) error {
gate := actor.(*zgate.Server)
a := gate.Actor
m := zmsg.GetMessage()
defer m.Release()
if err := ziface.MarshalVTToMsg(data, m); err != nil {
return err
}
m.MsgId = data.GetMsgId()
m.ToClient = true
m.IsResponse = true
m.SessionId = channelId
m.TarActor = gate.GetActorId()
m.SrcActor = a.GetActorId()
a.SendMsg(m.Retain())
return nil
}
(历史上若见到 SendToClientByChannel 等名称,应以当前源码中的 SendToClient / SendMsg 为准。)
HTTP 常用于运维/控制面触发向长连接下行(须自行维护 channelId / SessionId 与鉴权)。
1.7.4. 安全防护
ReadHeaderTimeout: 5s:防止 slowloris 攻击(慢速连接耗尽服务器资源)- 错误信息不暴露内部细节:handler 返回 error 时,客户端只看到 500 状态码
1.8. 4.1.8 本节要点
- 入口:
Server嵌入zactor.Actor+SessionManager,持IServer与LocalRouter。 - 路由序:
gateHandler— 自身GetMsgList→RouteLocal→routeToRemoteActor→sendNoRouteError。 - 下行:
sendClient按SessionId(channelId) 找连接;ToClient 响应可经Push快路径(见 4.4)。 - 会话:连接维度映射在
BaseServer/Channel;业务authId → 分片 Actor在SessionManager(4.2)。 - HTTP:
SetHTTPAddr非空时随RunServer启zhttp,与 Gate 共享同一 Actor。
4.2 节:两层映射与线程安全约定。