Websocket协议 详解(rfc6455)

Websocket 开发相关的两篇文章

WebSocket协议 + nginx 动态负载均衡 (史上最全)
Websocket协议 详解(rfc6455)

Websocket协议简介

Websocket协议能在受控的环境内实现浏览器与服务器之间的双向通讯(浏览器中的应用可能是不可靠的,但是仍然可以与服务器建立websocket连接),使得浏览器应用与服务器进行双向通讯时不必同时打开多个HTTP连接(使用XMLHttpRequest 、iframe 或者长轮询实现双向通讯时经常会打开多个连接)。Websocket位于TCP之上(位于应用层),主要包括握手过程、数据传输两个主要部分。

1.介绍

1.1.背景

本部分为非权威描述

历史上,web应用(即时通讯或者游戏)为了实现与服务器的双向通讯,一般会建立一个发送消息的http连接与一个接收消息的http连接。 这样会导致几个问题:

  1. 服务器与每个客户端维持多个连接。
  2. 网络会产生过多的负载,因为每一个http消息都有头部信息。
  3. 客户端需要把发送消息的连接与接收消息的连接建立映射。

Websocket协议设计的目标是使用一个连接实现客户端与服务器之间的通讯以此替代http长轮训。Websocket基于http实现双向通讯可以从当前基础设施(代理、过滤、认证)中获得更多支持。 Websocket基于http协议的80和443端口工作,即使这样会增加协议的复杂度。 当然,我们在设计Websocket时没有限制必须基于http协议,未来也许会单独开一个端口,然后使用更简单的握手过程从而替换掉底层依赖的http协议。

1.2.协议概览

本部分为非权威描述

协议主要分为两部分:握手(handshake)和传输(data transfer)。

客户端发起的握手协议如下:

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

服务器响应的握手协议如下:

        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat

客户端发送的首行遵循Request-Line格式。 服务器响应的首行遵循Status-Line格式。这两种格式都在rfc2616中定义。

Request-Line或者Status-Line之后都跟随着一组无序的头部字段。这些字段的具体意义,在本文第四章有讲述。其他的字段也可以使用,比如cookies(RFC6265),头部字段的定义和解析在rfc2616中定义。 如果客户端和服务器成功地完成了握手阶段,那么连接进入数据传输阶段。这是一个双向的连接,连接两段都可以随意发送数据。

握手成功之后,客户端和服务器可以相互发送数据,我们把相互发送的数据单位称作消息(message)。实际传输过程中,一个消息可能包含多个frame(帧)。

每个帧都有特定类型。同属于一个消息的数据帧拥有相同的数据类型。数据类型包括文本(UTF-8编码的字符)、二进制(具体解析方式由程序定义)、控制(用作控制作用,比如管理连接的打开和关闭)。这个版本的协议定义了六种数据类型,并且预留十种类型。

1.3.开始握手

本部分为非权威描述
握手阶段主要用来兼容http服务器或者中间件,这样同一个端口就可以使用websocket协议和http协议。Websocket用来升级Http请求的报文如下:

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

为了与[RFC2616]兼容,头部字段可以以任何顺序排列。

Get方法中的Request-URI用来指定可以处理websocket请求的服务器接口,这样可以在一个ip下部署多台服务器,也可以在一台服务器中部署多个应用服务器(根据Request-URI进行路由)。

客户端在发送的请求头部添加|Host|字段,指定要连接的主机名字。

其他的字段用来指定协议提供的其他选项。常见的选项有子协议(|Sec-WebSocket-Protocol|)、子协议扩展(|Sec-WebSocket-Extensions|)、|Origin|字段等。 |Sec-WebSocket-Protocol|列出了客户端支持的子协议列表,服务器选择其中一个协议并返回给客户端。

Sec-WebSocket-Protocol: chat

|Origin|字段方便服务器识别未授权的浏览器应用发送的websocket连接建立请求。服务器可以通过这个字段获取客户端的Origin信息,如果服务器不接受来自这个Origin的连接建立请求,可以拒绝客户端的请求。|Origin|字段的值是由浏览器设置的,非浏览器环境的客户端可以根据当时环境设置合理的值。

为了证明服务器收到了来自客户端发送的握手信息,服务器需要使用两个字符串生成一个响应信息。第一个信息来自客户端握手消息中的|Sec-WebSocket-Key|字段:

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

服务器把Sec-WebSocket-Key字段的值与GUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”(如果网络节点中没有运行websocket协议的话会很难理解这个字符串的含义)连接生成一个字符串,然后进行SHA-1运算,最后进行base64编码,生成的数据作为服务器的响应。

比如:客户端发送的字段|Sec-WebSocket-Key|中包含的值为dGhlIHNhbXBsZSBub25jZQ,服务器把这个值与258EAFA5-E914-47DA-95CA-C5AB0DC85B11进行连接,然后生成dGhlIHNhbXBsZSBub25jZQ258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串,进行SHA-1运算,生成0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea,最后进行base64编码生成s3pPLMBiTxaQ9kYGzzhZRbK+xOo=,然后放入服务器的|Sec-WebSocket-Accept|字段中并响应客户端请求。

服务器返回的握手信息很简单,第一行为http状态行,状态为101:

HTTP/1.1 101 Switching Protocols

其他状态码表示握手还没完成,状态码的含义依然遵循http定义。

|Connection|和|Upgrade|表示握手过程完成。|Sec-WebSocket-Accept|表示服务器是否接收这个连接,如果有这个字段,这个字段的值必须为客户端提供的|Sec-WebSocket-Key|字段的值与预先定义好的GUID值进行哈希,在进行base64编码。任何其他的值都表明服务器没有接受客户端发起的请求。

   HTTP/1.1 101 Switching Protocols
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

这些字段会在客户端进行校验,如果|Sec-WebSocket-Accept|的值与客户端期望的值不一致、没有这个字段或者HTTP状态码不为101,那么连接不会被建立并且websocket数据帧不会发送。

|Sec-WebSocket-Protocol|表示服务器选择的子协议。客户端校验服务器返回的值是否为客户端。

Sec-WebSocket-Protocol: chat

1.4.挥手过程

本部分为非权威描述

挥手过程要比打开过程简单的多。 任何一端都可以发送一个Close帧来开始挥手过程,Close帧可能带有部分数据(比如描述关闭的原因以及状态码)。任何一端收到一个Close帧,如果之前没有回复过的话,需要发送Close帧。主动关闭的一端在收到对端返回的响应后,在确定没有数据需要继续接收之后,开始关闭底层连接(shutdown)。

在发送Close帧之后不应该发送任何数据帧,在收到对端发送过来的Close帧后,对于后续的数据,接收端不予处理。

两端可以同时发送挥手控制帧。

挥手控制帧用来关闭两端之间的tcp连接,因为有时两端之间并不是直接相连,中间有可能会有代理或者其他中间设备。

发送一个挥手控制帧然后等待响应可以防止某些情况下丢失数据。比如在某些软件平台,如果socket一端在接收队列里还有未处理的数据,但是关闭了连接,这时会向对端发送一个rst消息,对端在收到rst消息后会让在recv()监听的线程收到函数返回的错误信息,即使当时接收队列里还有数据(因为两端都没有成功处理消息,所以两端需要对类似的rst错误进行处理)。

1.5.设计哲学

本部分为非权威描述

Websocket协议应该尽量减少使用帧相关的概念(只有在描述协议是基于帧的而不是基于流的时候与用来区分文本帧和二进制帧的时候会涉及到帧)。应用层在websocket层之上,所发送的数据都会经过websocket这一层去传递,这一点与http使用tcp去发送数据大致相同。

概念上讲,websocket就是基于tcp的协议,拥有以下功能:

  1. 为浏览器添加基于origin的安全模型
  2. 增加寻址和命名服务,可以实现多个服务(http和websocket)监听同一个端口,同时也可以实现一个ip有多个主机名字。
  3. 在tcp之上建立分帧功能,类似tcp基于的ip协议,但是websocket的帧没有长度限制。
  4. 增加挥手过程,用来解决链路中的代理或者其他中间系统。

