Add realtime gateway and simulator bridge

This commit is contained in:
2026-03-27 21:06:17 +08:00
parent 0703fd47a2
commit 2c0fd4c549
36 changed files with 6852 additions and 1 deletions

View File

@@ -0,0 +1,287 @@
# Realtime Gateway + Cloudflare Tunnel 本机联调说明
本文档说明如何在**不正式部署到线上服务器**的前提下,把本机的 `realtime-gateway` 暴露给外部设备或远程联调方。
适用场景:
- 真机调试
- 外网联调
- 家长端 / 场控端远程验证
- 演示环境
不适用场景:
- 正式生产实时链路
- 严格 SLA 场景
- 高稳定长时间压测
---
## 1. 推荐结论
当前阶段建议:
1. 网关继续运行在本机
2. 本机使用 Cloudflare Tunnel 暴露新网关
3. 旧模拟器继续本地运行
4. 旧模拟器通过桥接把数据发给本机新网关
5. 外部客户端只访问 Cloudflare Tunnel 暴露出来的 `wss` 地址
这样做的好处:
- 不需要先买独立服务器
- 不需要先做正式公网部署
- 不改变当前本机开发结构
- 新旧链路可以并行使用
---
## 2. 本机网关配置
推荐先使用:
[tunnel-dev.json](D:/dev/cmr-mini/realtime-gateway/config/tunnel-dev.json)
这个配置相比开发默认配置更适合 Tunnel 联调:
- 端口改为 `18080`
- 关闭匿名 consumer
- token 不再使用默认开发值
建议先把其中的 token 改成你自己的值。
启动方式:
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go build -o .\bin\gateway.exe .\cmd\gateway
.\bin\gateway.exe -config .\config\tunnel-dev.json
```
本机地址:
- HTTP: `http://127.0.0.1:18080`
- WebSocket: `ws://127.0.0.1:18080/ws`
---
## 3. Quick Tunnel 方案
Quick Tunnel 最适合当前阶段。
Cloudflare 官方文档说明:
- Quick Tunnel 用于测试和开发,不建议生产使用
- 可以直接把本地 `http://localhost:8080` 之类的地址暴露出去
- 命令形式是 `cloudflared tunnel --url http://localhost:8080`
来源:
- [Quick Tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)
- [Set up Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/setup/)
### 3.1 启动 Quick Tunnel
你的网关如果跑在 `18080`
```powershell
cloudflared tunnel --url http://localhost:18080
```
启动后,`cloudflared` 会输出一个随机 `trycloudflare.com` 域名。
例如:
```text
https://random-name.trycloudflare.com
```
对应的 WebSocket 地址就是:
```text
wss://random-name.trycloudflare.com/ws
```
注意:
- 对外使用时WebSocket 必须写成 `wss://`
- 本地 origin 仍然是 `http://localhost:18080`
### 3.2 Quick Tunnel 限制
Cloudflare 官方说明里Quick Tunnel 当前有这些限制:
- 仅适合测试和开发
- 有并发请求上限
- 不支持 SSE
因此它适合:
- 临时分享联调地址
- 验证 WebSocket 接入
- 短期演示
不适合拿来当正式生产入口。
另外,官方文档提到:
- 如果本机 `.cloudflared` 目录里已有 `config.yaml`Quick Tunnel 可能不能直接使用
来源:
- [Quick Tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)
---
## 4. Named Tunnel 方案
如果你后面不想每次拿随机域名,可以改用 Named Tunnel。
这时推荐的本地 `cloudflared` 配置示例在:
[config.example.yml](D:/dev/cmr-mini/realtime-gateway/deploy/cloudflared/config.example.yml)
示例内容:
```yaml
tunnel: YOUR_TUNNEL_ID
credentials-file: C:\Users\YOUR_USER\.cloudflared\YOUR_TUNNEL_ID.json
ingress:
- hostname: gateway-dev.example.com
service: http://localhost:18080
- service: http_status:404
```
关键点:
- Tunnel 把 `gateway-dev.example.com` 映射到本机 `http://localhost:18080`
- 最后一条 `http_status:404` 是 catch-all
Cloudflare 官方文档对 published application 和 ingress 的说明见:
- [Set up Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/setup/)
- [Protocols for published applications](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/protocols/)
启动后,对外 WebSocket 地址就是:
```text
wss://gateway-dev.example.com/ws
```
---
## 4.1 外部 consumer 小工具
如果你要在另一台机器上用仓库里的 `mock-consumer` 做联调,推荐复制:
[consumer-tunnel.example.json](D:/dev/cmr-mini/realtime-gateway/config/consumer-tunnel.example.json)
[consumer-gps-heart.example.json](D:/dev/cmr-mini/realtime-gateway/config/consumer-gps-heart.example.json)
填好实际的公网地址和 token例如
```json
{
"url": "wss://gateway-dev.example.com/ws",
"token": "your-consumer-token",
"deviceId": "child-001",
"topics": [
"telemetry.location",
"telemetry.heart_rate"
],
"snapshot": true
}
```
然后运行:
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-consumer -config .\config\consumer-tunnel.example.json
```
命令行参数会覆盖配置文件中的同名项,所以临时换 `deviceId` 时可以直接追加:
```powershell
go run .\cmd\mock-consumer -config .\config\consumer-tunnel.example.json -device-id child-002
```
如果只想临时直接从命令行看 GPS 和心率:
```powershell
go run .\cmd\mock-consumer -url wss://gateway-dev.example.com/ws -token your-consumer-token -device-id child-001 -topics telemetry.location,telemetry.heart_rate
```
---
## 5. 旧模拟器如何配合
旧模拟器仍然本地运行:
```powershell
cd D:\dev\cmr-mini
npm run mock-gps-sim
```
然后在旧模拟器页面的“新网关桥接”区域里填:
- 网关地址:`ws://127.0.0.1:18080/ws`
- Producer Token与你 `tunnel-dev.json` 里配置一致
- 目标 Device ID按你的联调对象填写
- Source ID例如 `mock-gps-sim-a`
这里要注意:
- 旧模拟器桥接到的是**本机网关地址**
- 外部消费者连接的是**Cloudflare Tunnel 暴露的公网地址**
不要把旧模拟器桥接目标直接写成公网 `wss` 地址。
旧模拟器和网关都在本机,直接走本地回环最稳。
---
## 6. 推荐联调拓扑
```text
旧模拟器页面
-> 本机 mock-gps-sim
-> 本机 realtime-gateway (ws://127.0.0.1:18080/ws)
-> Cloudflare Tunnel
-> 外部 consumer / 真机 / 调试端 (wss://<public-host>/ws)
```
这样职责最清晰:
- 旧模拟器只负责发模拟数据
- 新网关负责实时中转
- Cloudflare Tunnel 只负责把本机网关暴露到外部
---
## 7. 当前阶段的安全建议
即使只是联调,也建议至少做到:
- `allowAnonymousConsumers = false`
- Producer / Consumer / Controller token 全部替换
- 不把默认 token 长期暴露在公网链接中
- Tunnel 只暴露新网关,不一定要暴露旧模拟器 UI
如果只是你自己本机调试:
- 旧模拟器 UI 保持本地访问即可
- 只把 `realtime-gateway` 暴露出去
---
## 8. 结论
当前阶段最推荐的方案是:
1. 本机用 [tunnel-dev.json](D:/dev/cmr-mini/realtime-gateway/config/tunnel-dev.json) 启动新网关
2. 本机旧模拟器桥接到 `ws://127.0.0.1:18080/ws`
3.`cloudflared tunnel --url http://localhost:18080` 先跑 Quick Tunnel
4. 外部客户端使用 `wss://<quick-tunnel-domain>/ws`
5. 等你需要固定域名或更稳定的入口时,再切换 Named Tunnel
这条路径最轻、最稳,也最符合你现在“先不正式上线”的目标。

