Skip to content

WebSocket 基础

Phase 06 — Real-time · 知识点 01–03:WebSocket · Connection · Message


1. 学习目标

完成本知识点后,你应该能够:

  • 解释 WebSocket 与 HTTP 长轮询的本质区别
  • 在 Go 中使用 gorilla/websocket 实现 WebSocket 升级与握手
  • 管理客户端 Connection 的生命周期(建立、读写、关闭)
  • 设计适合 Three.js 数字孪生场景的消息格式(JSON)
  • 区分 Text Message 与 Binary Message 的使用场景
  • 正确处理读写 goroutine 与 Close 语义

2. 为什么需要

数字孪生前端(Vue + Three.js)需要持续接收 AGV 位置、设备状态、告警等增量数据。HTTP 请求-响应模型每次都要重新建立连接,开销大、延迟高,不适合高频推送。

WebSocket 在单次 HTTP 握手后升级为全双工、长连接通道,服务端可以主动推送,客户端也可以随时发送订阅指令。你在前端已经用过 WebSocket API,本阶段重点是从 Go 服务端 建立连接、管理会话并推送结构化消息。


3. 核心概念

3.1 WebSocket 协议

概念说明
握手客户端发 HTTP Upgrade 请求,服务端返回 101 Switching Protocols
帧(Frame)数据以帧传输,支持 Text、Binary、Ping、Pong、Close
全双工同一连接上读写可并发进行
持久连接连接保持直到一方关闭或网络中断

3.2 Connection(连接)

在 Go 服务端,每个客户端对应一个 *websocket.Conn 实例。你需要:

  • 在升级成功后保存连接引用
  • 为每个连接分配唯一 ID(便于日志与调试)
  • Read 循环中处理客户端消息
  • 在断开时清理资源,避免 goroutine 泄漏

3.3 Message(消息)

类型用途
TextJSON 文本,数字孪生场景首选
BinaryProtobuf、压缩数据,高吞吐场景
Ping/Pong协议层心跳(与应用层心跳互补)

3.4 常用库对比

特点
gorilla/websocket社区最常用,文档丰富,本路线默认选用
nhooyr.io/websocketAPI 更现代,Context 友好
标准库(Go 1.20+ 实验特性)长期方向,生产环境仍以成熟库为主

4. 基础语法

4.1 安装依赖

bash
go get github.com/gorilla/websocket

4.2 最小 WebSocket 服务端

go
package main

import (
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        // 开发环境允许所有 Origin;生产环境应校验具体域名
        return true
    },
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("upgrade:", err)
        return
    }
    defer conn.Close()

    for {
        _, msg, err := conn.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            return
        }
        log.Printf("recv: %s", msg)

        if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
            log.Println("write:", err)
            return
        }
    }
}

func main() {
    http.HandleFunc("/ws", wsHandler)
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

4.3 消息格式示例(数字孪生 AGV)

go
type AGVPositionMsg struct {
    Type      string  `json:"type"`       // "agv.position"
    DeviceID  string  `json:"deviceId"`
    X         float64 `json:"x"`
    Y         float64 `json:"y"`
    Heading   float64 `json:"heading"`
    Timestamp int64   `json:"timestamp"`  // Unix 毫秒
}

前端 Three.js 收到后更新 Mesh 位置:

javascript
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'agv.position') {
    updateAGVMesh(msg.deviceId, msg.x, msg.y, msg.heading);
  }
};

4.4 读写分离模式

go
func serveConn(conn *websocket.Conn) {
    done := make(chan struct{})

    go func() {
        defer close(done)
        for {
            _, _, err := conn.ReadMessage()
            if err != nil {
                return
            }
        }
    }()

    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-done:
            return
        case <-ticker.C:
            payload, _ := json.Marshal(AGVPositionMsg{
                Type: "agv.position", DeviceID: "AGV-001",
                X: 12.5, Y: 3.8, Heading: 90, Timestamp: time.Now().UnixMilli(),
            })
            if err := conn.WriteMessage(websocket.TextMessage, payload); err != nil {
                return
            }
        }
    }
}

5. 代码解析

go
var upgrader = websocket.Upgrader{ ... }

Upgrader 负责把 HTTP 连接升级为 WebSocket。CheckOrigin 用于 CORS 校验,开发时可放宽,上线必须限制来源。

go
conn, err := upgrader.Upgrade(w, r, nil)

Upgrade 完成握手。成功后 http.ResponseWriter 的底层连接被 WebSocket 接管,不能再w.Write 写 HTTP 响应。

go
_, msg, err := conn.ReadMessage()

阻塞读取一条完整消息。返回消息类型、字节内容和错误。客户端断开时通常返回 websocket: close 1006 等错误。

go
conn.WriteMessage(websocket.TextMessage, payload)

发送 Text 帧。同一 Conn 的并发 WriteMessage 需要加锁,或使用 WriteJSON/NextWriter 等 API 时注意文档说明。


6. JavaScript / TypeScript 对比