除了上面列出的功能外,websocket没有增加其他功能。考虑到web浏览器的限制,尽可能的只把原生的tcp接口暴露给脚本去调用。如果客户端发送过来的是合法的http升级请求,那么websocket服务器可以与http服务器共享一个端口。有人可能会使用其他协议来实现客户端和服务器之间的消息通讯,但是websocket协议的设计初衷就是提供一个相对简单的协议来与http协议或者http基础设施(代理)共存,websocket提供的长连接使得经过这些基础设施时与直接使用tcp一样安全,并且通过增加一些附属功能来简化使用的方式。

协议具有扩展性,未来版本可能会新增其他的功能(概念),比如多路复用。

1.6.安全模型

本部分为非权威描述

Websocket协议使用与web浏览器一样的安全模型(origin-based)来控制应用可以与哪些服务器建立连接。如果在一个专用的客户端中使用websocket协议,这个安全模型就显得没有必要了,因为客户端可以提供任何可能的origin值。

运行[SMTP]与HTTP协议的服务器不会与websocket客户端建立连接,但是如果HTTP服务器支持升级到websocket协议则可以建立连接。为了保证协议的正确性,在握手没有完成之前不可以发送应用数据。

如果websocket服务器接收到了其他协议的(主要指http协议数据)数据,那么整个连接会被关闭。Websocket在握手阶段会使用专用的头部字段,服务器可以通过验证这些专用的头部字段来保证握手过程的合法性,在这个规范编写的时候,网络攻击者不会在网页应用(html和js)中使用XMLHttpRequest发送带有|Sec-|前缀的头部字段。

1.7.与TCP和HTTP的关系

本部分为非权威描述

Websocket是基于TCP独立设计的协议。与HTTP的唯一关系是,websocket的握手协议是通过HTTP的协议升级实现的。

默认情况下,普通websocket连接使用80端口,安全的websocket连接使用443端口,基于TLS安全层。

1.8.建立一个连接

本部分为非权威描述

向一个既支持websocket协议又支持http协议的服务器发送websocket请求时,应该使用传统GET请求,并且带有Upgrade头部字段。在简单的部署场景中,一台服务器可以同时支持websocket与http协议。在一些复杂的场景中(多机部署以及负载均衡部署),websocket服务器和http服务器分开部署易于管理。在编写规范的时候,80端口和443端口的连接成功率不一样,443的成功率要高点,这个可能随着时间的变化会有所变化。

1.9.使用子协议

本部分为非权威描述

客户端可以在握手请求中添加|Sec-WebSocket-Protocol|字段要求服务器从中选择一个支持的子协议。服务器在握手响应中需要包含这个字段并且选择支持的子协议。

子协议按照[Section 11.5]规定注册名字。为了避免名字冲突,协议名字应该包含子协议指定方的主机名字,并且主机名字是ascii编码格式的。举个例子,比如Example公司打算创建一个chat子协议,这个协议可能会被网络上的很多服务器使用,那么这个子协议的名字可以命名chat.example.com。如果Example组织也创建了自己的子协议名称,命名为chat.example.org,那么多个服务器可以同时实现,并且在握手过程中从客户端提供的子协议列表中选择一个。

子协议为了实现向后兼容可以更改名称,比如bookings.example.net改成v2.bookings.example.net。这些协议可以被客户端轻松的分辨出来。重用相同的子协议名字也可以实现向后兼容,但是这么做的时候需要认真设计子协议(比如通过其他扩展字段实现版本化来支持向后兼容)。

2.规范要求

如果有的图表、示例、注释在这个文档中标记为非规范的,表示非标准规范,没有显式标记的都是规范标准。

文本中的关键字,“MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL"都在[RFC2119]中进行记录。 在描述算法的文字中,那些祈使句一般可以解释成"MUST”, “SHOULD”, “MAY"等(比如"strip any leading space characters” or “return false and abort these steps”)。

规范如果被表述为某些算法或者某些规定的步骤,那么他们的实现方式可能是多样性的,但是如果他们的结果是一样的,那就是可以接受的(特别的,本规范里规定的算法都很简单,并且方便实现。)。

2.1.术语和其他约定

_ASCII_表示[ANSI.X3-4.1986]中描述的字符编码。

本文档使用定义在[RFC3629]中的UTF-8字符编码。

关键字和算法命名与定义都用_this_表示。

头部字段和变量使用|this|这种格式。

变量值使用/this/。

[Section 7.1.7]中描述的流程定义为_Fail the WebSocketConnection_。

关键字_Converting a string to ASCII lowercase_ 是指把U+0041 到 U+005A区间的字符替换成U+0061 到 U+007A区间的字符。

_ASCII case-insensitive_表示比较两个字符串是大小写不敏感的,字母A-Z与a-z之间相应的字母认为是相同的(A与a是相同的)。

URI的意思与[RFC3986]定义的一样。

当websocket实现被要求_send_发送一个数据时,具体实现可以按照需要在某个时间去真正发送数据(数据可能事先在buffer中缓存)。

本文在不同的章节都使用使用[RFC5234]和[RFC2616]中的[ABNF]扩展语法规则。

3.WebSocket URIs

本规范使用了两种URI框架,使用了[RFC 5234]中定义的ABNF语法与[RFC 3986 ]中定义的单词和其他的一些术语。

    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>

端口组件是可选的,ws默认是80端口,wss默认端口是443。

如果端口组件是wss(大小写不敏感)的话,那么这个连接是安全连接。

资源名字可以由一下几部分组成:

  • 如果path为空,使用"/"。
  • path组件。
  • 如果query组件不为空使用?。
  • query组件。

段落标识符在websocket uri中没有意义,在所有的URI框架中,如果#不表示段落的开始,那么应该使用%23进行转义。

4.开始握手

4.1.客户端要求

客户端与服务器成功建立socket连接之后会发送websocket握手信息。Websocket在开始阶段处于CONNECTING状态。客户端需要提供/host/, /port/, /resource name/和 /secure/,具体含义在第三章中有描述,除了这些参数之外有可能会提供/protocols/ 和 /extensions/列表,如果客户端是浏览器,还需要添加/origin/。

便携式设备中的浏览器访问网络时可能会通过某些代理软件,所以,本规范的客户端包括了便携式设备中的浏览器软件和代理软件。

使用(/host/, /port/, /resource name/和 /secure/ )、/protocols/ 、 /extensions/、/origin/(如果客户端是浏览器)来与服务器建立连接,然后发送握手请求,等待服务器的响应。具体的如何打开连接,如何发送握手请求,服务器如何对握手请求进行回应,都会在下文进行描述。后续我们会使用第三章中规定的单词进行讨论(比如/host/与/secure/标识符)。

  1. 传入的/host/, /port/, /resource name/和/secure/ 标志位必须符合第三章中的规定,如果不符合,客户端必须马上_Fail the WebSocket Connection_,然后退出这个流程。
    如果客户端与服务器(ip为1.1.1.1)正在建立连接,即使服务器使用了其他的服务器名字(/host/),客户端必须等待这个连接建立完成或者关闭连接,保证只有一个连接处于CONNECTING状态。如果客户端有多个连接同时连接一个服务器(同一个IP),那么客户端必须串行化这些连接操作,保证同一时刻只有一个连接执行如下步骤。

  2. 如果客户端没办法判断服务器的ip应该假设每个主机域名都对应一个ip地址,并且客户端应该限制处于connecting状态的连接数量(比如客户端允许分别与a.example.com、b.example.com服务器建立连接)。浏览器通过限制用户同时打开的tab数量来防止客户端发送大量连接建立请求,以便防止DDOS攻击。服务器遭受DDOS攻击时会首先暂停接收新的连接建立请求,然后慢慢关闭部分已创建的连接,这样可以防止因为关闭连接过多而导致的客户端大量重连操作。

    注意:客户端和服务器之间可以建立的websocket连接(connected)数量没有限制,如果一个客户端建立了太多连接或者连接占用太多服务器资源,服务器可以主动关闭这些连接。

  3. Proxy Usage:如果客户端使用了代理,那么客户端与代理建立连接后应该通知代理与/host/和/port/指定的服务器建立tcp连接。

    假如客户端使用了代理来处理http请求,在与example.com:80端口通讯时,可以向代理发送如下指令:

       CONNECT example.com:80 HTTP/1.1
       Host:example.com
    
       如果有密码字段,则可以发送如下执行:
    
       CONNECTexample.com:80 HTTP/1.1
       Host:example.com
    
       Proxy-authorization: Basic ZWRuYW1vZGU6bm9jYXBlcyE=
    

    如果客户端没有设置代理,那么客户端可以直接与指定的 /host/ 和/port/建立连接。

    注意:如果没有UI界面来为Websocket连接选择代理,那么建议使用SOCKs5来代理Websocket连接,如果不能选择使用SOCKs5代理,建议使用HTTPs代理而不是HTTP代理。

  4. 如果不能打开连接,不管是由于直连或者通过代理连接,客户端都应该关闭相应的连接并且不再尝试建立连接。

  5. 如果设置了/secure/标识符,客户端必须在连接上发送TLS握手请求[RFC2818],然后再发送websocket握手信息。如果TLS握手失败(服务器证书不合格),客户端应该关闭连接。如果成功的话,之后所有的数据都应该在安全的TLS通道上发送。

    客户端在TLS握手过程中必须使用Server Name Indication extension[RFC6066]相关扩展。

