为什么 WebSocket 握手看起来像 HTTP 请求?它和 HTTP 到底是什么关系?TCP 在其中扮演什么角色?本文一次性讲清楚。
一、先理清三个协议的关系
在讲 WebSocket 之前,必须先搞清楚 TCP、HTTP、WebSocket 三者是什么关系。
层次关系
┌─────────────┐
│ HTTP │ ← 应用层(文本协议)
├─────────────┤
│ WebSocket │ ← 应用层(二进制协议)
├─────────────┤
│ TCP │ ← 传输层
├─────────────┤
│ IP │ ← 网络层
└─────────────┘
一句话总结各自角色
| 协议 | 角色 |
|---|---|
| TCP | 可靠传输管道,保证数据不丢、不重、有序 |
| HTTP | 在 TCP 上定义了"请求-响应"规则,半双工 |
| WebSocket | 在 TCP 上定义了"全双工"规则,支持服务器主动推送 |
核心结论
TCP 是管道,HTTP 和 WebSocket 是两套独立的语言。
WebSocket 只是在打招呼的时候说了一句 HTTP 的话,让门卫放行。进去之后,它说的完全是自己的语言。
二、一个常见的误解
很多人以为 WebSocket 是 HTTP 的"升级版"或"加强版",甚至认为 WebSocket 依赖 HTTP。
真相是 :WebSocket 和 HTTP 没啥关系 ,只是握手时借用了 HTTP 的格式。
三、WebSocket 的完整握手过程
整个建立连接的过程只有三步:
第 1 步:TCP 三次握手
这是传输层的事,WebSocket 和 HTTP 都绕不开:
客户端 ──SYN──▶ 服务端
客户端 ◀─SYN+ACK─ 服务端
客户端 ──ACK──▶ 服务端
结果:TCP 连接建立,可以发数据了。
第 2 步:借 HTTP 格式完成握手
客户端发一个看起来像 HTTP 的请求:
http
GET /chat HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
注意:这只是披着 HTTP 的外衣 ,目的不是获取 /chat 资源,而是告诉服务器:"我想把协议换成 WebSocket"。
服务端同意切换,回复:
http
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
结果:双方达成一致,准备切换协议。
第 3 步:切换协议,用 opcode 进行长连接通信
握手完成后,双方彻底抛弃 HTTP 格式,改用 WebSocket 帧格式通信:
客户端 ── [帧头: opcode=0x1] + "Hello" ──▶ 服务端
服务端 ── [帧头: opcode=0x1] + "World" ──▶ 客户端
(随时双向发,连接保持)
opcode(操作码) 是 WebSocket 帧里的核心字段,决定了这一帧的用途:
| opcode | 含义 | 说明 |
|---|---|---|
| 0x1 | 文本帧 | UTF-8 文本数据 |
| 0x2 | 二进制帧 | 任意二进制数据 |
| 0x8 | 关闭连接 | 主动关闭连接 |
| 0x9 | Ping | 心跳探测 |
| 0xA | Pong | 心跳回复 |
这就是"opcode 沟通"------双方通过 opcode 告诉对方:这帧是文本、二进制,还是心跳、关闭。
四、为什么要"借"HTTP 格式?
直接原因:防火墙、代理、负载均衡器只认识 HTTP。
如果不借 HTTP 外衣
客户端 → 发一个自定义的握手包 → 防火墙看不懂 → 直接丢掉
借了之后
客户端 → 发一个"看起来像 HTTP"的包 → 防火墙一看是 HTTP → 放行
本质:穿着 HTTP 的衣服混过中间设备,进去之后脱掉衣服,换上 WebSocket 自己的衣服。
五、TCP、HTTP、WebSocket 完整对比
| 维度 | TCP | HTTP | WebSocket |
|---|---|---|---|
| 层级 | 传输层 | 应用层 | 应用层 |
| 通信模式 | 全双工 | 请求-响应(半双工) | 全双工 |
| 服务器主动推送 | 可以 | ❌ 不支持 | ✅ 支持 |
| 连接时长 | 可长可短 | 短(默认请求完就关) | 长(一直保持) |
| 消息边界 | 无(流式) | 有(空行+Content-Length) | 有(帧头带长度) |
| 协议形式 | 二进制 | 文本 | 二进制 |
| 浏览器 JS 可用 | ❌ | ✅ | ✅ |
六、为什么浏览器不直接用 TCP?
这是很多人会问的问题:既然 WebSocket 底层也是 TCP,为什么不直接给 JS 暴露 TCP API?
答案:安全。
如果 JS 能直接操作 TCP:
- 恶意网站可以扫描你内网端口
- 可以绕过同源策略攻击你的路由器
- 可以伪造请求攻击内网服务
所以浏览器只开放了 HTTP 和 WebSocket 这两个"受控"的 API,不开放原始 TCP。
后端程序就不一样,Go/Java/Python 可以直接用 TCP:
go
// 后端可以直接用 TCP
conn, _ := net.Dial("tcp", "127.0.0.1:8080")
conn.Write([]byte("自定义协议"))
七、WebSocket 的数据帧结构(了解即可)
WebSocket 帧的最小结构(2字节头 + 可变长负载):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
- opcode:4 位,表示帧类型(文本/二进制/关闭/Ping/Pong)
- MASK:1 位,表示是否掩码(客户端发服务器必须掩码)
- Payload len:7 位,负载长度
对比 HTTP 头部:HTTP 每次请求几百字节头部,WebSocket 帧头只有 2-14 字节,这就是 WebSocket 更高效的原因之一。
八、常见问题 Q&A
Q1:WebSocket 和 HTTP/2 的 Server Push 有什么区别?
| HTTP/2 Server Push | WebSocket | |
|---|---|---|
| 推送内容 | 只能推送资源(如 CSS、JS、图片) | 可以推送任意数据(文本、二进制、JSON) |
| 推送时机 | 响应请求时顺便推 | 任何时候都可以推 |
| 双向通信 | 不支持(还是请求-响应) | 支持全双工 |
HTTP/2 的 Server Push 是"你问 A,我顺便给你 B、C、D"。
WebSocket 是"随时可以互相发任何东西"。
Q2:WebSocket 能替代 HTTP 吗?
不能。 两者定位不同:
- HTTP:适合 REST API、静态资源、文件上传下载
- WebSocket:适合实时通信(聊天、游戏、行情)
各司其职。
Q3:WebSocket 的 Ping/Pong 是什么?
WebSocket 协议内置的心跳机制:
- Ping:一方发送 Ping 帧
- Pong:另一方回复 Pong 帧
作用:检测连接是否存活,保持 NAT 映射不过期。
对比 TCP 的 KeepAlive:TCP 的 KeepAlive 是操作系统层面的,默认 2 小时才发一次;WebSocket 的 Ping/Pong 是应用层可控的,可以按需设置频率。
Q4:为什么 WebSocket 握手必须用 101 状态码?
101 是 HTTP 协议专门预留的 Switching Protocols 状态码,表示"服务器同意切换协议"。这是 HTTP/1.1 标准的一部分,不是 WebSocket 独有。
Q5:WebSocket 和 Socket 是什么关系?
| Socket | WebSocket | |
|---|---|---|
| 定义 | 网络编程接口(API) | 应用层协议 |
| 层级 | 传输层接口 | 应用层协议 |
| 关系 | WebSocket 底层使用 Socket 编程实现 |
简单说:Socket 是编程接口,WebSocket 是协议规范。
九、一个完整的 WebSocket 示例(Go + HTML)
服务端(Go)
go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func handler(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
defer conn.Close()
for {
// 读取消息
_, msg, _ := conn.ReadMessage()
fmt.Printf("收到: %s\n", msg)
// 回复消息
conn.WriteMessage(websocket.TextMessage, []byte("收到: " + string(msg)))
}
}
func main() {
http.HandleFunc("/ws", handler)
http.ListenAndServe(":8080", nil)
}
客户端(HTML + JS)
html
<script>
const ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = () => {
ws.send("Hello WebSocket!");
};
ws.onmessage = (event) => {
console.log("收到:", event.data);
};
</script>
十、总结
你的理解完全正确
- 走完 TCP 三次握手
- 建立 HTTP 请求(借 HTTP 形式)
- 进行长连接 opcode 沟通
一句话总结
WebSocket 不是 HTTP 的升级版,它只是借 HTTP 的形式完成握手,然后用自己的协议(opcode + 帧格式)在长连接上进行双向通信。
最终记忆口诀
- 握手时:穿 HTTP 衣服
- 握手后:脱掉 HTTP 衣服,换上 WebSocket 衣服
- 通信时:用 opcode 告诉对方发的是什么
思考题
如果防火墙和代理都原生支持 WebSocket 协议,你觉得 WebSocket 还需要借 HTTP 的格式握手吗?
(答案:不需要,直接自定义握手就行。但现实是------它们只认 HTTP。)