在传统的 Web应用 开发中,通信主要依赖于 HTTP 协议。这种通信模式通常是这样的:客户端主动向服务器发送一个 HTTP 请求,服务器处理完这个请求后,再向客户端回复一个 HTTP 响应。换句话说,HTTP 协议是一个典型的“请求-响应”模型,只能由客户端发起,服务器被动响应。

然而,在很多实际的应用场景里,我们需要服务器能够主动向客户端推送信息。这时候,单纯的 HTTP 协议就显得有些力不从心了。举一个最常见的例子:实现一个 Web 聊天室。当有新的聊天消息到达时,服务器必须能够立刻通知在线的客户端。如果只用 HTTP,客户端就只能通过不断向服务器“轮询”(即定时发送请求)来询问是否有新消息。
轮询这种方式存在一个致命缺陷:性能和实时性难以兼得。如果轮询频率设置得太高,服务器将承受巨大的、不必要的请求压力;如果为了节省资源而降低轮询频率,消息通知的实时性又会大打折扣,用户体验变差。
显然,服务器主动推送数据是一个强需求,最好能在网络协议层面得到原生支持。为此,WebSocket 协议应运而生。
WebSocket,顾名思义,它为 Web 应用引入了 套接字(Socket) 般的通信能力。它是一个建立在 TCP 协议之上的应用层协议,为通信的双方提供了一个 全双工 的信道,允许服务器和客户端在任何时刻互相发送数据。
为了最大程度地兼容现有的 Web 基础设施(比如广泛部署的 HTTP代理 和防火墙),WebSocket 的设计非常巧妙。它复用了 HTTP 的 80 和 443 端口,并且连接建立阶段完全使用 HTTP 协议(通过一个特殊的 Upgrade 请求头)。这意味着,现有的 Nginx 等中间件可以无缝支持 WebSocket 协议,无需进行大规模的网络架构调整。如果你对网络协议栈的底层原理感兴趣,可以在 云栈社区 的网络技术板块找到更多深入的讨论。
如何表示 WebSocket 服务器地址?
和 HTTP 协议一样,WebSocket 服务器的地址也使用 URL 来表示,只是协议部分换成了 ws(非加密)或 wss(加密,类似 HTTPS)。
# ws 代表普通的 WebSocket 协议,默认使用 80 端口
ws://api.example.com/chat
# wss 代表安全的 WebSocket 协议,默认使用 443 端口
wss://api.example.com/chat
连接是如何建立的?
WebSocket 连接的建立过程可以称为“握手”,它始于一个标准的 HTTP 请求。
-
客户端发起升级请求:客户端首先通过 TCP 连接到服务器。然后,它通过这个 TCP 连接发送一个特殊的 HTTP GET 请求。这个请求的关键在于其头部,必须包含 Connection: Upgrade 和 Upgrade: websocket,以此告知服务器:“我想把当前的 HTTP 连接升级为 WebSocket 连接”。
GET /chat HTTP/1.1
Host: localhost:8080
User-Agent: Go-http-client/1.1
Connection: Upgrade
Sec-WebSocket-Key: bLPIf/xpAnrtCKuifPKTUg==
Sec-WebSocket-Version: 13
Upgrade: websocket
-
服务器同意升级:服务器接收到这个请求后,会检查 Upgrade 头部。如果服务器支持 WebSocket,它将回复一个状态码为 101 Switching Protocols 的 HTTP 响应,表示同意切换协议。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: lShvB7NL9TbGxezz+KUd5ee6jhA=

一旦这次 HTTP 请求-响应交互完成,客户端和服务器之间的 TCP 连接就被“升级”了。此后,双方将不再使用 HTTP 协议,而是完全使用 WebSocket 协议帧来进行双向通信。
数据是如何传输的?—— 理解数据帧
连接建立后,通信的所有数据都被组织成“帧(Frame)”来传输。一条完整的应用层消息(比如一段聊天文本)可能会被拆分成一个或多个连续的帧。
WebSocket 数据帧的结构比较复杂,但其核心目的是高效、可靠地承载数据和元信息。

