前两篇讲了架构/CI/CD 和前端渲染。最后这篇落到后端:浏览器怎么实时拿到工厂数据。
核心需求很明确——一个 WebSocket 端点,后端每秒采样一次遥测,广播给所有连接的浏览器;新连接进来先推一份全量快照。选型 Go + gorilla/websocket:长连接高并发下 goroutine 模型很轻;编译成单二进制,2G 小服务器也能跑;配合 gin 做路由。
一、目录结构
server/
├── cmd/main.go # gin 路由 + 启动 Hub
├── Dockerfile.server # golang:1.27 → distroless
├── go.mod
└── internal/
├── ws/ws.go # Hub 广播 + 升级 + 协议
├── ws/ws_test.go # 100% 覆盖测试
├── mock/ # 遥测引擎(采样 + 报警)
└── model/ # Asset / Metric / Alarm 领域模型
二、Hub 广播模型:把连接抽象成接口
所有连接挂在 Hub 上。关键设计:把 websocket.Conn 抽象成 conn 接口,这样测试能注入假的连接,不用真起 socket:
// server/internal/ws/ws.go type conn interface { Close() error WriteMessage(int, []byte) error ReadMessage() (int, []byte, error) } type Hub struct { mu sync.Mutex clients map[*Client]struct{} engine Telemetry }
广播就是锁内遍历所有客户端,往各自 send channel 塞消息:
func (h *Hub) broadcastMsg(msg []byte) { h.mu.Lock() defer h.mu.Unlock() for c := range h.clients { select { case c.send <- msg: // 客户端消费快:投递 default: // 客户端消费慢:直接丢弃并清理 c.drop() delete(h.clients, c) } } }
这个 default 分支很重要:某台浏览器卡死、网络抖动,send channel 满(缓冲 256),新消息直接丢、连接清理。避免一个慢客户端拖垮整个广播循环——实时监控场景下这是必要的背压策略。
三、协议:snapshot / tick / alarm / ping
新连接进来,先推全量快照,再进每秒 tick 循环:
// 升级后:注册 + 推快照 + 起读写 pump func (h *Hub) Serve(c *gin.Context) { conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { return } cli := &Client{conn: conn, send: make(chan []byte, 256), hub: h} h.register(cli) assets, metrics := h.engine.Snapshot() snap, _ := json.Marshal(map[string]any{ "type": "snapshot", "ts": time.Now().UnixMilli(), "assets": assets, "metrics": metrics, }) cli.send <- snap go cli.writePump() go cli.readPump() }
每秒 Tick 采样并广播指标 + 报警:
func (h *Hub) Tick() { h.engine.Sample() vals := make(map[string]float64) for _, m := range h.engine.Metrics() { vals[m.ID] = m.Value } tick, _ := json.Marshal(map[string]any{"type": "tick", "ts": now, "updates": vals}) h.broadcastMsg(tick) for _, a := range h.engine.Alarms() { h.broadcastMsg(alarmMsg(a)) } }
前端 WsClient(web/lib/ws.ts)按 type 分发:snapshot 灌入全局 store,tick 增量更新指标,alarm 推入告警列表。客户端还会每 20s 发 ping,后端回 pong,断线指数退避重连(最多 10s)。
四、坑:CheckOrigin 必须放行 Vercel 域
浏览器 WS 握手带 Origin,后端 CheckOrigin 校验防跨站。最初写的是精确白名单,只放行 https://twin.wxbuluo.com。结果前端上了 Vercel(默认域 wxbuluo-twin.vercel.app,preview 部署还有 *.vercel.app),握手直接被拒,连不上后端。
修法:精确白名单 + 受信任部署平台后缀放行:
func checkOrigin(r *http.Request) bool { origin := r.Header.Get("Origin") if origin == "" { return true } // 非浏览器客户端放行 for _, o := range allowedOriginsFromEnv(...) { if o == origin { return true } // 精确白名单(TWIN_WS_ALLOWED_ORIGINS) } // 受信任部署平台:Vercel 默认域/预览部署 + 自有 wxbuluo.com 子域 if strings.HasSuffix(origin, ".vercel.app") || strings.HasSuffix(origin, ".wxbuluo.com") { return true } return false }
TWIN_WS_ALLOWED_ORIGINS 仍保留给"必须精确限定"的生产部署,默认 http://localhost:3100。这样以后 Vercel 新增任何部署域都不会再卡 origin。
五、遥测引擎(mock)与测试门禁
真实工厂数据走 OPC-UA / MQTT 接入,演示阶段用 mock.Engine:随机游走采样、阈值触发报警。Telemetry 接口隔离,Hub 不依赖具体实现。
CI 有** 100% 覆盖率门禁**(go test ./internal/... -cover)。conn 接口可注入的优势在这体现:测试里塞一个内存 conn 假实现,直接验证"广播是否真的发到每个客户端""慢客户端是否被丢弃""CheckOrigin 各分支是否覆盖"。门禁不过,CI 直接红,镜像不推——保证推上服务器的代码一定带测试。
六、镜像:distroless + 静态编译
2G 服务器跑不动 Go 编译,镜像在 GitHub runner 构建(见第一篇)。Dockerfile 用多阶段,最终落到 distroless,体积小、攻击面小:
# server/Dockerfile.server FROM golang:1.27 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /out/twin-server ./cmd FROM gcr.io/distroless/static-debian12 COPY --from=builder /out/twin-server /app/twin-server EXPOSE 8080 CMD ["/app/twin-server"]
CGO_ENABLED=0 静态链接,distroless 里无 libc 也能跑。
七、gin 路由与启动
main.go 极简:注册健康检查、WS、起 Hub 采样循环:
r.GET("/healthz", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) r.GET("/ws/", hub.Serve) addr := os.Getenv("ADDR"); if addr == "" { addr = ":8080" } go hub.Run(context.Background(), 1*time.Second) // 每秒采样广播 _ = r.Run(addr)
/healthz 是 CI 部署后的健康检查端点——runner 拉起容器后 curl 它确认服务真活了才算部署成功(deploy.yml 里那段 for i in 1..10 轮询)。
八、完整链路回顾
浏览器 (vercel) --wss--> nginx slb (443, Let's Encrypt)
--proxy_pass--> twin-server 容器 (docker_default:8080)
└ Hub.Run 每秒 Tick → broadcastMsg → 所有 WS 客户端
deploy.yml 只筛 server/**,push 即触发:runner 构建推 ghcr.io/ningbnii/wxbuluo-twin-server:latest → SSH 服务器 pull + docker compose up -d → 健康检查。服务器 2G 内存只拉镜像,绝不编译。
小结:后端关键点
| 关注点 | 做法 |
|---|---|
| 广播 | Hub + per-client send channel,慢客户端非阻塞丢弃 |
| 协议 | snapshot(首推全量)/ tick(每秒增量)/ alarm / ping-pong |
| 跨域 | CheckOrigin 精确白名单 + *.vercel.app / *.wxbuluo.com 后缀 |
| 可测 | conn 接口注入假连接,100% 覆盖门禁卡 CI |
| 部署 | distroless 静态二进制 + GitHub Actions 自动推镜像 |
到这里,数字孪生项目的前后端 CI/CD 全部打通:前端 R3F 渲染带纹理的工业设备、状态实时发光;后端 Go 把遥测秒级广播;WS 经 nginx 反代端到端 101 Switching Protocols。系列三篇完。