连接建立成功之后,客户端向服务器发送websocket握手消息。握手消息中包括一个HTTP Upgrade以及一系列必须的或者可选的头部字段。具体的要求信息如下:

  1. 握手消息必须是一个合法的HTTP消息(rfc2616)。
  2. 请求方法必须为GET,并且http版本必须为1.1以上(包括1.1)。如果websocket的uri为ws://example.com/chat,那么请求行为GET /chat HTTP/1.1。
  3. 请求地址(URI)(Request-URI)必须满足[Section 3]中定义的规则(相对地址)或者绝对地址(http/https),通过解析后有/resource name/、 /host/和 /port/ ,并且满足相应的ws/wss URI。
  4. 请求必须包括|Host|字段并且字段的值为/host/加上相应的/port/(不使用默认端口的情况下)。
  5. 必须包括|Upgrade|字段并且值必须包含websocket关键字。
  6. 必须包括|Connection|字段,并且值为Upgrade。
  7. 必须包括|Sec-WebSocket-Key|字段,值为随机选择的没有任何意义的16字节数据,并且经过base64编码。每个连接的编码都必须不一样。
    举个例子:如果选择的值为0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10,那么经过base64编码之后AQIDBAUGBwgJCgsMDQ4PEC==。
  8. 如果请求是从浏览器发送的,那么必须包括|Origin|字段。如果是从非浏览器发送的请求并且符合当前描述的使用场景,也可以发送这个字段。这个字段的值为运行前端应用的主机名字并且是经过ascii编码的。[RFC6454]通过了解更多的赋值规则。
    举个例子,前端应用从www.example.com地址下载的,应用与ww2.example.com服务器建立连接时,那么这个值为http://www.example.com。
  9. 必须包括|Sec-WebSocket-Version|字段,并且值为13。
    注意:尽管这个协议已经有多个版本的草稿(-09、-10、-11和-12),但是他们仍然不会被当做Sec-WebSocket-Version的值使用。这些值注册在IANA中,但是不会被使用。
  10. 请求可能包括|Sec-WebSocket-Protocol|字段。如果有这个字段,字段值为客户端希望使用的子协议列表,子协议按照期望程度进行排序。子协议的名字必须是唯一的并且由U+0021到U+007E之前的字符组成,不包括分隔符。这个字段值的命名规范使用ABNF中的1#token,这个规则在[RFC2616]中有描述。
  11. 请求可能包含|Sec-WebSocket-Extensions|字段。这个字段表示客户端希望使用的协议扩展。具体的协议扩展格式在[Section 9.1]中有描述。
  12. 可能还包括其他字段,比如cookies[RFC6265]、授权相关的|Authorization|[RFC2616],这些字段的解析规则都要参考定义他们的文档。

一旦客户端发送了握手请求,客户端必须等待服务器的响应,在获取响应前不应该发送任何数据。客户端也应该按照如下规则检验服务器的响应信息。

  1. 如果状态码不是101,那么客户端应该按照[RFC2616]中规定的过程处理响应的数据。如果收到401状态码,客户端需要进行认证过程。服务器可能通过3xx状态码要求客户端进行重定向(客户端可以不进行重定向操作)。其他情况按照如下规则处理。
  2. 如果响应头部没有|Upgrade|字段或者字段的值在大小写不敏感的情况下与websocket不匹配,那么客户端可以直接_Fail the WebSocket Connection_。
  3. 如果响应头部没有|Connection|字段,并且字段值没有包含大小写不敏感的“Upgrade”,那么客户端应该_Fail the WebSocket Connection_。
  4. 如果响应头部没有|Sec-WebSocket-Accept|字段或者|Sec-WebSocket-Accept|包含的值不是|Sec-WebSocket-Key|(没有经过base64加密的)与258EAFA5-E914-47DA-95CA-C5AB0DC85B11连接并且经过base64-encoded SHA-1的值(出去前后的空白字符),客户端必须_Fail the WebSocket Connection_。
  5. 如果包括|Sec-WebSocket-Extensions|字段,并且值并不是客户端提供的,客户端必须_Fail the WebSocket Connection_。
  6. 如果包括|Sec-WebSocket-Protocol|字段,并且值并不是客户端提供的,客户端必须_Fail the WebSocket Connection_。

如果服务器响应不符合[Section 4.2.2]规定的要求,客户端必须_Fail the WebSocket Connection_。

根据[RFC2616]中的规定,http中的所有请求字段和响应字段都是大小写不敏感的。

如果服务器响应通过上述验证规则,那么websocket连接就进入打开状态。当前使用的协议扩展是服务器响应头部返回的|Sec-WebSocket-Extensions|字段的值,如果响应中没有指定协议扩展,那么当前连接就没有使用协议扩展。正在使用的子协议也是使用服务器返回的|Sec-WebSocket-Protocol|字段中的值,如果响应中没有指定子协议,那么当前连接就没有使用子协议。除此之外,如果在握手阶段服务器要求设置cookies[RFC6265],那么cookies就是在握手阶段设置的cookies。

4.2.服务端要求

服务器可能使用网络代理管理连接(比如负载均衡服务器或者反向代理)。在这种场景中,协议指定的服务器包括了服务端的所有基础设施,从接受tcp连接的设施到处理客户端请求的设施。

4.2.1.读取客户端的握手请求

客户端的握手请求包含如下几部分。如果服务器在读取客户端的握手请求时发现客户端没有发送协议指定的字段,并且字段的名字或者值不符合ABNF规定的,服务器可以直接不处理这个握手请求,并且返回错误码(400 Bad Request)。

  1. HTTP协议版本为1.1或者更高,并且为Get请求,包括一个Request-URI[RFC2616]地址,这个URI应该被解析成/resource name/。如果是绝对地址,这个URI至少应该包括/resource name/。
  2. |Host|头部字段。
  3. |Upgrade|字段值为websocket并且大小写不敏感。
  4. |Connection|字段值为Upgrade并且大小写不敏感。
  5. |Sec-WebSocket-Key|字段,值为[Section 4 of RFC4648]中规定的格式,经过base64反解码可以得到16个字节的数据。
  6. |Sec-WebSocket-Version|值为13.
  7. |Origin|字段,所有的浏览器都应该有这个字段,如果请求没有这个字段,则发送请求的客户端不应该被当做浏览器对待。
  8. 可选的 |Sec-WebSocket-Protocol|字段列出了客户端期望使用的协议,根据期望程度进行排序。
  9. 可选的|Sec-WebSocket-Extensions|字段列出了客户端期望使用的协议扩展。这个字段的解析在[Section 9.1]中进行了描述。
  10. 其他可选的字段比如认证或者cookies。其他没有在[RFC2616]里面描述的字段都忽略。

4.2.2.发送服务器的握手响应

