前言
背景
一直以来,http
实现客户端和服务器的双向通信的轮询技术有几个缺点:
- 对每个客户端需要维持多个连接:一个用于接收消息,一个用于推送消息。
- 每个客户端到服务器的消息都需要
Htpp
头。 - 客户端脚本需要维护输出连接到输入连接的配对信息,以便对服务器的返回作出正确的处理。
为了兼容、利用现有的大量存在的 http
代码,websocket
是在 http
的基础上进行设计的。同时,也考虑到了未来完全脱离 http
的可能性。比如,使用更简单的握手协议。
协议概述
协议的两个主要部分:握手和数据传送。
-
握手
从客户端到服务器的发送的内容示例(请求):
GET /chat HTTP/1.1 Host: server.example.com Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat,superchat Sec-WebSocket-Version: 13
(1)
Get
方法的Request-URI
: 通过指定不同的Request-URI
, 可以实现由一个IP
地址服务多个websocket
端点。(2) Host: 向服务器发起请求的主机地址。
(3) Sec-WebSocket-Protocol: 指定可用的通信协议(应用层)。
(4) Sec-WebSocket-Version: 协议的版本。
(5) Origin: 服务器用来判断客户端的请求是否为跨域的未进行授权的。
(6) Sec-WebSocket-Key: 服务器用于计算
Sec-WebSocket-Accept
的值。HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
(1) 状态码 : 如果为101,表示握手完成;否则,表示握手完成,依然使用
http
协议。(2) Connecction 和 Upgrade: 表示
http
完成升级。(3) Sec-WebSocket-Accept: 用来证明服务器已经接受了客户端的握手请求。这样可以避免通过
XMLHttpRequest
对服务器进行的一些攻击。计算步骤:- 过滤
Sec-WebSocket-Key
的-
和空格字符。 - 将过滤的
Sec-WebSocket-Key
和 GUID 连接。 - 将连接得到的字符串进行
SHA-1(160bit)
、base64-encode
编码。
(4) Sec-WebSocket-Protocol: 在客户端支持的协议中,服务器最终选择使用的协议。
请求行和状态行格式语法要求、解析方法参考资料 RFC2616。
发送的内容的含义见 握手。
握手完成之后,进入数据发送的阶段。
- 过滤
-
数据发送
下面的提到的
message
, 在本文中代指从客户端到服务器或者服务器到客户端发送的完整的单次消息内容。message
可以由一帧或者许多帧组成,属于同一个message
的帧具有相同的帧类型。目前的帧类别有:- 文本帧
- 二进制帧
- 控制帧
- 保留帧
websocket URIs
ws
的默认端口是80,wss
的默认端口是443。
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
host = <host, defined in RFC3986,Section 3.2.2>
port = <port, defined in RFC3986, Section 3.2.3>
path = <path-abempty, defined in RFC3986,Section 3.3>
query = <query, defined in RFC3986, Section 3.4>
resource-name
包括四个部分:
(1) 如果 path
为空,“/” 为第一个部分。
(2) path
。
(3) 如果 query
为空,“?” 为第三个部分。
(4) query
最后一点,websocket-uri
没有锚部分。
握手
客户端
(1) 一个客户端不能同时有两个处于 CONNECTING
状态的连接到一个 websocket
端点(相同的 ip
和 port
)。这有助于防止分布式拒绝服务攻击(意即客户端同时请求大量恶意的连接,导致服务器瘫痪)。
(2) 对于客户端和服务器已经处于连接完成状态的 websocket connection
,没有数量的限制。服务器可以根据实际的情况,对恶意 IP
进行屏蔽。
(3) 代理。客户端通过连接到代理并要求代理打开一个 TCP
连接。
(4) 如果要求加密连接,在打开连接之后、opening handshake
之前,需要一个 TLS handshake
。
- 客户端的
opening handshake
(1) 首先需要是一个合法的 http
请求。见 RFC2616。
(2) 必须为 GET
方法,并且 Http
的版本必须在 1.1 以上。
```
GET /chat HTTP/1.1
```
(3) Request-Uri
的必须是一个合法的 resource-name
。
(4) 当不是使用默认端口的时候,Host
必须同时指定端口。
(5) 必须包含 Upgrade
字段,Upgrade
的值必须包含 websocket
关键词。
(6) 必须包含 Connection
字段,Connection
的值必须包含 Upgrade
关键词。
(7) 必须包含 Sec-WebSocket-Key
字段。Sec-WebSocket-Key
的值:经过 base64-encode
编码的 16 个字节的随机值。
(8) 如果请求来自浏览器客户端,必须包含 Origin
字段。如果 ww2.example.com
运行了一段来自 www.example.com
的代码,这段代码试图与 ww3.example.com
建立一个 websocket
连接,那么 origin
的值为 http://www.example.com
。详见 RFC6454。
(9) 必须包含 Sec-WebSocket-Version
字段,且值为 13。
(10) 必须包含 Sec-WebSocket-Protocol
字段。客户端提供的、瑜服务器进行交互的协议方式。
(11) 可能包含 Sec-WebSocket-Extensions
字段。
(12) 其他的可能的字段见 RFC2616。
服务器
(1) 如果为 HTTPS
连接,需要一个 TLS
握手连接。以后,所有的信息交互都应该是加密的,包括握手。
(2) 服务器可以要求客户端进行授权确认。返回 401 状态码,并在 WWW-Authenticate
的字段,设置相应值。
(3) 服务器根据 origin
字段判断是否允许客户端建立连接。
(4) 如果客户端发送的 Sec-WebSocket-Version
版本太低,服务器发送 426 或者 400 状态码等,并且在返回的 Sec-WebSocket-Version
指定最低的版本值。
(5) 如果客户端请求的、由 resource-name
指定的服务器资源不存在,服务器返回 404 状态码。
(6) 如果服务器不支持客户端发送的 Sec-WebSocket-Protocol
的字段的值,指定的任何一种协议,不能在回复的 Sec-WebSocket-Protocol
字段中,设置空值。应该设置服务器支持的协议值。
如果服务器接受了客户端的连接请求,需要:
(1) 返回 101 状态码。
(2) 字段 Upgrade
且值为 websocket
。
(3) 字段 Connecction
且值为 Upgrade
。
(4) 字段 Sec-WebSocket-Accept
以及处理之后的钥匙字符串。
client_sec_websocket_key = "dGhlIHNhbXBsZSBub25jZQ=="
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
concat_string = "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-
C5AB0DC85B11"
final_string = base64_encode(sha1(concat_string,20)) = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
(5) 字段 Sec-WebSocket-Protocol
及其值。
(6) 字段 Sec-WebSocket-Extensions
及其值。
协议字段的发送都是不区分大小写的。完成握手之后,连接进入了 open
状态。
数据帧
- 概述
(1) 出于安全以及其他的因素考虑,客户端发送到服务器的所有帧都需要进行掩码的处理,服务器发送到客户端的帧不能进行掩码的处理。如果服务器接收到客户端的帧,未进行掩码的处理,那么,需要发送 1002 状态码,并断开连接。
(2) 数据帧的发送时段:握手完成,连接处于 open
状态以及连接关闭帧发送之前。
-
帧结构
frame bitmap 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
:1bit
表示
message
中的最后一帧;message
可以独立成帧。 -
RSV1
、RSV2
、RSV3
:1bit
没有扩展协议的情况之下,值必须为零;否则,接收端必须关闭连接。
-
Opcode
:4bit
定义对
Payload data
的内容怎么进行解释。%x0 : 表示一个连续帧。
%x1 : 表示一个文本帧。
%x2 : 表示一个二进制帧。
%x3-7 : 为未来保留的非控制帧。
%x8 : 表示连接关闭。
%x9 : 表示
ping
帧。%xA : 表示
pong
帧。%xB-F : 为未来保留的控制帧。
-
Mask
: 1bit
表明
payload data
是否经过掩码处理。如果mask = 1
, 根据masking-key
对payload data
进行反掩码的处理。所有从客户端发送到服务器的帧都需要进行掩码处理。 -
Payload length
: 7bits
, 7 + 16bits
, or 7 + 64bits
ws-frame = frame-fin ; 1 bit in length frame-rsv1 ; 1 bit in length frame-rsv2 ; 1 bit in length frame-rsv3 ; 1 bit in length frame-opcode ; 4 bits in length frame-masked ; 1 bit in length frame-payload-length ; either 7, 7 + 16, or 7 + 64 bits in length [ frame-masking-key ] ; 32 bits in length frame-payload-data ; n * 8 bits in length where n >= 0 frame-fin = %x0 ; more frames of this messege follow / %x1 ; final frame of this message ; 1 bit in length frame-rsv1 = %x0 / %x1 ; 1 bit in length ,must be 0 unless negotiated otherwise frame-rsv2 = %x0 / %x1 ; 1 bit in length ,must be 0 unless negotiated otherwise frame-rsv3 = %x0 / %x1 ; 1 bit in length ,must be 0 unless negotiated otherwise frame-opcode = frame-opcode-non-control / frame-opcode-control / frame-opcode-cont frame-opcode-cont = %x0 ; frame-continuation frame-opcode-non-control = %x1 ; text frame / %x2 ; binary control / %x3-7 ; 4 bits in length ; reserved for futher non-control frames frame-opcode-control = %x8 ; connection close / %x9 ; ping / %xA ; pong / %xB-F ; reserved for futher control ; frames ; 4 bits in length frame-masked = %x0 ; frame is not masked, no frame-masking0-key / %x1 ; frame is masked, frame-masking-key present ; 1 bit in length frame-payload-length = (%x00-7D) / (%x7E frame-payload-length-16) / (%x7F frame-payload-length-63) ; 7, 7+16, 7+64 bits in length, ; respectively frame-payload-length-16 = %x0000-FFFF ; 16 bits in length fraem-payload-length-63 = %x0000000000000000 - 7FFFFFFFFFFFFFFF ; 64 bits in length frame-masking-key = 4 (%x00-FF) ; present only if frame-masked is 1 ; 32 bits in length frame-payload-data = (frame-masked-extension-data、frame-masked-application-data) ; when frame-masked is 1 / (frame-umasked-extension-data、frame-umask-application-data) ; when frame-masked is 0 frame-masked-extension-data = * (%x00-FF) ; reserved for future extensibility ; n*8 bits in length, where n >= 0 frame-masked-application-data = * (%x00-FF) ; n*8 bits in length, where n >= 0 frame-unmasked-extension-data = * (%x00-FF) ; reserved for future extensibility ; n*8 bits in length, where n >= 0 fraem-masked-application-data = * (%x00-FF) ; n*8 bits in length, where n >= 0
-
客户端到服务器的掩码算法
对数据进行掩码算法是为了防止代理缓存污染攻击。
当
frame-masked=1
时,用frame-masking-key
对payload data
进行掩码运算。其中payload data
包括了extension data
和application data
两个部分。由客户端指定一个随机的32位的数据作为
frame-masking-key
的值。frame-masking-key
的值越不容易猜到越好。假设:
original-octel-i
: 原始数据的第i
个字节。transformed-octel-i
: 为转换后数据的第i
个字节。j
: 为i
mod
4 的值。masking-key-octet-j
: 为masking-key
的第j
个字节。j = i mod 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
-
帧
将
message
分成帧,有两个目的。其中,主要的目的是为了在不知道message
长度的情况之下,将message
分成一序列的帧,这样就不用在FIN
位发送之前,缓冲整个message
的信息了。第二个目的,是为了多路复用,避免大信息的message
阻塞了信道。(1) 如果一条
message
包含在单独的一帧里面,那么该帧的FIN
位被置位,并且opcode
的值非0。(2) 如果
message
被分成多帧发送,那么这些帧的特点有:首帧:`FIN` 复位、`opcode` 非 0。 中间帧: `FIN` 复位、`opcode` 为 0。 最后一帧:`FIN` 置位、`opcode` 为 0。
(3) 控制帧不能分帧发送。
(4)
message
帧以怎样的顺序被发送出去,接收方就应该以怎样的顺序接收。(5) 除非有扩展协议,否则单条
message
的多个发送帧被接收的时候,不应该插入其他message
的帧。(6) 通信端点应该具备从
message
的多个帧中,鉴别并处理控制帧的能力。(7) 发送方可能将
message
分成任意数量的帧。(8) 客户端和服务端都应该能够处理分帧的
message
或者未分帧的message
。(9) 因为控制帧不能分帧,所中间代理不能改变控制帧。
(10) 当帧内的保留位被使用,如果中间代理不知道帧内保留位的含义,不能改变帧的内容。
(11) 当协议的扩展被启用的时候,如果代理不知道协议的含义,不应改变帧的内容。
(12)
message
的所有分帧的类型都应该是一样的:要么text
、binary
等。在
message
过大的时候,控制帧,比如ping
帧可能会被延迟。为了解决这种情况,需要能够优先处理控制帧的能力。 -
控制帧
控制帧
opcode
的第一位为 1 。当前opcode
使用到的值包括 0x8 (close)、0x9 (Ping)、0xA (Pong) . 其中 0xB - 0xF 为保留值。控制帧的
payload length
的长度不能大于 125。(1) 关闭帧
关闭帧的
Application data
= 2 个字节的status code
+ 自定义的错误信息其中
status code
包括但不限于:1000 : 正常关闭连接。
1001 : 表示服务器或者客户端主动断开了连接。
1002 : 表示对端发送的协议错误导致了连接的关闭。
1003 : 表示对端收到了不符合期望的数据类型导致连接的关闭。比如,期望
text
类型的帧类型,却收到了binary
的帧类型。1004 : 保留值。
1005 : 保留值。该值不能用于关闭帧的状态码。当应用没有收到期望的状态码的时候,向对端发送此状态码。
1006 : 保留值。该值不能用于关闭帧的状态码。当非正常关闭连接的时候使用。
1007 : 端收到的
message
的多个分帧时,出现了帧类型不一致的情况。1008 : 收到了违反协议规则的
message
时使用。如果 1003 或者 1009 等状态码不适合使用的时候、希望不透露具体违反的规则的细节的时候,可以使用 1008。1009 : 收到的
message
内容过多,导致无法处理。1010 : 客户端使用。客户端希望使用
extension
, 但是服务器没有作出回应。extension
的列表需要包含在关闭帧的reason
部分。1011 : 服务器使用。服务器遇到预料之外的情况,导致不能完成请求。
1015 : 保留值。该值不能用于关闭帧的状态码。
TLS handshake
失败的时候使用。其他的范围的状态码:0-999、1000-2999、3000-3999、4000-4999 均为保留码。
当某个通信端收到了关闭帧时,如果自己之前没有发送个关闭帧,那么应该在所有的信息都发送出去之后,向对端发送一个关闭帧,状态码和接收到的关闭帧一样。
(2)
ping
帧opcode
为 0x9。可以在
Application data
携带数据。在连接建立和关闭之前的任意时间,都可以发送
ping
帧。ping
帧可以用来保持心跳或者确认连接的有效性。(3)
pong
帧opcode
为 oxA。pong
可以只对最近收到的ping
帧进行回复。回复的内容要和ping
帧中Application data
携带的内容一样。pong
帧可能在没有接受到ping
帧的情况下发送,对未经请求的pong
帧应该直接忽略。(4) 数据帧
数据帧的
opcode
的第一位为 0。文本帧:编码需要为
UTF-8
。二进制帧:由应用上层解释二进制帧的数据含义。
- 未进行掩码的文本帧 – 单帧
0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains "Hello")
- 进行掩码的文本帧 – 单帧
0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (contains "Hello")
- 未进行掩码的文本帧 – 多帧
0x01 0x03 0x48 0x65 0x6c (contains "Hel") 0x80 0x02 0x6c 0x6f (contains "lo")
- ping 帧和 pong 帧
0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains a body of "Hello",but the contents of the body are arbitrary) 0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (contains a body of "Hello", matching the body of the ping)
- 携带 256 字节二进制数据的未掩码二进制帧 – 单帧
0x82 0x7E 0x0100 [256 bytes of binary data]
- 携带 64kb 字节二进制数据的未掩码二进制帧 – 单帧
0x82 0x7F 0x0000000000010000 [65536 bytes of binary data]
断线重连
为了避免在某一个时刻,服务器接收到大量的连接请求,客户端在断线重连的时候应该考虑延迟的做法。建议的时间是 0 至 5 秒内。如果连续的请求失败,应该考虑使用 截断二进制指数退避算法
延迟连接。
由于瞬时的大量请求导致服务器瘫痪的攻击,请查阅 分布式拒绝服务攻击
。