View File

@@ -0,0 +1,123 @@
# 实时网关 MVP 拆分
本文档用于把 `realtime-gateway` 第一阶段工作拆成可执行任务。
---
## 1. 目标
第一阶段只解决以下问题:
- 建立一个独立于现有模拟器的新 Go 网关工程
- 能接收 Producer 上报
- 能让 Consumer 按设备订阅
- 能转发位置和基础 telemetry
- 能保存 latest state
- 能提供最基础的健康检查和运行入口
---
## 2. 任务拆分
### 2.1 工程骨架
- 新建 `realtime-gateway/`
- 初始化 `go.mod`
- 建立 `cmd/``internal/` 结构
- 提供最小运行 README
- 提供开发配置文件
### 2.2 网络入口
- HTTP server
- `/healthz`
- `/metrics`
- `/ws`
- 优雅关闭
### 2.3 会话管理
- 生成 `sessionId`
- 维护连接生命周期
- 记录角色和订阅
- 断线清理
### 2.4 协议处理
- `authenticate`
- `subscribe`
- `publish`
- `snapshot`
- 错误返回格式统一
### 2.5 路由与 latest state
-`deviceId` 路由
- 支持 `groupId``topic` 过滤
- latest state 内存缓存
- `snapshot` 获取最新值
### 2.6 轻鉴权
- Producer token
- Controller token
- Consumer token 或匿名策略
### 2.7 基础观测
- JSON 日志
- 连接日志
- 发布日志
- 基础 metrics 占位接口
---
## 3. 暂不进入 MVP
- 规则引擎正式实现
- 通知分发正式实现
- Recorder 正式实现
- Replayer 正式实现
- Redis
- 集群
- 数据库存档
- 高级限流
- 复杂 ACL
---
## 4. 建议开发顺序
1. 骨架与配置
2. HTTP 和 WebSocket 入口
3. 会话管理
4. 发布与订阅
5. latest state
6. 鉴权
7. 观察与日志
8. 插件总线占位
---
## 5. 完成标准
满足以下条件可视为 MVP 跑通:
- 可以启动一个独立 Go 服务
- 模拟 Producer 能发布 `telemetry.location`
- Consumer 能订阅某个 `deviceId`
- Consumer 能收到实时转发
- Consumer 可读取某个设备的 latest state
- Producer、Controller、Consumer 的角色边界基本可用
---
## 6. 下一阶段
MVP 跑通后优先做:
1. 插件总线正式化
2. Recorder 插件
3. 模拟器对接新协议
4. 简单规则插件
5. Dispatcher 插件

View File