当客户端与服务器建立websocket连接时,服务器必须执行如下流程来接收这个连接,并且返回响应。

  1. 如果连接的端口是443端口,那么服务器必须执行一个TLS握手过程。如果TLS握手失败(客户端在扩展字段中指定的host地址和服务器的地址不一致),服务器关闭这个连接。如果成功,所有后续的通讯都要在这个加密通道中传输[RFC5246]。

  2. 服务器可以执行一些认证,比如,返回401状态码同时带有|WWW-Authenticate|字段,这个字段在[RFC2616]中有描述。

  3. 服务器可能通过3xx[RFC2616]状态码要求客户端跳转到指定位置。这一步可能在认证之前也可能之后执行。

  4. 证实如下信息:

    /origin/表示程序从哪里下载的。origin的值为ASCII字符并且是小写的。服务器可能使用这个值来判断是否接收这个连接。如果服务器不检查这个字段的值,那么服务器会接收来自任何客户端的连接请求。如果服务器不打算接收这个连接可以返回一个错误码(比如403拒绝)。更多相关信息可以查看[Section 10]。

    |Sec-WebSocket-Key| 这个头部字段是客户端在握手过程中发送的,经过base64加密的,如果解密的话会获得16个字节的数据。服务器在生成握手响应的时候会使用这个数据来表示接收了客户端的握手请求。

    |Sec-WebSocket-Version|表示客户端希望使用的websocket版本号。如果服务器没有客户端请求使用的协议版本号,那么服务器应该返回一个错误码(比如426)并且使用|Sec-WebSocket-Version|表示服务器可以使用的协议版本号。

    /resource name/表示服务器提供的某种服务。如果服务器提供多种服务,那么应该从客户端的握手信息(Request-URI[RFC2616])中提取到具体的值来确定使用哪些服务。如果服务器没有对应的服务,那么应该返回一个HTTP错误码(比如404 Not Found)。

    /subprotocol/表示服务器打算使用的一个子协议。这个子协议的值必须是从客户端握手请求中提供的子协议列表中选择的,在|Sec-WebSocket-Protocol|字段中指定。如果客户端没有使用这个字段或者服务器没有选择子协议的话,那么这个连接不使用任何子协议。没有这个字段与有这个字段但是值为空(null)的效果是一样的,如果服务器不选择任何一个子协议的话,握手响应中不能有这个字段(|Sec-WebSocket-Protocol|)。空字符串与null的值是不一样的,空字符串不是一个合法的取值。值的格式准寻ABNF规则,在[RFC2616]中有描述相关规则。

    /extensions/列出了服务器打算使用的协议扩展(有可能也没有列出),这些扩展值必须从客户端握手请求中的|Sec-WebSocket-Extensions|字段值中选择出来的。没有|Sec-WebSocket-Extensions|这个字段与空值(null)是一样的效果。空字符串不是合法的值。具体的选择规则以及解析规则在[Section 9.1]中有描述。

  5. 如果服务器打算接收客户端发送的连接请求,必须按照如下流程做出响应。

    1. HTTP/1.1 101 Switching Protocols。
    2. |Upgrade|:websocket。
    3. |Connection|:"Upgrade"。
    4. |Sec-WebSocket-Accept|字段,取值为上面第四步里描述的/key/值与258EAFA5-E914-47DA-95CA-C5AB0DC85B11串联起来,进行sha-1运算最终获得20个字节,再最后进行base64编码。
       ABNF定义的值为:
          Sec-WebSocket-Accept    = base64-value-non-empty
          base64-value-non-empty 	= (1*base64-data [ base64-padding ]) |base64-padding
          base64-data      		= 4base64-character
          base64-padding   		= (2base64-character "==") |(3base64-character "=")
          base64-character 		= ALPHA | DIGIT | "+" | "/"
    比如:客户端在|Sec-WebSocket-Key|字段中提供了“dGhlIHNhbXBsZSBub25jZQ==”这个值,服务器把这个值与“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”进行拼接获得“dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11”之后进行SHA-1运算获得
    0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea,
    最后进行base64编码获得“s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”,把这个值作为|Sec-WebSocket-Accept|字段的值返回。
    |Sec-WebSocket-Protocol|值为上面中定义的/subprotocol/。
    5.可选的|Sec-WebSocket-Protocol|字段取值为上面第四步中定义的/subprotocol/。
    6.可选的|Sec-WebSocket-Extensions|字段取值为上面第四步中定义的/extensions/,如果有多个扩展,可以再这个字段中列出来,也可以使用多个|Sec-WebSocket-Extensions|字段。
    

这样就算完成了握手过程。如果服务器没有关闭连接,那么websocket连接便进入OPEN状态,之后的数据就可以在这个连接上进行传递。

4.3.在握手阶段使用的并且符合ABNF规则的新增字段

本部分使用了在 Section 2.1 of[RFC2616]描述的ABNF语法规则,同时也包括隐式的 *LWS规则。 如下描述的ABNF规则在本段落使用。规则描述了对应字段值的格式。比如Sec-WebSocket-Key描述了|Sec-WebSocket-Key|值的规则。带有-Client后缀的规则描述了客户端请求中的字段值的规则。带有-Server后缀的规则描述了服务器响应中的字段值的规则。比如,Sec-WebSocket-Protocol-Client描述了客户端发送的|Sec-WebSocket-Protocol|字段值的规则。如下字段是客户端发送到服务器的字段值的规则:

      Sec-WebSocket-Key = base64-value-non-empty
      Sec-WebSocket-Extensions = extension-list
      Sec-WebSocket-Protocol-Client = 1#token
      Sec-WebSocket-Version-Client = version

      base64-value-non-empty = (1*base64-data [ base64-padding ]) |
                                base64-padding
      base64-data      = 4base64-character
      base64-padding   = (2base64-character "==") |
                         (3base64-character "=")
      base64-character = ALPHA | DIGIT | "+" | "/"
      extension-list = 1#extension
      extension = extension-token *( ";" extension-param )
      extension-token = registered-token
      registered-token = token
            extension-param = token [ "=" (token | quoted-string) ]
           ; When using the quoted-string syntax variant, the value
           ; after quoted-string unescaping MUST conform to the
           ; 'token' ABNF.
      NZDIGIT       =  "1" | "2" | "3" | "4" | "5" | "6" |
                       "7" | "8" | "9"
      version = DIGIT | (NZDIGIT DIGIT) |
                ("1" DIGIT DIGIT) | ("2" DIGIT DIGIT)
                ; Limited to 0-255 range, with no leading zeros

如下字段描述了服务器返回的字段的值的格式:

      Sec-WebSocket-Extensions = extension-list
      Sec-WebSocket-Accept     = base64-value-non-empty
      Sec-WebSocket-Protocol-Server = token
      Sec-WebSocket-Version-Server = 1#version

4.4.支持多版本的websocket协议

本部分给出了实现客户端和服务器之间支持多个版本的指导意见。

客户端通过|Sec-WebSocket-Version|字段声明它希望使用的协议版本。如果服务器支持客户端提供的版本并且客户端提供的其他字段也是合法的,服务器会接收这个版本。如果服务器不支持客户端提供的协议版本,服务器必须返回一个|Sec-WebSocket-Version|(或者多个字段)字段来声明它所支持的协议。如果客户端支持其中一个协议,可以继续使用上述过程把支持的协议版本发送到服务器。

以下示例描述了协议版本的协商过程:

      GET /chat HTTP/1.1
      Host: server.example.com
      Upgrade: websocket
      Connection: Upgrade
      ...
      Sec-WebSocket-Version: 25

服务器的响应信息如下:

      HTTP/1.1 400 Bad Request
      ...
      Sec-WebSocket-Version: 13, 8, 7

服务器也可能返回如下:

      HTTP/1.1 400 Bad Request
      ...
      Sec-WebSocket-Version: 13
      Sec-WebSocket-Version: 8, 7

客户端再次发送握手请求:

      GET /chat HTTP/1.1
      Host: server.example.com
      Upgrade: websocket
      Connection: Upgrade
      ...
      Sec-WebSocket-Version: 13

5.数据分帧

5.1.总览