一个数据帧主要包含以下几个部分:
- 标志位(4位):
FIN (1位): 如果为1,表示这是当前消息的最后一帧。
RSV1, RSV2, RSV3 (各1位): 保留位,用于未来的协议扩展,通常为0。
- 操作码(4位): 定义了帧的类型,这是理解帧功能的关键。
%x0 (0): 连续帧。表示这是一个分片消息的中间帧。
%x1 (1): 文本帧。表示帧内承载的是 UTF-8 编码的文本数据。
%x2 (2): 二进制帧。表示帧内承载的是任意的二进制数据。
%x3-7: 保留,用于未来的非控制帧。
%x8 (8): 连接关闭帧。用于优雅地关闭连接。
%x9 (9): Ping 帧。用于连接保活探测。
%xA (10): Pong 帧。用于响应 Ping 帧。
%xB-F: 保留,用于未来的控制帧。
- 掩码位(1位): 指示“有效负载数据”是否被掩码处理。根据协议规范,所有从客户端发往服务器的帧必须置1(掩码),而服务器发往客户端的帧则通常为0。这主要是出于安全考虑,防止恶意代码或缓存污染。
- 有效负载长度(7位): 这是一个变长字段,用于表示“有效负载数据”的字节数。
- 如果值在 0-125 之间,它直接表示长度。
- 如果值为 126,则实际的长度由后面2个字节(16位) 的扩展字段表示。
- 如果值为 127,则实际的长度由后面8个字节(64位) 的扩展字段表示。
- 扩展负载长度(0, 2 或 8字节): 如上所述,当基本长度字段为126或127时,用于存储实际长度。
- 掩码密钥(4字节): 仅当掩码位为1时存在。客户端会生成一个随机的32位密钥,用于对有效负载数据进行异或掩码计算。
- 有效负载数据: 帧实际承载的应用数据。
为了更直观地理解“有效负载长度”字段的三种情况,可以参考下图:

注:上图为简化示意图,假设掩码位 MASK=0。
简单来说,WebSocket 帧结构可以抽象为头部和数据两部分。头部承载元信息,最重要的是操作码和数据长度。
根据操作码,帧可以分为两大类:
- 控制帧 (
opcode 8, 9, 10): 用于管理连接本身。
close: 协商关闭连接。
ping / pong: 用于连接保活和心跳检测。一方发送 ping,另一方必须回复 pong。这能确保在网络空闲时连接依然活跃,并能及时探测到连接是否意外断开。
- 非控制/数据帧 (
opcode 0, 1, 2): 用于承载实际的业务数据。
text: 传输文本数据。
binary: 传输二进制数据(如图片、文件、Protobuf消息等)。
核心要点总结
- 兼容性设计:WebSocket 完美兼容 HTTP 生态,利用 HTTP 协议来完成初始握手和协议升级。
- 两阶段通信:
- 握手阶段:使用 HTTP 协议(带
Upgrade 头)建立连接。
- 通信阶段:切换为纯粹的 WebSocket 协议,进行全双工数据传输。
- 帧式传输:所有数据都封装在“帧”中传输,一个消息可由一帧或多帧组成。
- 帧结构:帧由头部和有效负载数据构成。头部包含决定帧类型的操作码和表示数据大小的长度字段。
- 帧分类:按操作码可分为控制帧(管理连接)和数据帧(承载业务数据)。
- 控制帧类型:
ping/pong: 保活与心跳检测。
close: 优雅关闭连接。
- 数据帧类型:
text: 用于文本协议(如 JSON)。
binary: 用于二进制协议,效率更高。
通过 WebSocket,开发者可以轻松构建需要实时、双向通信的 Web 应用,如在线聊天、实时通知、协同编辑、股票行情、在线游戏等,彻底摆脱了 HTTP 轮询带来的性能与实时性困境。
|