WebSocket 实时通信实战:从握手到全双工消息的完整指南
WebSocket 实时通信实战:从握手到全双工消息的完整指南
从轮询到长轮询再到 WebSocket,实时通信经历了漫长的进化。WebSocket 以"一次握手、全双工、低延迟"的特性,成为聊天、协作、行情推送等实时场景的主流方案。本文从协议原理到生产实践,完整讲解 WebSocket 的落地。
一、为什么需要 WebSocket
HTTP 是请求-响应模式:客户端不请求,服务器就无法主动推送。WebSocket 通过一次 HTTP 升级握手建立长连接,之后双方可以随时互发消息:
- 低延迟:无需轮询开销,消息毫秒级到达;
- 全双工:服务端可主动推送(通知、行情、在线状态);
- 轻量:帧开销仅 2~14 字节,远小于 HTTP 头。
二、握手与协议基础
2.1 握手过程
客户端发起升级请求,服务端返回 101:
客户端 → 服务器
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务器 → 客户端
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept 由 Key + 固定 GUID 做 SHA-1 后 Base64 生成,双方据此确认协议版本。
2.2 帧格式要点
WebSocket 帧最小 2 字节:FIN + Opcode + Mask + Len + Payload。客户端发往服务器必须掩码(Mask=1),服务器发客户端不掩码。
三、服务端实现:Go + Gorilla WebSocket
3.1 基础连接
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true }, // 生产需校验来源
}
func handleWS(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { log.Println("升级失败:", err); return }
defer conn.Close()
// 读循环
for {
_, msg, err := conn.ReadMessage()
if err != nil { log.Println("连接关闭:", err); break }
handleMessage(r.Context(), conn, msg)
}
}
func main() {
http.HandleFunc("/ws", handleWS)
http.ListenAndServe(":8080", nil)
}
3.2 并发安全:写锁是必须的
WebSocket 连接不允许并发写,多个 goroutine 同时 WriteMessage 会数据错乱:
type Client struct {
conn *websocket.Conn
send chan []byte // 发送队列
mu sync.Mutex
}
func (c *Client) Write(msg []byte) {
c.mu.Lock() // 串行化写入
defer c.mu.Unlock()
c.conn.WriteMessage(websocket.TextMessage, msg)
}
更稳妥的模式是单写 goroutine:所有消息投递到 send channel,由唯一 writer 循环写出,天然无竞争。
四、连接管理:Hub 模式
实时服务需要维护所有在线连接,实现广播与定向推送:
type Hub struct {
clients map[*Client]bool // 在线连接
register chan *Client // 新连接
unregister chan *Client // 断开连接
broadcast chan []byte // 广播消息
}
func (h *Hub) Run() {
for {
select {
case c := <-h.register:
h.clients[c] = true
case c := <-h.unregister:
if _, ok := h.clients[c]; ok {
delete(h.clients, c)
close(c.send)
}
case msg := <-h.broadcast:
for c := range h.clients {
select {
case c.send <- msg:
default: // 发送队列满,关闭慢消费者
delete(h.clients, c)
close(c.send)
}
}
}
}
}
慢消费者保护:某个客户端处理不过来(队列满)时必须断开,否则它会拖垮整个 Hub——"杀掉慢的,保住全局"。
五、生产环境的三个关键问题
5.1 鉴权:连接前的身份确认
WebSocket 握手是普通 HTTP,鉴权在握手阶段完成:
// 方式一:握手 URL 带 token
ws := new WebSocket(`wss://api.example.com/ws?token=` + token)
// 方式二:握手请求带 Cookie/Header
// 服务端在 Upgrade 前校验身份,不通过则返回 401
func handleWS(w http.ResponseWriter, r *http.Request) {
if !validateAuth(r) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
conn, _ := upgrader.Upgrade(w, r, nil)
...
}
原则:鉴权必须在 Upgrade 之前完成,不要在连接建立后用消息来鉴权。
5.2 心跳保活
网络中间件会回收空闲连接,必须周期性发 Ping/Pong:
// 服务端定期发 Ping
go func() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second))
}
}()
// 读循环中设置读超时,超时未收到 Pong 即断开
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
5.3 水平扩展:多实例间的消息路由
单机 Hub 无法支撑多实例部署。方案:
- Redis Pub/Sub:实例订阅频道,广播消息发到频道,各实例推给各自连接;
- MQ(Kafka/NATS):消息量大时用 MQ 解耦;
- 网关层路由:按用户 ID 哈希到实例(如
user_id % N),配合 Redis 记录"用户在哪个实例"。
// Redis 广播示例
redis.Publish(ctx, "ws:broadcast", string(msg))
// 各实例订阅
redis.Subscribe(ctx, "ws:broadcast")
六、消息协议设计
生产环境建议在 WebSocket 之上定义自己的消息信封:
{
"type": "chat.message",
"id": "evt_001",
"data": { "room_id": "r1", "sender": "u3", "text": "你好" }
}
设计要点:
type字段驱动客户端分发;- 业务消息带幂等键(重连重投场景);
- 消息体积设上限(如 64KB),超大内容走附件。
七、前端消费
// 带自动重连的轻量客户端
export function useSocket(url: string) {
let ws: WebSocket | null = null
function connect() {
ws = new WebSocket(url)
ws.onmessage = (e) => handlers.forEach((h) => h(JSON.parse(e.data)))
ws.onclose = () => setTimeout(connect, 3000) // 指数退避重连
ws.onerror = () => ws?.close()
}
connect()
return { send: (msg: unknown) => ws?.readyState === 1 && ws.send(JSON.stringify(msg)) }
}
前端注意:断线重连后要重新订阅频道、补齐离线期间的消息(由服务端按游标补发)。
结语
WebSocket 让实时交互从"轮询的妥协"变成"真正的全双工"。落地要点可以浓缩为四句话:握手时鉴权、连接要心跳、写入要串行、扩展靠路由。无论做聊天室、协作编辑还是行情推送,先搭好 Hub 与心跳骨架,再逐步加鉴权与水平扩展——实时系统的稳定性,正是这些基础细节堆出来的。
读完了?给作者一点支持
评论区加载中…