在websocket协议中,数据是通过一系列帧进行传递的。为了防止网络攻击(Websocekt为网络安全带来哪些挑战?),客户端在发送数据时必须对数据帧进行掩码(数据帧进行掩码与是否运行在TLS安全层上无关)。服务器如果收到没有掩码的数据帧,需要立即关闭这个连接。在这种情况下,服务器可能发送一个关闭帧,然后状态码为1002(Section 7.4.1)。服务器不用对数据帧进行掩码。客户端如果收到掩码的数据,必须关闭这个连接。在这种情况下客户端可能需要返回1002(Section 7.4.1)状态码。这些规则限制可能在协议的未来版本中变得宽松。

帧协议定义了表示类型的opcode字段、数据长度(payload length)字段、表示扩展数据(designated locations for “Extension data”)和应用数据(Application data)位置的字段,扩展数据和应用数据共同组成了数据部分(Payload data)。其他数据位和opcode值预留给未来扩展使用。客户端和服务器建立连接后如果没有关闭连接的话,可以双向发送数据帧。

5.2.协议的基本分帧功能

数据传输的格式通过ABNF规则描述[RFC5234]。注意,不像其他章节的ABNF规范一样,本章的ABNF主要是用来描述位组,位组的长度在注释中标注了。当编码时,最高位为最左边的数据位。数据格式的总体描述如下图。如果同一个规则在下图和下面使用的ABNF规则里都描述了,以图为准。

      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

    表示是否为最后一帧。第一个数据帧可能同时也是最后一个帧。

  • RSV1, RSV2, RSV3: 每个1位

    如果协商过程中没有确定意义的话,每个值都为0。如果收到一个非0的设置,但是在协商过程中又没有规定具体的意义,接收端应该关闭这个连接。

  • Opcode 四位<

    定义Payload data的类型,如果收到一个无法理解的值,接收方应该立即关闭这个连接。具体的定义如下:

      *  %x0 表示后续还有数据帧
      *  %x1 表示文本帧
      *  %x2 表示二进制帧
      *  %x3-7 预留
      *  %x8 表示连接关闭
      *  %x9 ping
      *  %xA pong
      *  %xB-F 预留
  • Mask: 1 bit

    标记Payload data是否经过掩码。如果设置为1,masking-key字段的值用来编码,也可以用来解码。所有从客户端发送到服务器的数据都必须进行掩码(mask标记必须为1)。

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

Payload data长度,以字节为单位,如果是0-125表示Payload data的字节数。如果是126,后续的2个字节为无符号数,表示Payload data的长度。如果是127,后面的8个字节表示为无符号数,表示Payload data的长度。多字节数据按照网络字节序(大端)处理。注意:在所有场景中,最少的字节数表示Payload data的长度,比如124个字节长度的字符串不能表示为126, 0, 124。payload length表示Extension data数据的长度和Application data的长度总和。Extension data的长度有可能为0,这时候payload length的长度为Application data(应用数据)的长度。

  • Masking-key: 0 or 4 bytes

    所有从客户端发送到服务器的数据帧都需要与一个32位长的key进行掩码。这个32位长的key随数据帧一起发送。如果MASK设置为1,key有值。详细的编码过程参照Section 5.3

  • Payload data: (x+y) bytes

Payload data是由Extension data" 与 "Application data"组成。

  • Extension data: x bytes

如果在协商阶段没有规定扩展数据的话,这个长度为0。如果协商阶段说明了扩展数据,扩展数据的长度必须标明,如果没有标明长度,也要在协商的过程中说明如何计算扩展数据长度并且也同时要说明如何使用这些扩展数据。

  • Application data: y bytes

应用数据紧随扩展数据之后,应用数据的长度为Payload length减去扩展数据的长度。

基本的分帧操作由下面的ABNF进行描述。注意,这里的数据都是二进制数据,不是ASCII字符。比如 %x0 / %x1代表一位数据,值为0或者1,不是一个ASCII字符中的0或者1(占用一个字节)。占四位的字段%x0-F,表示四个位,并不是四个ASCII字符。在ABNF中,一个字符只是表示一个非负的整数。在某些场景中会指定某些编码值与某些字符集(ASCII)进行映射。每个字段的值都用固定位数的二进制值表示,不同字段的值的长度可能不一样。

 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 message 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 frame
                            / %x3-7
                            ; 4 bits in length,
                            ; reserved for further non-control frames

    frame-opcode-control    = %x8 ; connection close
                            / %x9 ; ping
                            / %xA ; pong
                            / %xB-F ; reserved for further control
                                    ; frames
                                    ; 4 bits in length
                                    
        frame-masked            = %x0
                            ; frame is not masked, no frame-masking-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, or 7+64 bits in length,
                            ; respectively

    frame-payload-length-16 = %x0000-FFFF ; 16 bits in length

    frame-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-unmasked-extension-data
                                frame-unmasked-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

    frame-unmasked-application-data = *( %x00-FF )
                            ; n*8 bits in length, where n >= 0

5.3.客户端的掩码操作

经过掩码的数据帧中的frame-masked标志必须置为1。掩码key会放在frame-masking-key字段随着数据帧一起发送。

Key的值是客户端随机选择的32位的数据。当进行掩码时,客户端需要获取一个新的32位长度的值。这个key必须是不确定的,并且不能让服务器或者代理通过当前的key来猜测到下一个key的值。Key的不确定性可以防止恶意用户构造恶意请求。RFC 4086描述了对于安全比较敏感的应用如何产生比较安全的掩码key。

掩码操作不影响Payload data数据的长度。掩码或者解码获取原数据都可以遵循如下规则。

i表示数据帧的第i个字节,4表示4个字节的mask key。

   j                   = i MOD 4
   transformed-octet-i = original-octet-i XOR masking-key-octet-j

数据帧中frame-payload-length字段表明的数据长度不包括masking key。

5.4.分帧

分帧的目的就是发送未知长度的消息。如果不能分帧,发送端需要缓存整个消息。如果有了分帧,发送端或者中间设备可以随意设置缓存,在缓存满的时候,把数据包装成一个数据帧发送出去。

分帧的第二个作用就是多路复用,如果一个大消息发送时没有分帧的话,会一直占用逻辑通道,这样会影响其他使用通道的应用。

除非设置了扩展标志,否则数据帧没有其他的特殊处理逻辑。客户端和服务器之间如果没有协商某些扩展信息的话,中间设备可以随意组合和拆分数据帧,如果客户端和服务器之间进行了扩展信息的协商,而且中间设备也了解这些扩展信息,中间设备也可以自由的组合与拆分这些数据帧。

分帧的规则如下:

  • 未分帧的消息只包含一个数据帧,数据帧的FIN标志会被设置并且opcode值不为0.

  • 如果消息经过了分帧,第一个分帧的FIN被设置为0,opcode不为0,后续跟着0或者多个数据帧,这些数据帧的FIN为0,并且opcode为0,最后的数据帧FIN标记设置为1,opcode也为0。分帧的数据帧概念上是一个整体的消息,消息的长度就是这些数据帧长度的总和,如果存在扩展数据的话,这个规则可能不会成立,因为扩展数据的位置与解析由扩展信息决定。比如,有的扩展数据可能只在第一个分帧数据帧中,也有可能存在后面的具有扩展标志的数据帧中。先不考虑扩展数据,数据帧的具体操作流程如下:

    比如,对于一个文本数据,第一个数据帧的opcode设置为1,FIN标志位0,第二个数据帧的opcode为0,FIN为0,第三个数据帧的opcode为0,FIN为1。

  • 控制帧可以放在分帧的消息中间。控制帧不能继续分帧。

  • 数据段接收方收到的顺序和发送方发送的顺序必须一致。

  • 两个消息的分帧不能交叉,除非在握手协商过程中说明了如何解析这种交叉分帧。

  • 终端可以处理位于中间的控制帧。

  • 发送方可以发送任意大小的数据分帧。

  • 客户端和服务器必须可以处理经过分帧的和未经过分帧的消息。

  • 因为控制帧不能被分帧,所以中间设备必须不能对控制帧进行分帧。

  • 中间设备如果不能理解预留字段的具体含义不能对数据帧进行进一步操作。

  • 在一个连接中,如果消息的扩展信息在握手协商中确定了具体的意义,但是中间设备不了解这些扩展信息的具体意义,所以不能改变这些消息的分帧信息。同样的,如果中间设备不了解websocekt的握手过程,同样不能更改消息的分帧操作。

  • 根据上述规则,一个消息的所有分帧类型都必须一样,分帧类型只在第一个分帧进行设置。因为控制帧不能分帧,所以经过分帧的消息类型都是文本、二进制或者其他的自定义类型。

