一、WebSocket 原理
WebSocket是HTML5提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。
WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。
WebSocket 特点的如下:
● 支持双向通信,实时性更强
● 可以发送文本,也可以发送二进制数据
● 建立在TCP协议之上,服务端的实现比较容易
● 数据格式比较轻量,性能开销小,通信高效
● 没有同源限制,客户端可以与任意服务器通信
● 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL
● 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
二、WebSocket 握手
WebSocket 服务端使用标准 TCP 套接字监听进入的连接。下文假定服务端监听 http://example.com 的 8000 端口,响应 http://example.com/chat 上的 GET 请求。
握手是 WebSocket 中 “Web”。它是从 HTTP 到 WebSocket 的桥梁。在握手过程中,协商连接的细节,并且如果行为不合法,那么任何一方都可以在完成前退出。服务端必须仔细理解客户端的所有要求,否则可能出现安全问题。
2.1 客户端握手请求
客户端通过联系服务端,请求 WebSocket 连接的方式,发起 WebSocket 握手流程。客户端发送带有如下请求头的标准 HTTP 请求(HTTP 版本必须是 1.1 或更高,并且请求方法必须是 GET):
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
在这里,客户端可以请求扩展和/或子协议。此外,也可以使用常见的请求头,比如 User-Agent、Referer、Cookie 或者身份验证请求头。这些请求头与 WebSocket 没有直接关联。
如果存在不合法的请求头,那么服务端应该发送 400 响应(“Bad Request”),并且立即关闭套接字。通常情况下,服务端可以在 HTTP 响应体中提供握手失败的原因 。如果服务端不支持该版本的 WebSocket,那么它应该发送包含它支持的版本的 Sec-WebSocket-Version 头。在上面的示例中,它指示 WebSocket 协议的版本为 13。
在请求头中,最值得关注的是 Sec-WebSocket-Key。接下来,将讲述它。
2.2 服务端握手响应
当服务端收到握手请求时,将发送一个特殊响应,该响应表明协议将从 HTTP 变更为 WebSocket。
该响应头大致如下(记住,每个响应头行以 \r\n 结尾,在最后一行的后面添加额外的 \r\n,以说明响应头结束):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
此外,服务端可以在这里对扩展/子协议请求做出选择。Sec-WebSocket-Accept响应头很重要,服务端必须通过客户端发送的Sec-WebSocket-Key请求头生成它。具体的方式是,将客户端的Sec-WebSocket-Key与字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"(“魔法字符串”)连接在一起,然后对结果进行 SHA-1 哈希运算,最后返回哈希值的 Base64 编码。
因此,如果 Key 为"dGhlIHNhbXBsZSBub25jZQ==“,那么Sec-WebSocket-Accept响应头的值是"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”。服务端发送这些响应头后,握手完成,可以开始交换数据。
下面的 Python 代码根据Sec-WebSocket-Key请求头生成Sec-WebSocket-Accept响应头的值:
import typing
from hashlib import sha1
import base64
SEC_WS_MAGIC_STRING: bytes = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def get_sec_ws_accept(sec_ws_key: typing.Union[bytes, str]) -> bytes:
if isinstance(sec_ws_key, str):
sec_ws_key = sec_ws_key.encode()
return base64.b64encode(sha1(sec_ws_key + SEC_WS_MAGIC_STRING).digest())
if __name__ == "__main__":
assert get_sec_ws_accept(b"dGhlIHNhbXBsZSBub25jZQ==") == b"s3pPLMBiTxaQ9kYGzzhZRbK+
三、数据帧(Data Framing)
3.1 概览
在 WebSocket 协议中,使用一系列帧传输数据。为避免混淆网络中间人(比如拦截代理),以及出于安全考虑,客户端必须对发送给服务端的所有帧进行掩码(Mask)处理。(注意,无论 WebSocket 协议是否运行在 TLS 上,都需要进行掩码处理。)服务端在收到未进行掩码处理的帧时,必须关闭连接。在这种情况下,服务端可以发送状态码为 1002(协议错误)的关闭帧。服务端不得对发送给客户端的任何帧进行掩码处理。如果客户端检测到掩码帧,那么必须关闭连接。在这种情况下,可以使用状态码 1002(协议错误)。
基础帧协议定义了一种帧类型,包括操作码(Opcode)、有效载荷长度,以及“扩展数据”和“应用数据”的指定位置,它们一起定义“有效载荷数据”。一些位和操作码被保留,以供未来扩展协议。
在握手完成后,端点被发送关闭帧前,客户端和服务端可以随时传输数据帧。
3.2 基础帧协议
帧的格式如下图所示:
FIN:1 比特
表示该帧是消息中的最后一个分片。第一个分片也可能是最后一个分片。
RSV1、RSV2、RSV3:每个 1 比特
除非协商了定义非零值含义的扩展,否则必须为 0。如果收到非零值,并且没有协商的扩展定义该非零值的含义,那么接收端点必须使该 WebSocket 连接失败。
操作码:4 比特
定义对“有效载荷数据”的解释。如果收到未知操作码,那么接收端点必须使该 WebSocket 连接失败。定义的值如下:
%x0 表示延续帧
%x1 表示文本帧
%x2 表示二进制帧
%x3-7 为将来的非控