@@ -0,0 +1,344 @@
# 实时网关协议草案
本文档描述 `realtime-gateway` 第一版协议草案,范围只覆盖 MVP。
---
## 1. 连接方式
- 协议WebSocket
- 地址:`/ws`
- 编码JSON
- 通信模式:客户端主动发消息,服务端返回状态或事件
---
## 2. 消息类型
客户端上行 `type`
- `authenticate`
- `join_channel`
- `subscribe`
- `publish`
- `snapshot`
服务端下行 `type`
- `welcome`
- `authenticated`
- `subscribed`
- `published`
- `snapshot`
- `event`
- `error`
---
## 3. 鉴权
### 3.1 authenticate
```json
{
"type": "authenticate",
"role": "producer",
"token": "dev-producer-token"
}
```
说明:
- `role` 可选值:`producer``consumer``controller`
- 第一版 `consumer` 可允许匿名,是否启用由配置控制
### 3.2 join_channel
```json
{
"type": "join_channel",
"role": "producer",
"channelId": "ch-xxxx",
"token": "producer-token"
}
```
说明:
- `channelId` 必填
- `token` 必须和 `channelId` 对应
- `role` 不同,使用的 token 也不同
- `producerToken`
- `consumerToken`
- `controllerToken`
---
## 4. 订阅
### 4.1 subscribe
```json
{
"type": "subscribe",
"subscriptions": [
{
"deviceId": "child-001",
"topic": "telemetry.location"
}
]
}
```
支持字段:
- `channelId`
- `deviceId`
- `groupId`
- `topic`
第一版匹配规则:
- 非空字段必须全部匹配
- 空字段视为不过滤
---
## 5. 发布
### 5.1 publish
```json
{
"type": "publish",
"envelope": {
"schemaVersion": 1,
"messageId": "msg-001",
"timestamp": 1711267200000,
"topic": "telemetry.location",
"source": {
"kind": "producer",
"id": "watch-001",
"mode": "real"
},
"target": {
"channelId": "ch-xxxx",
"deviceId": "child-001",
"groupId": "class-a"
},
"payload": {
"lat": 31.2304,
"lng": 121.4737,
"speed": 1.2,
"bearing": 90,
"accuracy": 6,
"coordSystem": "GCJ02"
}
}
}
```
说明:
- 只有 `producer``controller` 能发布
- 第一版不做复杂 schema 校验
- 第一版缓存键为 `channelId + deviceId`
- 如果连接已经通过 `join_channel` 加入通道,服务端会自动补全 `target.channelId`
---
## 6. 快照
### 6.1 snapshot
```json
{
"type": "snapshot",
"subscriptions": [
{
"channelId": "ch-xxxx",
"deviceId": "child-001"
}
]
}
```
服务端返回:
```json
{
"type": "snapshot",
"sessionId": "sess-2",
"state": {
"schemaVersion": 1,
"timestamp": 1711267200000,
"topic": "telemetry.location",
"source": {
"kind": "producer",
"id": "watch-001",
"mode": "real"
},
"target": {
"deviceId": "child-001"
},
"payload": {
"lat": 31.2304,
"lng": 121.4737
}
}
}
```
---
## 7. 服务端消息
### 7.1 welcome
```json
{
"type": "welcome",
"sessionId": "sess-1"
}
```
### 7.2 authenticated
```json
{
"type": "authenticated",
"sessionId": "sess-1"
}
```
### 7.3 subscribed
```json
{
"type": "subscribed",
"sessionId": "sess-1"
}
```
### 7.4 joined_channel
```json
{
"type": "joined_channel",
"sessionId": "sess-1",
"state": {
"channelId": "ch-xxxx",
"deliveryMode": "cache_latest"
}
}
```
### 7.5 published
```json
{
"type": "published",
"sessionId": "sess-1"
}
```
### 7.6 event
```json
{
"type": "event",
"envelope": {
"schemaVersion": 1,
"timestamp": 1711267200000,
"topic": "telemetry.location",
"source": {
"kind": "producer",
"id": "watch-001",
"mode": "real"
},
"target": {
"channelId": "ch-xxxx",
"deviceId": "child-001"
},
"payload": {
"lat": 31.2304,
"lng": 121.4737
}
}
}
```
### 7.7 error
```json
{
"type": "error",
"error": "authentication failed"
}
```
---
## 8. 第一版约束
- 不做历史回放协议
- 不做压缩和二进制编码
- 不做批量发布
- 不做通配符 topic
- 不做持久会话
- 不做 ACK 队列
---
## 9. 管理接口
### 9.1 创建 channel
`POST /api/channel/create`
请求:
```json
{
"label": "debug-a",
"deliveryMode": "cache_latest",
"ttlSeconds": 28800
}
```
返回:
```json
{
"snapshot": {
"id": "ch-xxxx",
"label": "debug-a",
"deliveryMode": "cache_latest"
},
"producerToken": "....",
"consumerToken": "....",
"controllerToken": "...."
}
```
### 9.2 管理台读取接口
- `GET /api/admin/overview`
- `GET /api/admin/sessions`
- `GET /api/admin/latest`
- `GET /api/admin/channels`
- `GET /api/admin/traffic`
- `GET /api/admin/live`
---
## 10. 第二阶段预留
后续协议可以增加:
- `command`
- `batch_publish`
- `rule_event`
- `plugin_status`
- `replay_control`
- `auth_refresh`

View File

