websocket 协议

前言

背景

一直以来,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 对服务器进行的一些攻击。计算步骤:

    1. 过滤 Sec-WebSocket-Key- 和空格字符。
    2. 将过滤的 Sec-WebSocket-Key 和 GUID 连接。
    3. 将连接得到的字符串进行 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 端点(相同的 ipport)。这有助于防止分布式拒绝服务攻击(意即客户端同时请求大量恶意的连接,导致服务器瘫痪)。

(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. 概述

(1) 出于安全以及其他的因素考虑,客户端发送到服务器的所有帧都需要进行掩码的处理,服务器发送到客户端的帧不能进行掩码的处理。如果服务器接收到客户端的帧,未进行掩码的处理,那么,需要发送 1002 状态码,并断开连接。

(2) 数据帧的发送时段:握手完成,连接处于 open 状态以及连接关闭帧发送之前。

  1. 帧结构

     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 :1 bit

    表示 message 中的最后一帧;message 可以独立成帧。

  • RSV1RSV2RSV3 :1 bit

    没有扩展协议的情况之下,值必须为零;否则,接收端必须关闭连接。

  • Opcode :4 bit

    定义对 Payload data 的内容怎么进行解释。

    %x0 : 表示一个连续帧。

    %x1 : 表示一个文本帧。

    %x2 : 表示一个二进制帧。

    %x3-7 : 为未来保留的非控制帧。

    %x8 : 表示连接关闭。

    %x9 : 表示 ping 帧。

    %xA : 表示 pong 帧。

    %xB-F : 为未来保留的控制帧。

  • Mask : 1 bit

    表明 payload data 是否经过掩码处理。如果 mask = 1, 根据 masking-keypayload data 进行反掩码的处理。所有从客户端发送到服务器的帧都需要进行掩码处理。

  • Payload length : 7 bits, 7 + 16 bits, or 7 + 64 bits

    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                                                 
                             
    
  1. 客户端到服务器的掩码算法

    对数据进行掩码算法是为了防止代理缓存污染攻击。

    frame-masked=1 时,用 frame-masking-keypayload data 进行掩码运算。其中 payload data 包括了 extension dataapplication 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
    
  2. 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 的所有分帧的类型都应该是一样的:要么 textbinary等。

    message 过大的时候,控制帧,比如 ping 帧可能会被延迟。为了解决这种情况,需要能够优先处理控制帧的能力。

  3. 控制帧

    控制帧 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 秒内。如果连续的请求失败,应该考虑使用 截断二进制指数退避算法 延迟连接。

由于瞬时的大量请求导致服务器瘫痪的攻击,请查阅 分布式拒绝服务攻击

博客

  1. WebSocket:5分钟从入门到精通

资料

  1. RFC2616,Http/1.1
  2. websocket
  3. GUID
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值