外观
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(消息)
| 类型 | 用途 |
|---|---|
| Text | JSON 文本,数字孪生场景首选 |
| Binary | Protobuf、压缩数据,高吞吐场景 |
| Ping/Pong | 协议层心跳(与应用层心跳互补) |
3.4 常用库对比
| 库 | 特点 |
|---|---|
| gorilla/websocket | 社区最常用,文档丰富,本路线默认选用 |
| nhooyr.io/websocket | API 更现代,Context 友好 |
| 标准库(Go 1.20+ 实验特性) | 长期方向,生产环境仍以成熟库为主 |
4. 基础语法
4.1 安装依赖
bash
go get github.com/gorilla/websocket4.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.onmessage | conn.ReadMessage() 循环 |
| 发送消息 | ws.send(data) | conn.WriteMessage(...) |
| 关闭 | ws.close() | conn.Close() |
| 事件驱动 | 回调 / Promise 封装 | 阻塞 IO + goroutine |
| 二进制 | ArrayBuffer / Blob | []byte + BinaryMessage |
关键差异:
- 前端 WebSocket 是事件回调;Go 端通常用
for循环 + goroutine 处理 - Go 必须显式处理
Upgrade失败和读写错误 - 消息格式需前后端约定
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
| 特性 | WebSocket | SSE (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. 学习检查
完成练习后,确认你能回答:
- WebSocket 握手 HTTP 状态码是什么?
Upgrade失败后应该如何处理?- 为什么推荐读写分离?
- 数字孪生消息为什么要包含
type和timestamp? CheckOrigin在生产环境应如何配置?- Text Message 与 Binary Message 如何选择?
12. 下一步
| 已完成 | 下一知识点 | 关系 |
|---|---|---|
| WebSocket · Connection · Message | Heartbeat · Reconnect · Broadcast · Room | 连接可用 → 可靠性与多客户端 |
建议顺序:掌握基础连接与消息格式后,学习心跳保活、断线重连、广播与房间隔离,为多人同时观看同一仓库数字孪生场景做准备。
学习导航
上一篇:Redis 缓存、Session 与 Pub/Sub · 对应练习 · 下一篇:WebSocket 可靠性与广播