@@ -0,0 +1,877 @@
# 实时设备数据网关最终方案
本文档用于收敛当前关于 GPS 模拟、中转、监控、规则判定、回放、通知分发等讨论,给出一版可直接进入实现设计的最终方案。
---
## 1. 目标与定位
本系统不再定义为“GPS 模拟器”,而定义为:
- 一个独立部署的实时设备数据网关
- 负责实时接入、标准化、路由、订阅、最新状态同步
- 默认不依赖数据库,不承担历史存储职责
- 通过插件扩展规则判定、通知分发、回放、归档等能力
一句话定义:
> 一个以实时中转为核心、以插件扩展业务能力的轻量遥测网关。
---
## 2. 设计原则
### 2.1 核心优先级
核心目标按优先级排序如下:
1. 实时性能
2. 稳定性
3. 低耦合
4. 易扩展
5. 易观测
### 2.2 核心边界
核心服务只负责:
- 长连接接入
- 消息标准化
- 路由转发
- 订阅管理
- latest state 缓存
- 连接管理
- 心跳和断线清理
- 基础鉴权
- 限流与基本防护
核心服务不负责:
- 历史存储
- 业务报表
- 复杂规则引擎
- 第三方通知发送
- 业务数据库查询
- 面向家长端或场控端的专属业务逻辑
---
## 3. 总体架构
### 3.1 拓扑关系
```text
Producer
├─ 手机客户端
├─ 外部模拟器
├─ 回放器
└─ 设备接入器
|
v
Realtime Device Gateway
├─ Connection Manager
├─ Protocol Layer
├─ Session Manager
├─ Router / Fanout
├─ Latest State Cache
└─ Plugin Bus
|
+--> Consumer
| ├─ 小程序 / App
| ├─ 家长端
| ├─ 场控端
| └─ 调试端 / 大屏
|
+--> Plugins
├─ Rule Engine
├─ Dispatcher
├─ Recorder
├─ Replayer Adapter
└─ Webhook / Bridge
Business Server
├─ 用户与设备关系
├─ 配置管理
├─ 历史归档
└─ 业务接口
```
### 3.2 在整个系统中的定位
实时网关在整个系统中,不是主业务服务器的替代品,而是一层独立的实时中枢。
职责分工建议固定为:
- 设备、模拟器、回放器
- 负责产出实时数据
- 实时网关
- 负责接入、路由、订阅、latest state、实时分发、运行态观察
- 业务服务器
- 负责用户、设备归属、配置、历史存档、业务查询
- 插件
- 负责规则、通知、归档、回放等异步能力
一句话理解:
> 主业务服务器负责“谁是谁”,实时网关负责“现在发生了什么,发给谁看”。
### 3.3 服务角色
- `Producer`
- 实时数据生产者
- 包括真实设备、模拟器、回放器
- `Consumer`
- 实时数据消费者
- 包括家长端、场控端、调试端、大屏
- `Controller`
- 可向 Producer 或 Gateway 下发控制命令
- 包括调试控制台、场控后台、回放控制器
### 3.4 角色使用流程
#### 开发模拟
1. Controller 在管理台创建 `channel`
2. 网关返回 `channelId / producerToken / consumerToken`
3. 老模拟器作为 Producer 加入 channel 并开始发 `telemetry.location / telemetry.heart_rate`
4. 管理台和调试 Consumer 实时观察数据
5. 业务服务器此时可以不参与,或仅做低频归档
#### 家长端监控
1. 真机设备作为 Producer 向网关持续上报位置和状态
2. 家长端作为 Consumer 订阅某个 `deviceId``groupId`
3. 网关负责实时分发
4. 业务服务器负责账号关系、历史数据和业务页面
#### 场控
1. 场控端作为 Consumer 订阅多个设备或一个分组
2. 网关持续推送实时位置、状态和事件
3. 如果有控制需求,场控端再以 Controller 身份下发命令
4. 规则插件可基于同一条实时流做违规判定
#### 归档
1. Producer 只向网关上报实时数据
2. Recorder 插件异步消费网关流
3. Recorder 再按 `10-30s` 批量写入业务服务器
这样可以保证:
- 实时链路和归档链路解耦
- 客户端可以逐步从双写演进到单写网关
- 实时数据和历史数据口径更一致
---
## 4. 部署原则
### 4.1 独立部署
实时网关必须独立于主业务服务器部署。
这样做的原因:
- 不占用主业务服务器的实时长连接资源
- 不让实时 fanout 干扰主业务接口
- 实时服务可独立扩容
- 便于分离故障和性能问题
### 4.2 与主业务服务的关系
主业务服务只提供控制面能力:
- 用户与设备归属关系
- 权限配置
- 策略配置
- 历史存档入口
实时网关处理数据面:
- 高频 telemetry
- 最新状态同步
- 实时下行通知和控制
原则上,不允许每条实时上报都回主业务服务查库。
---
## 5. 0 数据库方案
### 5.1 可行性
第一版核心网关可以做到 0 数据库依赖。
这里的含义是:
- 不接 MySQL
- 不接 PostgreSQL
- 不接业务持久化存储
- 不存历史轨迹
- 不做持久化 session
### 5.2 保留的内存态
即使 0 数据库,网关仍然必须保留以下运行时内存数据:
- 连接表
- 订阅关系
- 设备 latest state
- 会话元数据
- 限流计数
- 心跳状态
### 5.3 重启语义
网关重启后允许丢失:
- 当前连接
- 当前订阅
- latest state
- 临时限流计数
系统恢复方式:
- 客户端自动重连
- 重新鉴权
- 重新订阅
- Producer 继续上报最新点
这对实时网关是可接受的。
---
## 6. 性能目标
### 6.1 第一版容量目标
第一版至少满足:
- `1000-2000` 设备同时在线上报
- 默认 `1 Hz` 实时上报能力
- 支持部分调试设备升频到 `2-5 Hz`
- 端到端实时链路延迟目标 `< 500 ms`
- 插件故障不阻塞核心中转
### 6.2 带宽认知
实时中转一定消耗网关所在服务器的带宽,但不会占用主业务服务器带宽。
不做历史存储,只能减少:
- 磁盘 IO
- 存储成本
- 数据库压力
不能减少:
- 长连接压力
- 实时 fanout 压力
- 网关入口和出口带宽
### 6.3 核心优化原则
- 不允许全量广播
- 必须按设备或组定向订阅
- payload 尽量小
- latest state 只保留当前值
- 插件必须异步消费
- 高频调试源必须可限流
---
## 7. 消息模型
### 7.1 统一信封
所有进入网关的数据统一使用一个标准信封:
```json
{
"schemaVersion": 1,
"messageId": "uuid",
"timestamp": 1711267200000,
"topic": "telemetry.location",
"source": {
"kind": "producer",
"id": "watch-001",
"mode": "real"
},
"target": {
"deviceId": "child-001",
"groupId": "class-a"
},
"payload": {}
}
```
### 7.2 topic 分类
建议只保留四类顶级 topic
- `telemetry.*`
- `state.*`
- `event.*`
- `command.*`
示例:
- `telemetry.location`
- `telemetry.heart_rate`
- `telemetry.motion`
- `state.device`
- `event.rule_violation`
- `event.sos`
- `command.replay.start`
- `command.simulation.stop`
### 7.3 source mode
建议显式区分数据来源模式:
- `real`
- `mock`
- `replay`
- `control`
这样业务侧可以明确识别是真实数据还是模拟/回放数据。
---
## 8. 主要数据载荷
### 8.1 位置 telemetry
```json
{
"lat": 31.2304,
"lng": 121.4737,
"altitude": 12.3,
"speed": 1.5,
"bearing": 90,
"accuracy": 8,
"coordSystem": "GCJ02"
}
```
最少保留字段:
- `timestamp`
- `lat`
- `lng`
- `speed`
- `bearing`
- `accuracy`
- `coordSystem`
### 8.2 心率 telemetry
```json
{
"bpm": 142,
"confidence": 0.92
}
```
### 8.3 运动 telemetry
```json
{
"cadence": 168,
"stepCount": 3201,
"calories": 248.5,
"movementState": "run"
}
```
### 8.4 设备状态
```json
{
"online": true,
"battery": 76,
"network": "4g",
"charging": false
}
```
### 8.5 事件
```json
{
"eventType": "rule_violation",
"ruleId": "geo-fence-01",
"severity": "high",
"text": "leave allowed area"
}
```
### 8.6 控制命令
```json
{
"command": "simulation.start",
"args": {
"sessionId": "sim-001",
"speedRate": 2
}
}
```
---
## 9. 订阅与路由模型
### 9.1 路由单位
实时网关按以下维度路由:
- `deviceId`
- `groupId`
- `topic`
### 9.2 推荐订阅粒度
- 订阅某个设备全部实时消息
- 订阅某个设备某类消息
- 订阅某个组某类消息
示例:
- `device:child-001`
- `device:child-001/topic:telemetry.location`
- `group:class-a/topic:event.*`
### 9.3 latest state
每个设备保留一个最新状态快照,不存历史。
用途:
- 新订阅者刚连上时立即获得当前状态
- 页面重连后快速恢复
- 插件可从最新态快速计算
---
## 10. 鉴权策略
### 10.1 不建议完全不鉴权
如果完全不鉴权,会带来两个高风险问题:
- 任意客户端都能伪造位置或心率
- 任意客户端都可能下发控制命令
### 10.2 推荐分级鉴权
- `Producer`
- 必须强鉴权
- 防止伪造上报
- `Controller`
- 必须强鉴权
- 防止误操作和恶意控制
- `Consumer`
- 可轻鉴权
- 可按内网、白名单、临时 token、短期票据放宽
### 10.3 第一版推荐方式
第一版建议:
- Producer 使用签名 token 或短期接入 token
- Controller 使用管理 token
- Consumer 使用轻量订阅 token
网关本身不查数据库,只做:
- token 基本校验
- 角色识别
- 权限范围校验
复杂鉴权可以由独立鉴权服务承担。
---
## 11. 插件体系
### 11.1 原则
事件处理不是核心同步主链路的一部分,而是插件。
同步主链路只做:
`ingest -> normalize -> route -> fanout -> update latest state`
然后异步投递到插件总线:
`publish -> plugin bus -> plugin async consume`
### 11.2 第一批插件类型
- `Rule Engine`
- 实时规则判定
- `Dispatcher`
- 通知和下行分发
- `Recorder`
- 归档与历史写入
- `Replayer`
- 文件或历史流回放
- `Webhook`
- 向外部系统桥接
### 11.3 插件要求
- 不能阻塞主链路
- 失败必须隔离
- 可独立启停
- 最好支持单独部署
---
## 12. 规则判定与通知分发
### 12.1 规则不进核心
规则判定必须在插件层执行,不进入中转核心。
原因:
- 规则计算不稳定
- 容易引入复杂状态
- 容易放大 CPU 和 IO 消耗
- 不应影响实时转发
### 12.2 推荐规则类型
- 阈值规则
- 心率过高
- 速度异常
- 电量过低
- 时空规则
- 离开围栏
- 进入禁区
- 停留超时
- 偏离路线
- 行为规则
- 长时间静止
- 轨迹跳变
- 设备离线
### 12.3 动作分发
规则命中后由 Dispatcher 插件执行动作:
- 下发终端警告
- 推送给家长端
- 推送给场控端
- 上报业务服务
- 触发 Webhook
### 12.4 规则系统必须处理的问题
- 去重
- 冷却时间
- 恢复事件
- 幂等
---
## 13. 客户端上报策略
### 13.1 当前推荐的第一阶段方案
客户端保留两条链路:
- 实时链路
- 有需要时实时上报到网关
- 归档链路
-`10-30s` 打包一次发给业务服务器存档
这个方案的优点:
- 改造快
- 实时与归档解耦
- 主业务服务器不承受高频位置流
### 13.2 双写风险
客户端双写存在天然风险:
- 两边成功率不同
- 数据口径可能不一致
- 客户端逻辑更复杂
- 重试和去重更难
### 13.3 推荐演进方向
推荐第二阶段改成:
- 客户端只向网关实时上报
- `Recorder` 插件按 `10-30s` 批量归档到业务服务器
这样可以保证:
- 实时和归档数据同源
- 客户端逻辑更轻
- 后续回放、规则、告警直接复用同一条实时流
### 13.4 客户端升频策略
建议客户端支持策略切换:
- `normal`
- 低频或关闭实时上报
- `monitor`
- `1-5s` 实时上报
- `debug`
- `1 Hz`
- `alert`
- 临时升频
---
## 14. 模拟与回放
### 14.1 模拟器定位
模拟器不再是一个独立特殊系统,而是一个标准 Producer。
它可以上报:
- `telemetry.location`
- `telemetry.heart_rate`
- `telemetry.motion`
- `state.device`
### 14.2 回放器定位
回放器也是 Producer。
区别只是:
- source mode 为 `replay`
- 输入来源是文件或历史流
- 支持播放、暂停、倍速、循环
### 14.3 地图模拟器要求
地图拖点时不应直接瞬移发点,至少要支持:
- 时间戳
- 插值
- 平滑
- 速度和朝向生成
否则业务侧会看到不真实的轨迹。
---
## 15. 技术选型建议
### 15.1 技术选型原则
本系统技术选型必须遵守以下原则:
- 轻量
- 健壮
- 性能优先
- 少依赖
- 易部署
- 易排障
目标形态应更接近:
- 软路由插件
- 轻量代理
- 边缘网关
- 单二进制网络服务
而不是典型的重业务后台服务。
### 15.2 第一版推荐
核心网关建议明确采用以下技术栈:
- 服务端语言:`Go`
- 实时协议:`WebSocket`
- 管理接口:`HTTP`
- 消息格式:`JSON`
- 运行态存储:纯内存
- 配置方式:本地配置文件加环境变量
- 部署形态:单二进制
- 日志:结构化日志
推荐原因:
- 更适合长连接、低延迟、fanout 场景
- 内存和 CPU 占用更可控
- 二进制部署简单
- 依赖少,更接近基础设施服务风格
- 后续扩到更高在线数更稳
如果目标是先稳定支撑 `1000-2000` 在线连接,推荐优先考虑:
- 服务端:`Go`
- 实时协议:`WebSocket`
- 管理接口:`HTTP`
- 配置来源:本地配置文件或环境变量
- latest state进程内内存
原因:
- 长连接和 fanout 场景更贴近 Go 的强项
- 资源占用更稳
- 后续扩到更高并发成本更低
### 15.3 网关内部建议模块
为了保持“轻量但可扩展”,建议核心网关内部拆为以下模块:
- `listener`
- 管理 WebSocket 和 HTTP 入口
- `session manager`
- 管理连接、身份、心跳、会话元数据
- `router`
- 负责 topic、device、group 维度路由
- `fanout hub`
- 负责多订阅者分发
- `state cache`
- 保存设备 latest state
- `auth verifier`
- 校验 Producer、Consumer、Controller 的 token
- `rate limiter`
- 限制异常高频上报
- `plugin bus`
- 异步发布给规则、通知、归档等插件
这些模块都应保持进程内实现,避免第一版引入外部组件。
### 15.4 第一版不建议引入的依赖
第一版不建议引入:
- 数据库
- 消息队列
- 服务注册中心
- 复杂配置中心
- 脚本型规则执行器
- 重型 RPC 框架
这些能力都可以在后续容量和业务复杂度真正需要时再补。
### 15.5 可接受的替代方案
如果团队更熟悉 TypeScript也可以先用
- `Node.js + TypeScript + WebSocket`
但要注意:
- 网关层应保持非常薄
- 不要过早塞业务逻辑
- 一开始就准备好以后拆分插件
但如果核心目标明确是“像软路由一样稳定跑实时中转”,仍然优先推荐 Go。
---
## 16. MVP 范围
第一版只做以下内容:
### 16.1 核心网关
- WebSocket 接入
- Producer / Consumer / Controller 三类角色
- 统一消息信封
- 按设备订阅
- latest state 缓存
- 心跳和断线处理
- 基础限流
- 基础鉴权
### 16.2 模拟器接入
- 地图拖点
- 轨迹文件导入
- 播放 / 暂停 / 倍速 / 循环
- 输出位置和心率等标准 telemetry
### 16.3 观察端
- 订阅设备位置和状态
- 查看最新点
- 接收实时事件
### 16.4 插件最小集
- Recorder
- 可选启用
- Rule Engine
- 先做最简单几条规则
- Dispatcher
- 先支持网关内消息通知
### 16.5 明确不做
- 业务数据库耦合
- 复杂多租户
- 大报表系统
- 历史查询中心
- 集群调度系统
---
## 17. 第二阶段演进
第二阶段建议逐步增加:
- 归档从客户端双写迁移到 Recorder
- 多实例网关
- Redis 作为共享状态和实例间消息桥
- 更细粒度权限
- 完整规则库
- 回放服务独立化
- 外部通知集成
第三阶段再考虑:
- 数据库落盘
- 历史检索
- 统计分析
- 高可用和地域分布
---
## 18. 最终结论
最终方案建议如下:
- 以“实时设备数据网关”作为系统核心而不是继续围绕“GPS 模拟器”扩展
- 网关独立部署,不占主业务服务器的实时带宽和连接资源
- 第一版采用 0 数据库设计,仅保留运行时内存态
- 核心只做实时接入、标准化、路由、订阅和 latest state
- 规则判定、通知分发、回放、归档全部插件化
- 第一版先支撑 `1000-2000` 在线上报
- 当前客户端可继续采用“实时发网关 + 10-30 秒批量发业务服”的过渡方案
- 中期应演进为“客户端单写网关,归档由 Recorder 插件完成”
这套方案能同时覆盖:
- 开发模拟
- 家长端监控
- 场控
- 规则判定
- 通知分发
- 数据回放
- 后续更多传感器接入
同时又能把实时性能放在系统设计的首位。

