1. 背景
在互联网发展的早期阶段,HTTP 协议作为主要的通信协议,广泛应用于客户端与服务器之间的数据交换。然而,随着应用需求的不断增加,传统 HTTP 请求逐渐暴露出其在实时通信和高频交互场景中的不足。
传统的 HTTP 协议是基于请求-响应模型的。客户端发起请求,服务器处理后返回响应,无法实现服务器主动向客户端发送数据。这意味着如果服务器需要向客户端发送数据,客户端必须定期发起请求,这种方式称为轮询。轮询虽然能实现一定程度的数据更新,但由于其固有的延迟和资源消耗,常常不能满足对实时性的高要求。
例如,在在线聊天应用中,用户希望能够即时接收到新消息。如果采用轮询,客户端必须频繁地发送 HTTP 请求来检查是否有新消息。轮询的频率决定了数据更新的及时性,如果轮询间隔较长,用户可能需要等待一段时间才能接收到新数据,这就是轮询固有的延迟。此外,在轮询过程中,许多请求可能会返回“无新数据”的响应,这会造成不必要的数据传输和资源浪费;在高并发场景下,多个客户端频繁发起请求,还会对服务器造成显著压力。显然,轮询在需要即时响应的应用中是不理想的。再举一个例子,在金融交易平台中,用户希望能够实时获取股票价格的变化,如果使用轮询,用户可能会因为请求延迟而错过重要的市场动态,这进一步降低了交易的效率和用户体验。因此,轮询的局限性在这些需要快速响应的场景中尤为明显。
为了应对这些挑战,WebSocket 应运而生。
2. 什么是 WebSocket?
与 HTTP 类似,WebSocket 也是一种建立在 TCP 协议上的应用层网络通信协议,依赖于底层的 TCP 协议来进行数据传输,但它们的功能和使用场景有着显著的区别。
WebSocket 通过单个传输控制协议(TCP) 连接提供同步双向通信通道。 它具有 3 大特点:
- 持久链接:一旦建立连接,客户端和服务器之间的连接将保持开放状态,直到一方决定结束连接为止;
- 双向通信:客户端和服务器之间可以同时相互传输数据,无需等待对方完成;
- 较少的开销:连接建立后使用更少的头部信息进行数据传输,提高了整体传输效率。
这些特点使得 WebSocket 满足了现代应用对实时性的需求,成为需要即时响应或更新的应用程序的理想选择。
该协议目前是 Web 应用程序在客户端和服务器之间进行实时双向通信的技术标准,已被 IETF 标准化为 RFC 6455,并作为 HTML5 的关键技术得到广泛支持。
3. WebSocket 的工作原理
WebSocket 依赖于 HTTP。为什么这样说呢?
前面有提到,WebSocket 和 HTTP 都依赖于底层的 TCP 协议来进行数据传输。TCP 提供可靠的数据传输,是一个面向连接的协议,只有连接成功建立后,数据才能开始传输。在 HTTP 协议中,会通过三次握手来建立 TCP 连接。既然 HTTP 已经做好了这部分工作,为什么还要花力气重复造轮子呢?
因此,WebSocket 的设计者并没有自己重新设计一个握手机制,而是巧妙地选择直接利用 HTTP 来完成这个过程。具体而言,客户端首先会通过 HTTP 向服务器发起协议升级请求,服务器同意后通过 HTTP 响应确认协议升级为 WebSocket。一旦升级为 WebSocket,后续的数据传输就不再依赖 HTTP 协议,而是通过 WebSocket 协议进行全双工、持久的通信。
这种方式的好处是显而易见的:HTTP 协议通过 80 和 443 端口(标准的 Web 服务端口)已经被广泛开放,这意味着 WebSocket 可以通过这两个端口避开很多防火墙的限制。这使得 WebSocket 能够更容易地穿透网络障碍,成功建立连接。
4. WebSocket 的通信过程
4.1. 建立连接
WebSocket 通信过程的第一步就是建立连接。这一过程可以简单理解为客户端与服务器之间的握手问候:客户端向服务器询问:“你支持 WebSocket 吗?”如果服务器回应“Yes,我支持”,那么通信便会以 WebSocket 协议继续进行。
4.1.1 客户端:申请协议升级
连接的建立始于一个 HTTP 请求。具体指的是,客户端通过发送一个特殊的 HTTP GET 请求与服务器协商升级到 WebSocket 协议。可以看到,该请求采用的是标准的 HTTP 报文格式:
GET /chat HTTP/1.1
Host: server.example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
重点请求头部字段意义如下:
Connection: Upgrade:表示要升级协议。
Upgrade: websocket:表示要升级到 websocket 协议。
Sec-WebSocket-Key:与后面服务器响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护。
Sec-WebSocket-Version: 13:表示 websocket 的版本,如果服务端不支持该版本,需要返回一个相同的header,里面包含服务端支持的版本号。
注意,上面请求省略了部分非重点请求头部字段。由于是标准的 HTTP 请求,类似 Origin、Cookie 等请求首部会照常发送。在握手阶段,可以通过相关请求头进行安全限制、权限校验等。
4.1.2 服务端:响应协议升级
如果服务器支持 WebSocket 连接,它会以 HTTP 状态代码 101 进行响应,表示服务器接受此请求,同时也会发送服务端的 Sec-WebSocket-Accept 头信息加密结果,如下所示:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
4.1.3 客户端:协议升级确认
客户端收到服务器的响应后,会进行协议升级确认。具体来说,客户端会对服务器返回的 Sec-WebSocket-Accept 值进行解密,以确保服务器的响应与其发送的请求相匹配。如果加密结果正确,客户端就认为协议切换成功。从此时起,初始 HTTP 连接升级为 WebSocket 连接,该连接在相同的底层 TCP/IP 连接上运行,客户端和服务器可以同时进行数据传输,并且连接始终保持打开状态,直到一方主动将其关闭。
验证过程:
客户端会拿到服务器返回的 Sec-WebSocket-Accept 字段,该字段的值是对客户端发送的 Sec-WebSocket-Key 和一个固定字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)进行 SHA-1 哈希运算后,再进行 Base64 编码得到的。
客户端将自己发送的 Sec-WebSocket-Key 与固定字符串拼接,进行 SHA-1 哈希运算,然后再 Base64 编码。如果计算结果与服务器响应中的 Sec-WebSocket-Accept 相符,客户端就确认协议升级成功。
4.2 数据传输
协议升级完成后,所有后续的数据交换都遵照 WebSocket 的协议。在 WebSocket 协议中,客户端和服务端传输的数据单位被称为消息(messages)。
4.2.1 一条消息的组成
WebSocket 协议中最小的数据传输单元是帧,这些帧包含 WebSocket 通信所需的控制和数据信息。一条消息可以由一个或多个帧组成。在传输大的数据时,WebSocket 协议允许消息被分割成多个帧进行传输,直到所有帧传输完毕,形成完整的消息。
4.2.2 数据帧结构
WebSocket 数据帧结构如下所示(第一行 0、1、2、3 表示 4 个字节,第二行表示比特):
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| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
该结构的字段语义如下:
FIN:
如果消息跨多个帧,每个帧的 FIN 标志位都会表明是否是最后一帧。若 FIN = 1,表示这是消息的最后一帧;若 FIN = 0,则说明后面还有更多帧。
RSV1, RSV2, RSV3:
一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非 0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接会出错。
Opcode:
操作码,用于描述帧的类型。WebSocket 协议定义了多种帧类型,包括文本帧、二进制帧和控制帧,每种帧都有特定的用途。文本和二进制帧在客户端和服务器之间传输应用程序数据,控制帧则用于管理连接,包括 ping、pong 和 close 帧等类型。每个帧的类型由操作码决定,常见的操作码有:
0x1:文本帧(用于传输 UTF-8 编码的文本数据)
0x2:二进制帧(用于传输二进制数据)
0x3 ~ 7:目前保留, 以后将用作更多的非控制类 frame
0x8:close 帧(用于关闭连接)
0x9:Ping 帧(用于检测连接是否仍然有效)
0xA:Pong 帧(Ping 的回应,用于确认连接仍然活跃)
0xB ~ F:目前保留, 以后将用作更多的控制类 frame
Mask:
掩码位,表示是否对客户端发送的数据进行掩码操作,用于安全传输数据。
Masking key:
掩码键,如果 Mask 为 1,那么在 Masking-key 中会定义一个 Masking key 掩码键,并用这个掩码键来对数据进行处理,以防止恶意代码注入,服务端接收到数据后会对其进行反掩码处理。如果 Mask 为 1,但是服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
Payload length:数据负载的长度。
Payload data:负载数据,是真正需要传输的消息数据。
4.3 关闭连接
要关闭 WebSocket 连接,客户端或服务器只需发送关闭连接的请求即可。关闭连接后,将无法再发送或接收任何消息。
需要注意的是,除了主动关闭以外,websocket 连接也有可能被意外关闭,比如当出现网络连接错误或中断的时候。在这种情况下,通信双方将无法再发送或接收消息,为了确保 websocket 连接意外中断时能够快速恢复通信,WebSocket 需要一些额外的机制来保障连接的稳定性和可靠性,即心跳检测和断线重连。本篇重点讨论的是 WebSocket 的通信过程及原理,后续我们会单独对该机制进行深入探讨。