注意:如果控制帧不能插入数据段中间,那么在一个非常大的消息后面发送一个ping消息会有很大的延迟。

协议实现注意:如果没有特殊规定的话,接收方不必等到数据接收完成再处理,比如在流处理应用中,可以把部分数据交由应用处理。这种要求可能随着不同的websocket版本的不同而不同。

5.5.控制帧

opcode字段的最高位为1表示控制帧。当前定义的控制帧包括:0x8 (Close), 0x9 (Ping), and 0xA (Pong)。值在0xB-0xF中间的表示预留值。

控制帧用来交流websocket连接状态。控制帧可以放在数据帧中间。

所有的控制帧payload的长度必须为125字节或者比125少,并且都不能分帧。

5.5.1.Close控制帧

opcode标志为0x8。

Close帧可能包含body(帧的数据部分),这个body可能描述了关闭连接的原因,比如服务器宕机、收到一个很大的数据帧、或者收到一个不能识别的数据格式的帧。如果有数据部分,那么前两个字节(网络字节序)为无符号的整数,这个整数代表状态码(Section 7.4中有描述)。随后跟着UTF-8编码的文本。这个文本不一定必须是可以直接阅读,也有可能是与调试有关的数据。因为这个数据不一定方便人类阅读,所以客户端不能把这个数据直接展示给终端用户。

从客户端发送到服务器的Close帧必须按照Section 5.3规则进行掩码。

应用在发送Close帧之后不能再发送任何应用数据。

如果一个终端收到一个close帧,但是之前没有发送过close帧,这个终端应该发送一个close帧(在响应close帧时,终端应该把收到的close帧中的状态值放在自己发送的响应的close帧中)。这个响应的发送时机视具体情况而定。一个终端可能在发送完自己的数据后才会发送close帧。

发送完close帧之后并且也收到对端发送的close帧,终端可以关闭底层的TCP连接。服务器必须马上关闭TCP连接,客户端可以等待服务器关闭TCP连接,也可以在任何时候关闭TCP连接。

如果客户端和服务器同时发送了Close帧,两端都发送并且收到close帧可以认为WebSocket关闭了,并且关闭底层TCP连接。

5.5.2.Ping控制帧

Ping帧的opcode为0x9。

可能包含Application data。

收到Ping帧后,需要立马发送一个Pong帧作为响应,如果已经收到Close帧的话,就不用发送Pong帧。发送Pong的时机视具体情况而定。Pong在下一节描述。

终端可以在连接建立后到关闭前的任何时刻发送Ping帧。

注意:Ping可以用来判断连接是否存活。

5.5.3.Pong控制帧

Ping帧的opcode为0xA。

对于Ping控制帧的要求同样适用于Pong控制帧。

Pong帧中的应用数据应该与Ping中的应用数据一样。

如果终端还没来得及响应之前的Ping帧,又收到一个Ping帧,它可以选择响应最近的一个Ping帧。

一个Pong帧可能在没有接收到Ping帧的情况下发送,这种一般出现在单向心跳的情况下。Pong帧不需要响应。

5.6. 数据帧

opcode字段的最高位为0表示数据帧。目前表示数据帧的为 0x1 (Text), 0x2 (Binary)。opcodes为0x3-0x7为预留值,表示非控制帧。

数据帧包含应用数据或者扩展数据。opcode的值表示数据的解释方式。

Text

数据被编码成UTF-8字符。一个数据帧可能包含一个字符的部分UTF-8编码序列,但是整个消息的编码必须正确。包含非法的UTF-8编码序列的消息处理方法在8.1小节描述。

Binary

应用数据可以是任意的二进制数据,这些数据的具体含义由应用层负责解释。

5.7. 例子

  • 包含一个没有掩码的数据帧的消息,
    1. 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains “Hello”)
  • 包含一个掩码的数据帧的消息,
    1. 0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58(contains “Hello”)
  • 分段的未掩码的文本消息,
    1. 0x01 0x03 0x48 0x65 0x6c (contains “Hel”),
    2. 0x80 0x02 0x6c 0x6f (contains “lo”)
  • 未掩码的Ping请求和掩码的Ping响应,
    1. 0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains a body of “Hello”,but the contents of the body are arbitrary),
    2. 0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (contains a body of “Hello”, matching the body of the ping)
  • 一个包含256字节的二进制的消息未掩码的数据帧。
    1. 0x82 0x7E 0x0100 [256 bytes of binary data]
  • 一个包含64k字节的二进制的消息未掩码的数据帧。
    1. 0x82 0x7F 0x0000000000010000 [65536 bytes of binary data]

5.8. 扩展

协议支持扩展以实现对基础功能的增强。客户端和服务器在握手阶段必须协商扩展数据的具体含义。协议中的opcode的值在 0x3到0x7,0xB 到 0xF表示扩展,同时还提供了"Extension data" 字段、以及frame-rsv1,frame-rsv2,frame-rsv3这三个扩展位。扩展的协商过程在9.1章节进行讨论。

下面是一个可能的扩展用法,这些用法不是完备的并且也不是规范的。

  • 在Payload data中,Extension data可能在Application data之前。
  • 每个帧的预留位可以独立定义。
  • 预留操作码的值可以定义。
  • 如果需要更多的操作码值,可以使用预留位(Reserved bits)进行补充。
  • 可以使用Payload data对操作码或者预留位进行扩展。

6. 发送和接收数据

6.1. 发送数据

在websocket连接上发送消息必须执行如下流程:

  1. 发送端必须确保webcosket连接处于打开状态。如果任何时刻连接状态发生变化,发送端可以不执行如下流程。
  2. 发送端必须把数据包装到websocket帧中(帧格式在5.2章节中有介绍)。如果发送的数据很多或者在发送时不知道数据大小,发送端可以按照5.4章节介绍的过程对数据进行分帧。
  3. 第一个帧必须设置text或者binary类型,这样接收方就可以根据类型来解析数据。
  4. 最后一个发送的帧必须设置Fin为1.
  5. 如果是客户端发送的数据,每个帧必须设置Mask为1.
  6. 如果建立连接时协商了扩展点,应该对每个扩展点都进行详细的考虑。
  7. 生成的帧必须通过网络连接(tcp)进行发送。

6.2. 接收数据

接收端接收到的字节必须解析成5.2章节定义的数据格式。必须按照5.5章节定义的方式处理控制帧。对于数据帧,接收方必须解析数据帧的数据类型(5.2)。如果收到一个没有分帧的数据,那么就可以确认收到一个类型为/type/和数据为/data/的消息。如果收到一个分帧的数据,那么应用数据就是所有后续分帧的/data/组合。当最后一个Fin设置为1的数据帧到达后,那么一个完整的应用数据就接收完毕了,后续的帧就属于新的消息。

扩展设置可以改变数据解析的方式,可能包括消息边界。扩展设置除了可以在应用数据前添加扩展数据外,还有可能会对应用数据进行压缩。

正如5.3章节中描述的一样,服务器收到客户端发送的数据帧时,必须解码。

7.关闭连接

7.1.定义

7.1.1.关闭websocket连接

可以通过关闭底层TCP连接来关闭websocket连接。关闭TCP连接要干脆利落,同时也要关闭相关的TLS会话,如果还有未来得及处理的数据,也需要丢弃这些数据(可能在接收缓存中)。当受到网络攻击时,主机可以通过任何可以使用的方法来关闭连接。