View File

@@ -0,0 +1,443 @@
# Realtime Gateway 运行手册
本文档用于整理当前 `realtime-gateway` 的构建、运行、联调和排障方式,覆盖今天已经落地的能力。
## 1. 当前组成
当前工程由 3 部分组成:
- 新实时网关
- 路径:`D:\dev\cmr-mini\realtime-gateway`
- 老模拟器
- 路径:`D:\dev\cmr-mini\tools\mock-gps-sim`
- 文档目录
- 路径:`D:\dev\cmr-mini\doc`
当前推荐的本地开发方式:
- 老模拟器继续负责地图拖点、路径回放、心率模拟
- 新网关负责实时中转、channel 管理、实时查看、流量统计
## 1.1 中转服务器在整个系统中的用法
中转服务器在整个系统里,应当被当成“实时中枢”来使用,而不是业务主服务器的一部分。
当前建议的角色分工:
- `Producer`
- 老模拟器
- 后续真机设备
- 后续回放器
- `Consumer`
- 管理台
- CLI 调试端
- 后续家长端 / 场控端 / 手机观察端
- `Controller`
- 管理台
- 后续场控后台
- 后续回放控制器
- `Business Server`
- 用户、设备关系、配置、历史存档
系统里的基本流向应当是:
`Producer -> Gateway -> Consumer / Plugin -> Business Server`
也就是说:
- 实时数据先进入网关
- 网关负责实时观察和分发
- 业务服务器负责低频业务数据和历史
## 2. 端口约定
本地开发建议统一使用以下端口:
- 老模拟器 HTTP / WS`17865`
- 新网关 HTTP / WS`18080`
对应地址:
- 老模拟器页面:`http://127.0.0.1:17865/`
- 老模拟器本地 mock WS`ws://127.0.0.1:17865/mock-gps`
- 新网关管理台:`http://127.0.0.1:18080/`
- 新网关 WebSocket`ws://127.0.0.1:18080/ws`
## 3. 构建命令
### 3.1 构建网关
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go build -o .\bin\gateway.exe .\cmd\gateway
```
### 3.2 构建调试 CLI
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go build -o .\bin\mock-producer.exe .\cmd\mock-producer
go build -o .\bin\mock-consumer.exe .\cmd\mock-consumer
```
### 3.3 一次性编译检查
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go build ./...
```
## 4. 运行命令
### 4.1 启动新网关
开发期建议直接使用 Tunnel 开发配置:
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\gateway -config .\config\tunnel-dev.json
```
如果只想本机用默认开发配置:
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\gateway -config .\config\dev.json
```
### 4.2 启动老模拟器
在仓库根目录:
```powershell
cd D:\dev\cmr-mini
npm run mock-gps-sim
```
### 4.3 打开页面
- 新网关管理台:`http://127.0.0.1:18080/`
- 老模拟器页面:`http://127.0.0.1:17865/`
如果页面和实际代码不一致,先强刷一次:
```text
Ctrl + F5
```
## 5. 当前管理台能力
新网关管理台已经包含:
- 会话列表
- channel 创建与查看
- latest state 查看
- 实时数据窗口
- GPS / 心率专用格式化显示
- 轨迹预览
- 流量统计
- Published
- Dropped
- Fanout
- Topic 统计
- Channel 统计
相关接口:
- `/api/admin/overview`
- `/api/admin/sessions`
- `/api/admin/latest`
- `/api/admin/channels`
- `/api/admin/traffic`
- `/api/admin/live`
## 6. 本地联调方式
### 6.1 方式 A直接用 CLI
1. 启动网关
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\gateway -config .\config\tunnel-dev.json
```
2. 启动 consumer
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-consumer -config .\config\consumer-gps-heart.example.json
```
3. 发 GPS
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-producer -device-id child-001 -topic telemetry.location -count 5
```
4. 发心率
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-producer -device-id child-001 -topic telemetry.heart_rate -bpm 148 -count 5
```
### 6.2 方式 B老模拟器桥接新网关
这是当前最推荐的开发联调方式。
1. 启动网关
2. 启动老模拟器
3. 在老模拟器页面里打开“新网关桥接”
4. 填入:
- 网关地址:`ws://127.0.0.1:18080/ws`
- `Producer Token / Channel Token`
- `Channel ID` 可选
- `Device ID`
- `Group ID`
- `Source ID`
- `Source Mode`
5. 点“应用桥接配置”
这样可以同时保留:
- 老 mock 小程序链路
- 新网关旁路链路
### 6.3 现在最推荐的使用方式
今天这个阶段,最推荐这样用:
1. 老模拟器作为主 Producer
2. 新网关作为实时中转和观测面板
3. 管理台负责创建 channel、查看会话、观察实时流量
4. 业务服务器暂时不接入高频实时链路
这个组合的好处是:
- 不影响你现有模拟和调试方式
- 新网关可以独立收敛协议和运行态
- 出问题时边界清晰,便于排查
## 7. channel 模式怎么用
当前网关支持两种生产者接入模式。
### 7.1 老模式
不使用 channel直接走 `authenticate`
```json
{"type":"authenticate","role":"producer","token":"dev-producer-token"}
```
适合:
- 早期本机调试
- 兼容老桥接工具
### 7.2 新模式
先创建 channel再用 `join_channel`
```json
{"type":"join_channel","role":"producer","channelId":"ch-xxxx","token":"<producerToken>"}
```
适合:
- 多人联调
- 多会话隔离
- `drop_if_no_consumer` / `cache_latest` 策略
### 7.3 管理台创建 channel
在管理台可直接创建 channel返回
- `channelId`
- `producerToken`
- `consumerToken`
- `controllerToken`
### 7.4 老模拟器接 channel
老模拟器现在已经支持:
- 不填 `Channel ID`:走老的 `authenticate`
-`Channel ID`:自动走 `join_channel`
所以如果你在管理台里创建了 channel
- `Channel ID``channelId`
- `Producer Token / Channel Token``producerToken`
两者必须配套使用。
## 8. delivery mode 说明
当前 channel 支持两种分发策略:
- `cache_latest`
- 即使没有消费者,也会保留 latest state
- `drop_if_no_consumer`
- 没有消费者就直接丢弃,不保留 latest state
适用建议:
- 开发调试或临时通道:可用 `drop_if_no_consumer`
- 常规联调和状态观察:建议 `cache_latest`
## 9. 管理台实时窗口说明
“实时数据窗口”当前支持:
-`topic` 过滤
-`channelId` 过滤
-`deviceId` 过滤
- 实时事件滚动显示
- GPS 专用摘要
- 经纬度
- 速度
- 航向
- 精度
- 心率专用摘要
- bpm
- 轨迹预览
-`channelId / deviceId` 聚合
建议使用方式:
- 如果联调设备较多,先填 `channelId`
- 如果只看单个对象,再加 `deviceId`
## 10. 流量统计说明
网关已累计以下指标:
- `Published`
- 收到的总发布数
- `Dropped`
- 因策略被丢弃的发布数
- `Fanout`
- 实际分发到消费者的总次数
同时支持:
-`topic` 统计
-`channel` 统计
这几个指标适合用来观察:
- 老模拟器是否在稳定发流
- 哪个 topic 最热
- 哪个 channel 在大量占用实时流量
- `drop_if_no_consumer` 是否正在生效
## 11. 常用命令备忘
### 11.1 启动网关
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\gateway -config .\config\tunnel-dev.json
```
### 11.2 启动老模拟器
```powershell
cd D:\dev\cmr-mini
npm run mock-gps-sim
```
### 11.3 编译检查
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go build ./...
```
### 11.4 直接发 GPS
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-producer -device-id child-001 -topic telemetry.location -count 5
```
### 11.5 直接发心率
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-producer -device-id child-001 -topic telemetry.heart_rate -bpm 148 -count 5
```
### 11.6 channel 模式下的 producer
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-producer -channel-id ch-xxxx -token <producer-token> -topic telemetry.location -count 5
```
### 11.7 channel 模式下的 consumer
```powershell
cd D:\dev\cmr-mini\realtime-gateway
go run .\cmd\mock-consumer -channel-id ch-xxxx -token <consumer-token> -topics telemetry.location,telemetry.heart_rate
```
## 12. 常见问题
### 12.1 老模拟器显示 `authentication failed`
通常是这两种情况:
- 你填了 `producerToken`,但没填 `channelId`
- 你填的是 channel 的 token却还在走老的 `authenticate`
处理方式:
- 如果用 channel
- 必须同时填 `Channel ID` 和对应 `producerToken`
- 如果不用 channel
- `Channel ID` 留空
- token 使用全局 `dev-producer-token`
### 12.2 模拟器还连到 8080
当前开发统一使用 `18080`
如果页面还显示旧地址:
- 重启模拟器服务
- 浏览器强刷 `Ctrl + F5`
### 12.3 管理台创建 channel 成功,但页面没显示
这通常是浏览器缓存旧前端资源。
处理方式:
- 重启网关
- 打开 `http://127.0.0.1:18080/`
- 按一次 `Ctrl + F5`
### 12.4 管理台看不到实时数据
先检查:
- 网关是否启动在 `18080`
- 老模拟器桥接是否已认证
- 管理台实时窗口是否误填了 `channelId / deviceId` 过滤
## 13. 当前建议
今天这个阶段,最稳的开发方式是:
- 老模拟器继续做主生产者
- 新网关继续做中转和观测
- 手机端暂时不接正式消费链路
- 先把网关本身的运行态、流量、实时查看能力做稳
这也是当前最省风险的组合。