在 Web 开发中,大多数人每天都在编写 HTTP 接口,却很少真正思考一个问题:如果服务端需要"主动"把消息推送给客户端,该怎么办?
传统的 HTTP 请求--响应模型决定了通信只能由客户端发起,这在即时通信、实时推送、在线协作等场景下显得力不从心。正是在这样的背景下,WebSocket 应运而生。
本文将从通信模型的角度出发,系统讲解 WebSocket 是如何工作的,以及它为何成为实时 Web 系统的核心技术。
一、为什么需要 WebSocket
1.HTTP 的先天缺陷
HTTP 是 请求-响应模型:
客户端请求 → 服务端响应 → 连接关闭(或复用)
问题:
-
服务端 不能主动推送数据
-
实时性差
-
高频轮询浪费资源
例如以下的场景:
-
聊天消息
-
股票行情
-
设备状态变化
如果用 HTTP进行轮询
客户端:有新消息吗?
客户端:有新消息吗?
客户端:有新消息吗?
客户端会一直发送请求,而服务端会一直判断请求的结果。这样会导致一系列问题比如:
低效、延迟高、服务器压力大
2.WebSocket 的核心目标
建立一次连接,长期保持,双向实时通信
WebSocket 让通信模型变成:
客户端 ⇄ 服务端(随时互相发消息)
二、WebSocket 的本质
WebSocket 的本质是借用 HTTP 握手兼容网络、基于 TCP 实现的全双工应用层长连接协议,核心解决了 Web 场景下 "服务端主动推数据" 的需求。
WebSocket 是 HTML5 规范定义的应用层协议(RFC 6455 标准),其底层完全依赖 TCP 协议提供的可靠字节流传输,最终实现「客户端与服务端的全双工、长连接通信」
WebSocket = 基于 TCP 的全双工长连接协议
三、WebSocket 握手全过程
WebSocket 通过 HTTP Upgrade 建立连接
WebSocket 为了兼容现有网络(防火墙、代理通常放行 HTTP),采用「HTTP 升级握手」的方式建立连接:
- 客户端发送 HTTP 请求,头部携带
Upgrade: websocket和Connection: Upgrade,声明要升级为 WebSocket 协议; - 服务端同意升级,返回 101 Switching Protocols 响应;
- 握手完成后,底层 TCP 连接被 "接管",后续通信不再遵循 HTTP 规则,而是用 WebSocket 帧格式传输数据。
客户端发起 HTTP 请求
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务端返回 101 Switching Protocols
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
HTTP 协议结束,TCP 连接不关闭,开始使用WebSocket 帧协议
四、WebSocket 帧结构
| FIN | OPCODE | MASK | PAYLOAD LEN | DATA |
1. FIN:帧是否为消息的 "最后一帧"
核心作用:WebSocket 支持 "消息分片"------ 一个完整的消息可以拆分成多个帧发送,FIN 标记当前帧是否是该消息的最后一帧;
若一条消息拆成 3 帧 发送:第 1、2 帧 FIN=0,第 3 帧 FIN=1;
2. OPCODE:帧的 "类型"(操作码)
| OPCODE 值 | 含义 | 场景 |
|---|---|---|
| 0x0(0) | 延续帧(Continuation Frame) | 消息分片时,非首帧的后续帧 |
| 0x1(1) | 文本帧(Text Frame) | 传输 UTF-8 文本数据(如 JSON) |
| 0x2(2) | 二进制帧(Binary Frame) | 传输二进制数据(如文件、图片) |
| 0x8(8) | 关闭帧(Close Frame) | 主动关闭连接时发送 |
| 0x9(9) | 心跳请求(Ping Frame) | 检测连接是否存活(服务端 / 客户端均可发) |
| 0xA(10) | 心跳响应(Pong Frame) | 收到 Ping 后必须回复 Pong |
3. MASK:载荷数据是否 "加掩码"
核心规则(强制):
客户端 → 服务端:MASK=1(必须加掩码,否则服务端拒收);
服务端 → 客户端:MASK=0(禁止加掩码);
为什么加掩码?:防止恶意数据被中间件(如代理)误解为 HTTP 协议,提升安全性;
4. PAYLOAD LEN:载荷数据的长度
5. DATA:实际传输的 "有效数据"(PAYLOAD DATA)
五、Go 中 WebSocket 的基本使用
Go 标准库 不直接提供 WebSocket,通常使用:github.com/gorilla/websocket
WebSocket 服务端
Go
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 生产环境需校验
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
for {
msgType, msg, err := conn.ReadMessage()
if err != nil {
break
}
conn.WriteMessage(msgType, msg)
}
}
核心步骤:
-
HTTP 请求进入 Handler
-
Upgrade()完成协议升级 -
得到一个 TCP Socket 长连接
WebSocket 客户端
Go
conn, _, err := websocket.DefaultDialer.Dial(
"ws://localhost:8080/ws", nil,
)
if err != nil {
panic(err)
}
defer conn.Close()
conn.WriteMessage(websocket.TextMessage, []byte("hello"))
_, msg, _ := conn.ReadMessage()
fmt.Println(string(msg))
六、WebSocket 的通信模型
一个连接 = 一个 Socket
一个连接
├── 读协程(Read Loop)
└── 写协程(Write Loop)
-
避免写阻塞读
-
避免并发写 panic
七、WebSocket 心跳机制
为什么需要心跳?
-
NAT / 代理会悄悄断连接
-
TCP 断了你不一定立刻知道
常见方式
-
ping / pong -
定时消息
Go
// 设置 Pong 帧的处理函数:当收到对方(服务端/客户端)的 Pong 帧时,执行该回调
conn.SetPongHandler(func(string) error {
// 重置连接的「读超时时间」为当前时间 + 60秒
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil // 无错误返回,标识 Pong 帧处理成功
})
八、完整通信模型总结图(文字版)
HTTP 建立连接 ↓ Upgrade → WebSocket ↓ TCP 长连接保持 ↓ 消息驱动 + 全双工通信 ↓ 心跳维持连接 ↓ 任意一方关闭连接
WebSocket 基于 TCP 建立长连接,通信模型是全双工、消息驱动的,连接一旦建立,客户端和服务端都可以随时主动发送消息,适合实时、高频、双向通信场景。