在大部分场景下,底层的TCP连接都是由服务器关闭,这样服务器会保持TIME_WAIT状态一段时间(如果是客户端首先关闭连接,客户端需要保持2MSL时间才能重新打开连接),但是这样对服务器没有什么影响,只要SYN带有更大的seq,服务器可以重新打开这个连接。在非正常情况下(在一段时间后客户端没有服务器发送的Close请求),客户端可以首先发起关闭TCP连接请求。同样地,当服务器被要求关闭websocket连接的时候,服务器应该立马发起连接关闭请求,当客户端被要求关闭连接时,应该等待服务器发送的关闭连接请求。

当使用C编程语言时,在关闭伯克利socket时,我们可以调用shutdown()方法,并且附带SHUT_WR这个参数,之后调用recv()方法,并且等待获取一个值为0的字节,来表示对端也调用了shutdown()方法,最后调用socket的close方法来关闭连接。

7.1.2.发起Websocket关闭握手

关闭websocket连接时需要在Close控制帧中指定一个code值和原因。当一端既发送了Close控制帧,也收到一个Close控制帧,可以按照7.1.1章节介绍的规则关闭TCP连接。

7.1.3.Websocket关闭握手已经开始

只要发送或者收到一个Close控制帧,标志着Websocket关闭握手已经开始,并且Websocket连接已经进入CLOSING状态。

7.1.4.Websocket连接已经关闭

当底层的TCP连接已经关闭,表明Webcosket连接已经关闭,并且进入了CLOSED状态。当TCP连接在Websocket关闭流程完成后关闭,可以说Webcosket连接被优雅地关闭了。

当Webcosket连接不能建立,也可以说Webcosket连接被关闭了,只不过不是优雅地关闭(_ The WebSocket Connection is Closed_, but not _cleanly _)。

7.1.5.Websocket连接关闭状态码

正如5.5.1和7.4章节介绍一样,一个Close控制帧可能会包含一个状态码用来表明关闭连接的理由。Websocket关闭请求可以由任何一端发送,也有可能是同时发送。_The WebSocket Connection Close Code_定义为第一个Close控制帧中包含的并且在7.4章节中定义的状态码。当Close 控制帧没有状态码,_The WebSocket Connection Close Code_被认为是1005.当Websocket连接被关闭了,但是没有收到Close控制帧(底层TCP连接直接关闭),_The WebSocket Connection Close Code_被认为是1006.

注意:连接的两端可能存在_The WebSocket Connection Close Code_数值不一致的情况。比如,远端发送了一个Close控制帧,但是本地没有读取TCP中的数据,也就是没有读取远端发送的Close控制帧信息,本地应用打算关闭连接并且发送了一个Close 控制帧,这样的话,两端都发送并且收到一个Close控制帧,并且后续不会再发送Close控制帧。两端都看到了对面发送的_The WebSocket Connection Close Code_。这样的话,两端可能看到不同的_The WebSocket Connection Close Code_,这种情况是在两端几乎同时开启关闭握手时出现。

7.1.6.Websocket连接关闭原因

在5.5.1和7.4章节讲到,一个Close控制帧可能包含一个状态码用来指示关闭的原因,同时也能包含一个UTF-8编码的数据,这个数据的具体解释方式由接收方的应用处理。_The WebSocket Connection Close Reason_定义为附加在状态码之后的UTF-8编码的数据,这个数据包含在第一个Close控制帧中。如果Close控制帧中没有包含这个数据,那么_The WebSocket Connection Close Reason_被认为是空字符串。

注意:和7.1.5章节描述的一样,两端接收到的关闭原因可能不一样。

7.1.7.Websocket连接失败

某些算法或者规范要求终端可以_Fail the WebSocket Connection_。为了实现这个功能,客户端必须_Close the WebSocket Connection_并且向用户以合适的方式上报错误。同样的,服务器也应该_Close the WebSocket Connection_并且把错误日志打印出来。

如果_The WebSocket Connection is Established_在_Fail the WebSocket Connection_之前执行,终端应该_Fail the WebSocket Connection_这个连接并且向对面发送一个带有状态码的Close控制帧,然后再执行_Close the WebSocket Connection_。如果一个终端已经了解到对面不会处理任何websocket消息了,那么这个终端有可能就不会再发送Close帧,因为Websocket在建立连接的时候有可能就没有建立成功。当终端被命令_Fail the WebSocket Connection_时,它不能继续处理任何从对端发送过来的数据(包括对端发送过来的Close帧)。

除了上述情况或者应用主动关闭Websocket连接,其他情况下不应该关闭websocket连接。

7.2. 异常关闭

7.2.1. 客户端发起的关闭

某些算法,尤其在建立连接的握手阶段,要求客户端可以_Fail the WebSocket Connection_。为了实现这个功能,客户端必须按照7.1.7章节描述的过程去执行相应的步骤。

在任何时刻如果底层的TCP连接丢失了,客户端必须_Fail the WebSocket Connection_。

除了上述情况或者应用主动关闭Websocket连接,其他情况下不应该关闭websocket连接。

7.2.2. 服务端发起的关闭

某些算法,尤其在建立连接的握手阶段,要求服务端可以_Fail the WebSocket Connection_。为了实现这个功能,客户端必须按照7.1.7章节描述的过程去执行相应的步骤。

7.2.3. 从异常关闭中恢复

很多情况都会导致连接异常关闭。常见的是底层链路的连接错误,这种情况下可以重新建立连接。还有其他非链路错误,客户端可能非正常关闭了连接,但是又立刻或者持续地进行重连,服务器可能会经历类似于拒绝式服务攻击,因为很多客户端会尝试进行连接。最后可能会导致服务器无法在短时间内进行恢复,或者恢复过程变得很困难。

为了防止这种情况,客户端在遇到连接非正常关闭的情况下应该采用某种回退机制。

在经过某个随机时间后再进行重连操作。具体的随机算法由客户端去决定,0到5秒可能是一个不错的选择,不过客户端依然可以根据经验或者具体情况去选择重连的回退时间。

如果第一次重连失败,第二次重连时间应该适当增长,比如采用截断二进制指数退避算法(truncated binary exponential backoff)。

7.3. 正常关闭

服务器可以根据情况关闭webcosket连接。客户端不应该随意关闭连接。不管谁关闭连接,都应该遵守7.1.2描述的过程去_Start the WebSocket Closing Handshake_。

7.4. 状态码

终端在关闭已经建立的websocket连接时应该指定关闭的理由。当前规范没有定义接收到关闭理由时应该进行什么样的操作。Close帧可以选择是否记录状态码和相关文本。

7.4.1. 预留状态码

终端在发送Close帧时可以使用以下预留的状态码。

  • 1000

    1000表示正常关闭连接。

  • 1001

    1001表示终端已经“going away”,比如服务器宕机或者浏览器跳转到其他页面。

  • 1002

    1002表示终端由于协议错误终止了连接。

  • 1003

    1003表示终端因为接收到了不能处理的数据类型,所以打算关闭连接(比如终端只理解text数据,但是收到了binary类型的消息)。

  • 1004

    预留。

  • 1005

    1005是一个预留值,表示终端期望收到状态码但是没有收到,不能放在Close帧中。

  • 1006

    预留值,用来表示连接被异常关闭(没有发送或者收到Close帧),不能放在Close帧中。

  • 1007

    1007表示终端关闭了连接,因为发现收到的数据内容和实际的消息类型不匹配。(比如非UTF-8编码的数据放在了text消息中)

  • 1008

    1008表示收到一个不符合规则的消息,并打算关闭连接。这是一个比较通用的状态码,当没有更合适的状态码或者希望隐藏一些具体的细节的时候可以选择使用。

  • 1009

    1009表示收到一个比较大的不能处理的消息。

  • 1010

    用来关闭连接,因为在握手阶段,客户端希望服务器使用多个扩展,但是服务器没有返回相应的扩展信息。客户端在发送Close帧时把希望使用的扩展列表放在/reason/(关闭原因)中。服务器不需要发送这个状态码,因为服务器可以直接执行_ Fail the WebSocket Connection _。

  • 1011

    表示服务器遇到某些不能完成的请求。

  • 1015

    表示不能进行TLS握手的时候发送(比如服务器的证书不能得到验证),是一个预留状态码,不能在Close帧中使用。