概念浏览器端Go 服务端(gorilla/websocket)
建立连接new WebSocket(url)upgrader.Upgrade(w, r, nil)
接收消息ws.onmessageconn.ReadMessage() 循环
发送消息ws.send(data)conn.WriteMessage(...)
关闭ws.close()conn.Close()
事件驱动回调 / Promise 封装阻塞 IO + goroutine
二进制ArrayBuffer / Blob[]byte + BinaryMessage

关键差异

  1. 前端 WebSocket 是事件回调;Go 端通常用 for 循环 + goroutine 处理
  2. Go 必须显式处理 Upgrade 失败和读写错误
  3. 消息格式需前后端约定 type 字段,便于 Three.js 路由到不同更新逻辑

7. 常见错误

错误 1:Upgrade 后仍写 HTTP 响应

go
conn, _ := upgrader.Upgrade(w, r, nil)
fmt.Fprintln(w, "ok")  // ❌ 连接已切换,不可再写 HTTP body

错误 2:并发 Write 不加锁

多个 goroutine 同时 WriteMessage 可能导致帧交错。使用 sync.Mutex 或单写 goroutine。

错误 3:不设置 ReadDeadline 导致 goroutine 永久阻塞

长时间无数据时,应配合心跳或 SetReadDeadline 检测死连接。

错误 4:消息无 type 字段

前端无法区分快照、增量、告警,Three.js 场景更新逻辑会混乱。

错误 5:生产环境 CheckOrigin: func() bool { return true }

应校验 Origin 是否为允许的 Vue 前端域名。


8. 实际应用

数字孪生 — AGV 实时位置推送

架构概览

[模拟器/PLC] → [Go 数据采集] → [WebSocket Hub] → [Vue/Three.js 前端]

推荐消息类型

type说明主要字段
agv.snapshot全量快照devices[]
agv.position位置增量deviceId, x, y, heading
agv.status状态变更deviceId, status, battery
alert.new新告警level, message, deviceId

Connection 管理要点

  • 连接建立时发送 agv.snapshot,保证 Three.js 场景初始状态一致
  • 每个 Connection 记录订阅的仓库/区域(Room,下一知识点详述)
  • 断开时从 Hub 注销,释放内存

9. 深入理解

9.1 WebSocket vs SSE

特性WebSocketSSE (Server-Sent Events)
方向双向单向(服务端→客户端)
协议独立升级普通 HTTP 长连接
适用实时控制 + 推送纯推送、日志流

数字孪生如需客户端发送订阅/ACK,选 WebSocket。

9.2 缓冲与背压

慢客户端会导致写缓冲堆积。应监控 Write 错误,必要时断开慢连接,避免拖垮整个服务。

9.3 gorilla/websocket 与 net/http 集成

WebSocket Handler 仍是标准 http.HandlerFunc,可与 Gin 等框架的路由并存;Gin 也有 c.Writer 升级方式,但理解标准库升级过程更重要。


10. 练习

请独立完成,不要查看答案。完成后说「检查答案」并提交代码。

详细练习见 exercises/phase-06-realtime/01-websocket-fundamentals.md

Level 1 — 基础

练习 1.1:搭建最小 Echo WebSocket 服务,路径 /ws,用 websocat 或浏览器测试。

练习 1.2:定义 AGVPositionMsg 结构体,服务端每 2 秒推送一条 JSON。

练习 1.3:记录每个新连接的远程地址和连接时间。

Level 2 — 应用

练习 2.1:实现读写分离:读 goroutine 处理客户端 ping 文本,写 goroutine 定时推送位置。

练习 2.2:客户端发送 {"action":"subscribe","deviceId":"AGV-001"},服务端仅推送该设备数据。

练习 2.3:添加 WriteJSON / ReadJSON 封装,统一错误日志格式。

Level 3 — 综合

练习 3.1:实现 Hub 结构,维护 map[string]*Connection,支持注册与注销。

练习 3.2:连接建立时推送 agv.snapshot,包含至少 3 台 AGV 初始位置。

Level 4 — 项目实践

练习 4.1:为 Project 02 创建 WebSocket 端点,对接 Three.js 测试页,实现 AGV Mesh 实时移动。


11. 学习检查

完成练习后,确认你能回答:

  1. WebSocket 握手 HTTP 状态码是什么?
  2. Upgrade 失败后应该如何处理?
  3. 为什么推荐读写分离?
  4. 数字孪生消息为什么要包含 typetimestamp
  5. CheckOrigin 在生产环境应如何配置?
  6. Text Message 与 Binary Message 如何选择?

12. 下一步

已完成下一知识点关系
WebSocket · Connection · MessageHeartbeat · Reconnect · Broadcast · Room连接可用 → 可靠性与多客户端

建议顺序:掌握基础连接与消息格式后,学习心跳保活、断线重连、广播与房间隔离,为多人同时观看同一仓库数字孪生场景做准备。


学习导航

上一篇:Redis 缓存、Session 与 Pub/Sub · 对应练习 · 下一篇:WebSocket 可靠性与广播