7.4.2.预留状态码范围

  • 0-999

    还没有被使用。

  • 1000-2999

    这个范围的状态码被设计为协议预留,在协议未来的扩展或者校订版本中会用到这个范围的状态码。

  • 3000-3999

    这个范围的状态码预留给类库、框架、应用使用(基础应用比如apache http这样的应用或者一些开源框架)。这些状态码在IANA中直接注册。本协议没有规定具体含义。

  • 4000-4999

    这个范围的状态码没有在IANA中注册可以随意使用,具体的意义由websocket应用程序规定。

8.错误处理

8.1.处理关于UTF-8编码的数据异常

如果按照UTF-8格式不能成功解析字节流,终端必须_ Fail the WebSocket Connection _。握手或者后续的传递数据阶段都适用于这个规则。

9.扩展

Websocket客户端可能要求使用协议扩展。服务器可能接受部分或者所有来自客户端的扩展请求。如果客户端没有请求相关的扩展,服务器一定不能响应。如果在客户端和服务器协商阶段指定了部分扩展参数,那么参数的使用必须准守相关的扩展规则。

9.1.协商扩展

客户端使用扩展时可以在请求头中使用|Sec-WebSocket-Extensions| 字段,请求头的name和value规则遵循http请求头的相关规则。本章节使用ABNF规则来定义请求头。如果客户端或者服务器收到不符合ABNF规则的value,可以立刻_ Fail the WebSocket Connection _。

       Sec-WebSocket-Extensions = extension-list
         extension-list = 1#extension
         extension = extension-token *( ";" extension-param )
         extension-token = registered-token
         registered-token = token
         extension-param = token [ "=" (token | quoted-string) ]
             ;When using the quoted-string syntax variant, the value
             ;after quoted-string unescaping MUST conform to the
             ;'token' ABNF.

和其他HTTP头部字段一样,一个header值可以被拆分或者组装成多行。所以如下实例是一样的。

      Sec-WebSocket-Extensions: foo
      Sec-WebSocket-Extensions: bar; baz=2

等于

      Sec-WebSocket-Extensions: foo, bar; baz=2

使用的extension-token必须是已经注册的(在11.4章节介绍)。必须使用与扩展相关的参数。如果服务器没有确认某些扩展,客户端不能使用这些扩展的功能。

扩展项的排列顺序是有特殊意义的。扩展项之间的交互顺序都在定义他们的文档中有相关描述。如果没有相关文档进行定义,在客户端请求头部中列出的扩展项的顺序,就是它期望的顺序。服务器返回的扩展项的顺序是实际使用的顺序。扩展项处理数据的顺序就是按照他们在服务器握手响应头部中出现的顺序。

如果服务器返回的头部信息中|Sec-WebSocket-Extensions|中有“foo”和“bar”两个扩展项,那么对于数据的处理过程就是 bar(foo(data))。如果数据是分帧接收的话,那么把这个帧组装后再进行处理。

服务器一个不规范的扩展头部实例:

         Sec-WebSocket-Extensions: deflate-stream
         Sec-WebSocket-Extensions: mux; max-channels=4; flow-control,
          deflate-stream
         Sec-WebSocket-Extensions: private-extension

服务器通过|Sec-WebSocket-Extensions|头包含一个或者多个客户端发送过来的扩展项。服务器返回的扩展参数,以及返回什么样的值,都由具体的扩展项定义。

9.2.已知扩展

扩展可以为协议提供一些新的功能。本文档不描述任何扩展相关的信息。协议的实现可能会描述使用的相关扩展项.

10.安全方面的考虑

本章节讨论Websocket协议关于安全方面的点。特殊方面的安全点在后续的章节有讲述。

10.1.非浏览器客户端

Websocket在受信任的应用里运行的时候(比如浏览器)可以防止恶意JavaScript脚本的运行,比如检查|Origin|字段。查看1.6章节来了解更详细的内容。这个假设在其他类型的客户端中不成立。

然而这个协议本身就是为了让在网页中的脚本语言使用的,当然也可以被其他主机应用使用,所以这些主机可能随意的发送|Origin|,这样有可能会迷惑服务器。服务器应该谨慎的去与客户端交流,不能单纯的认为对面就是已知主机上的脚本。所以,服务器不能认为客户端发送的都是合法的。

举例:如果服务器使用前端传过来的sql进行数据库查询,所有的sql文本应该经过转义再发送到数据库,以免服务器遭受sql注入攻击。

10.2.Origin注意事项

服务器如果不是接收所有主机的请求的话,应该检查请求头中|Origin|的值。如果服务器不接受|Origin|主机的请求,应该在握手阶段返回一个http 403相应。

|Origin|可以防止恶意代码在受保护的应用中运行时发起的某些攻击。客户端可以通过|Origin|机制来确定是否需要为脚本程序授予通讯权限。这种机制不是防止非浏览器应用去建立Websocket连接,而是防止恶意的JavaScript发送一个虚拟的Websocket握手请求。

10.3.基础设施的攻击(Masking,掩码)

Websocket除了会攻击终端设备以外,还会攻击网络基础设施,比如代理服务器。

在协议的过程中,有一项实验用来模拟一类代理服务器攻击,这类攻击会污染一部分透明代理类(基于ip做转发的,不是普通的http代理服务器,比如带有http缓存功能的网关或者网桥设备,可以参考Talking to Yourself for Fun and Profit 中文版)。

为了防止中间代理服务器被攻击,现在对每一个客户端发送的数据进行掩码操作,这样攻击者就不能伪造http请求来攻击中间代理服务器了。

客户端必须为每个数据帧选择一个新的掩码key,不能预测下一个key的算法最安全。如果客户端使用了一个可以判断下一个key的算法,那么攻击者可能会使用某个数据,在与mask key进行掩码后刚好就是http请求,这样同样会对中间代理服务器进行缓存投毒。

如果数据帧已经在发送过程中,那么就不能再对数据进行修改。否则,攻击者的脚本可能在开始发送的时候会写一串0的数据,然后边发送边修改数据帧的值,比如改成http请求。

我们假定的攻击模型是客户端在发送数据时发送一个http请求,所以需要掩码的就是客户端发送到服务器的数据。服务器到客户端的数据可以看成是一个响应,所以没有必要对服务器的响应数据进行掩码。

10.4.实现方面的一些限制

应该对消息的大小进行限制,来避免攻击者会发送一个很大的消息或者分段的数据帧组合后出现很大的消息(2^60),这样容易使对面的服务器内存耗尽或者出现拒绝服务。

10.5.客户端认证

本协议不描述关于服务器如何认证客户端的相关规则。websocket服务器可以使用当前http服务器普遍使用的认证方式,比如cookies、http 认证、或者tls认证。

10.6.连接的保密性和完整性

连接的保密性和完整性通过使用TLS协议来实现。websocket协议的实现必须支持TLS。
使用TLS的连接,保密程度完全取决于在TLS握手阶段协商的加密算法是否够强。为了达到期望的安全程度,客户端应该使用更强的TLS算法。Web Security Context: User Interface Guidelines描述相关算法。

10.7.处理无效数据

客户端和服务器必须对接收到的数据进行检查。当终端接收到不能理解的或者接收到的数据违反了终端制定的安全准则再或者握手过程中遇到了之前没有交流过的某些值,终端可以直接关闭TCP连接。如果在握手成功之后,收到了无效的数据,终端应该发送一个Close帧和一个状态码,之后再执行_ Close the WebSocket Connection _流程。Close帧中写入状态码可以方便排查问题。如果在握手阶段接收到了无效数据,服务器可以返回Http相关状态码。

在发送文本数据的时候使用了错误的编码可能会引发一些安全方面的问题。本协议中的文本都是UTF-8编码。尽管协议指定了数据的长度,并且应用按照这个长度去读取消息,但是发送没有正确编码的数据仍然可能会导致数据丢失或者一些潜在的安全问题。

10.8.握手阶段使用SHA-1

这里描述的握手过程不依赖SHA-1相关的安全属性。

翻译自:https://tools.ietf.org/html/rfc6455

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章