Websocket协议能在受控的环境内实现浏览器与服务器之间的双向通讯(浏览器中的应用可能是不可靠的,但是仍然可以与服务器建立websocket连接),使得浏览器应用与服务器进行双向通讯时不必同时打开多个HTTP连接(使用XMLHttpRequest 、iframe 或者长轮询实现双向通讯时经常会打开多个连接)。Websocket位于TCP之上(位于应用层),主要包括握手过程、数据传输两个主要部分。
本部分为非权威描述
历史上,web应用(即时通讯或者游戏)为了实现与服务器的双向通讯,一般会建立一个发送消息的http连接与一个接收消息的http连接。 这样会导致几个问题:
Websocket协议设计的目标是使用一个连接实现客户端与服务器之间的通讯以此替代http长轮训。Websocket基于http实现双向通讯可以从当前基础设施(代理、过滤、认证)中获得更多支持。 Websocket基于http协议的80和443端口工作,即使这样会增加协议的复杂度。 当然,我们在设计Websocket时没有限制必须基于http协议,未来也许会单独开一个端口,然后使用更简单的握手过程从而替换掉底层依赖的http协议。
本部分为非权威描述
协议主要分为两部分:握手(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编码的字符)、二进制(具体解析方式由程序定义)、控制(用作控制作用,比如管理连接的打开和关闭)。这个版本的协议定义了六种数据类型,并且预留十种类型。
本部分为非权威描述
握手阶段主要用来兼容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进行连接,然后生成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|字段中并响应客户端请求。
服务器返回的握手信息很简单,第一行为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
本部分为非权威描述
挥手过程要比打开过程简单的多。 任何一端都可以发送一个Close帧来开始挥手过程,Close帧可能带有部分数据(比如描述关闭的原因以及状态码)。任何一端收到一个Close帧,如果之前没有回复过的话,需要发送Close帧。主动关闭的一端在收到对端返回的响应后,在确定没有数据需要继续接收之后,开始关闭底层连接(shutdown)。
在发送Close帧之后不应该发送任何数据帧,在收到对端发送过来的Close帧后,对于后续的数据,接收端不予处理。
两端可以同时发送挥手控制帧。
挥手控制帧用来关闭两端之间的tcp连接,因为有时两端之间并不是直接相连,中间有可能会有代理或者其他中间设备。
发送一个挥手控制帧然后等待响应可以防止某些情况下丢失数据。比如在某些软件平台,如果socket一端在接收队列里还有未处理的数据,但是关闭了连接,这时会向对端发送一个rst消息,对端在收到rst消息后会让在recv()监听的线程收到函数返回的错误信息,即使当时接收队列里还有数据(因为两端都没有成功处理消息,所以两端需要对类似的rst错误进行处理)。
本部分为非权威描述
Websocket协议应该尽量减少使用帧相关的概念(只有在描述协议是基于帧的而不是基于流的时候与用来区分文本帧和二进制帧的时候会涉及到帧)。应用层在websocket层之上,所发送的数据都会经过websocket这一层去传递,这一点与http使用tcp去发送数据大致相同。
概念上讲,websocket就是基于tcp的协议,拥有以下功能:
除了上面列出的功能外,websocket没有增加其他功能。考虑到web浏览器的限制,尽可能的只把原生的tcp接口暴露给脚本去调用。如果客户端发送过来的是合法的http升级请求,那么websocket服务器可以与http服务器共享一个端口。有人可能会使用其他协议来实现客户端和服务器之间的消息通讯,但是websocket协议的设计初衷就是提供一个相对简单的协议来与http协议或者http基础设施(代理)共存,websocket提供的长连接使得经过这些基础设施时与直接使用tcp一样安全,并且通过增加一些附属功能来简化使用的方式。
协议具有扩展性,未来版本可能会新增其他的功能(概念),比如多路复用。
本部分为非权威描述
Websocket协议使用与web浏览器一样的安全模型(origin-based)来控制应用可以与哪些服务器建立连接。如果在一个专用的客户端中使用websocket协议,这个安全模型就显得没有必要了,因为客户端可以提供任何可能的origin值。
运行[SMTP]与HTTP协议的服务器不会与websocket客户端建立连接,但是如果HTTP服务器支持升级到websocket协议则可以建立连接。为了保证协议的正确性,在握手没有完成之前不可以发送应用数据。
如果websocket服务器接收到了其他协议的(主要指http协议数据)数据,那么整个连接会被关闭。Websocket在握手阶段会使用专用的头部字段,服务器可以通过验证这些专用的头部字段来保证握手过程的合法性,在这个规范编写的时候,网络攻击者不会在网页应用(html和js)中使用XMLHttpRequest发送带有|Sec-|前缀的头部字段。
本部分为非权威描述
Websocket是基于TCP独立设计的协议。与HTTP的唯一关系是,websocket的握手协议是通过HTTP的协议升级实现的。
默认情况下,普通websocket连接使用80端口,安全的websocket连接使用443端口,基于TLS安全层。
本部分为非权威描述
向一个既支持websocket协议又支持http协议的服务器发送websocket请求时,应该使用传统GET请求,并且带有Upgrade头部字段。在简单的部署场景中,一台服务器可以同时支持websocket与http协议。在一些复杂的场景中(多机部署以及负载均衡部署),websocket服务器和http服务器分开部署易于管理。在编写规范的时候,80端口和443端口的连接成功率不一样,443的成功率要高点,这个可能随着时间的变化会有所变化。
本部分为非权威描述
客户端可以在握手请求中添加|Sec-WebSocket-Protocol|字段要求服务器从中选择一个支持的子协议。服务器在握手响应中需要包含这个字段并且选择支持的子协议。
子协议按照[Section 11.5]规定注册名字。为了避免名字冲突,协议名字应该包含子协议指定方的主机名字,并且主机名字是ascii编码格式的。举个例子,比如Example公司打算创建一个chat子协议,这个协议可能会被网络上的很多服务器使用,那么这个子协议的名字可以命名chat.example.com。如果Example组织也创建了自己的子协议名称,命名为chat.example.org,那么多个服务器可以同时实现,并且在握手过程中从客户端提供的子协议列表中选择一个。
子协议为了实现向后兼容可以更改名称,比如bookings.example.net改成v2.bookings.example.net。这些协议可以被客户端轻松的分辨出来。重用相同的子协议名字也可以实现向后兼容,但是这么做的时候需要认真设计子协议(比如通过其他扩展字段实现版本化来支持向后兼容)。
如果有的图表、示例、注释在这个文档中标记为非规范的,表示非标准规范,没有显式标记的都是规范标准。
文本中的关键字,“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”)。
规范如果被表述为某些算法或者某些规定的步骤,那么他们的实现方式可能是多样性的,但是如果他们的结果是一样的,那就是可以接受的(特别的,本规范里规定的算法都很简单,并且方便实现。)。
_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]扩展语法规则。
本规范使用了两种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(大小写不敏感)的话,那么这个连接是安全连接。
资源名字可以由一下几部分组成:
段落标识符在websocket uri中没有意义,在所有的URI框架中,如果#不表示段落的开始,那么应该使用%23进行转义。
客户端与服务器成功建立socket连接之后会发送websocket握手信息。Websocket在开始阶段处于CONNECTING状态。客户端需要提供/host/, /port/, /resource name/和 /secure/,具体含义在第三章中有描述,除了这些参数之外有可能会提供/protocols/ 和 /extensions/列表,如果客户端是浏览器,还需要添加/origin/。
便携式设备中的浏览器访问网络时可能会通过某些代理软件,所以,本规范的客户端包括了便携式设备中的浏览器软件和代理软件。
使用(/host/, /port/, /resource name/和 /secure/ )、/protocols/ 、 /extensions/、/origin/(如果客户端是浏览器)来与服务器建立连接,然后发送握手请求,等待服务器的响应。具体的如何打开连接,如何发送握手请求,服务器如何对握手请求进行回应,都会在下文进行描述。后续我们会使用第三章中规定的单词进行讨论(比如/host/与/secure/标识符)。
CONNECT example.com:80 HTTP/1.1
Host:example.com
如果有密码字段,则可以发送如下执行:
CONNECTexample.com:80 HTTP/1.1
Host:example.com
Proxy-authorization: Basic ZWRuYW1vZGU6bm9jYXBlcyE=
连接建立成功之后,客户端向服务器发送websocket握手消息。握手消息中包括一个HTTP Upgrade以及一系列必须的或者可选的头部字段。具体的要求信息如下:
一旦客户端发送了握手请求,客户端必须等待服务器的响应,在获取响应前不应该发送任何数据。客户端也应该按照如下规则检验服务器的响应信息。
如果服务器响应不符合[Section 4.2.2]规定的要求,客户端必须_Fail the WebSocket Connection_。
根据[RFC2616]中的规定,http中的所有请求字段和响应字段都是大小写不敏感的。
如果服务器响应通过上述验证规则,那么websocket连接就进入打开状态。当前使用的协议扩展是服务器响应头部返回的|Sec-WebSocket-Extensions|字段的值,如果响应中没有指定协议扩展,那么当前连接就没有使用协议扩展。正在使用的子协议也是使用服务器返回的|Sec-WebSocket-Protocol|字段中的值,如果响应中没有指定子协议,那么当前连接就没有使用子协议。除此之外,如果在握手阶段服务器要求设置cookies[RFC6265],那么cookies就是在握手阶段设置的cookies。
服务器可能使用网络代理管理连接(比如负载均衡服务器或者反向代理)。在这种场景中,协议指定的服务器包括了服务端的所有基础设施,从接受tcp连接的设施到处理客户端请求的设施。
4.2.1.读取客户端的握手请求
客户端的握手请求包含如下几部分。如果服务器在读取客户端的握手请求时发现客户端没有发送协议指定的字段,并且字段的名字或者值不符合ABNF规定的,服务器可以直接不处理这个握手请求,并且返回错误码(400 Bad Request)。
4.2.2.发送服务器的握手响应
当客户端与服务器建立websocket连接时,服务器必须执行如下流程来接收这个连接,并且返回响应。
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状态,之后的数据就可以在这个连接上进行传递。
本部分使用了在 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
本部分给出了实现客户端和服务器之间支持多个版本的指导意见。
客户端通过|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
在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值预留给未来扩展使用。客户端和服务器建立连接后如果没有关闭连接的话,可以双向发送数据帧。
数据传输的格式通过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 ... |
+---------------------------------------------------------------+
* %x0 表示后续还有数据帧
* %x1 表示文本帧
* %x2 表示二进制帧
* %x3-7 预留
* %x8 表示连接关闭
* %x9 ping
* %xA pong
* %xB-F 预留
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(应用数据)的长度。
Payload data是由Extension data" 与 "Application data"组成。
如果在协商阶段没有规定扩展数据的话,这个长度为0。如果协商阶段说明了扩展数据,扩展数据的长度必须标明,如果没有标明长度,也要在协商的过程中说明如何计算扩展数据长度并且也同时要说明如何使用这些扩展数据。
应用数据紧随扩展数据之后,应用数据的长度为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
经过掩码的数据帧中的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.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帧。
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帧。
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帧不需要响应。
opcode字段的最高位为0表示数据帧。目前表示数据帧的为 0x1 (Text), 0x2 (Binary)。opcodes为0x3-0x7为预留值,表示非控制帧。
数据帧包含应用数据或者扩展数据。opcode的值表示数据的解释方式。
Text
数据被编码成UTF-8字符。一个数据帧可能包含一个字符的部分UTF-8编码序列,但是整个消息的编码必须正确。包含非法的UTF-8编码序列的消息处理方法在8.1小节描述。
Binary
应用数据可以是任意的二进制数据,这些数据的具体含义由应用层负责解释。
协议支持扩展以实现对基础功能的增强。客户端和服务器在握手阶段必须协商扩展数据的具体含义。协议中的opcode的值在 0x3到0x7,0xB 到 0xF表示扩展,同时还提供了"Extension data" 字段、以及frame-rsv1,frame-rsv2,frame-rsv3这三个扩展位。扩展的协商过程在9.1章节进行讨论。
下面是一个可能的扩展用法,这些用法不是完备的并且也不是规范的。
在websocket连接上发送消息必须执行如下流程:
接收端接收到的字节必须解析成5.2章节定义的数据格式。必须按照5.5章节定义的方式处理控制帧。对于数据帧,接收方必须解析数据帧的数据类型(5.2)。如果收到一个没有分帧的数据,那么就可以确认收到一个类型为/type/和数据为/data/的消息。如果收到一个分帧的数据,那么应用数据就是所有后续分帧的/data/组合。当最后一个Fin设置为1的数据帧到达后,那么一个完整的应用数据就接收完毕了,后续的帧就属于新的消息。
扩展设置可以改变数据解析的方式,可能包括消息边界。扩展设置除了可以在应用数据前添加扩展数据外,还有可能会对应用数据进行压缩。
正如5.3章节中描述的一样,服务器收到客户端发送的数据帧时,必须解码。
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.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)。
服务器可以根据情况关闭webcosket连接。客户端不应该随意关闭连接。不管谁关闭连接,都应该遵守7.1.2描述的过程去_Start the WebSocket Closing Handshake_。
终端在关闭已经建立的websocket连接时应该指定关闭的理由。当前规范没有定义接收到关闭理由时应该进行什么样的操作。Close帧可以选择是否记录状态码和相关文本。
终端在发送Close帧时可以使用以下预留的状态码。
7.4.2.预留状态码范围
如果按照UTF-8格式不能成功解析字节流,终端必须_ Fail the WebSocket Connection _。握手或者后续的传递数据阶段都适用于这个规则。
Websocket客户端可能要求使用协议扩展。服务器可能接受部分或者所有来自客户端的扩展请求。如果客户端没有请求相关的扩展,服务器一定不能响应。如果在客户端和服务器协商阶段指定了部分扩展参数,那么参数的使用必须准守相关的扩展规则。
客户端使用扩展时可以在请求头中使用|Sec-WebSocket-Extensions| 字段,请求头的name和value规则遵循http请求头的相关规则。本章节使用ABNF规则来定义请求头。如果客户端或者服务器收到不符合ABNF规则的value,可以立刻_ Fail the WebSocket Connection _。
和其他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|头包含一个或者多个客户端发送过来的扩展项。服务器返回的扩展参数,以及返回什么样的值,都由具体的扩展项定义。
扩展可以为协议提供一些新的功能。本文档不描述任何扩展相关的信息。协议的实现可能会描述使用的相关扩展项.
本章节讨论Websocket协议关于安全方面的点。特殊方面的安全点在后续的章节有讲述。
Websocket在受信任的应用里运行的时候(比如浏览器)可以防止恶意JavaScript脚本的运行,比如检查|Origin|字段。查看1.6章节来了解更详细的内容。这个假设在其他类型的客户端中不成立。
然而这个协议本身就是为了让在网页中的脚本语言使用的,当然也可以被其他主机应用使用,所以这些主机可能随意的发送|Origin|,这样有可能会迷惑服务器。服务器应该谨慎的去与客户端交流,不能单纯的认为对面就是已知主机上的脚本。所以,服务器不能认为客户端发送的都是合法的。
举例:如果服务器使用前端传过来的sql进行数据库查询,所有的sql文本应该经过转义再发送到数据库,以免服务器遭受sql注入攻击。
服务器如果不是接收所有主机的请求的话,应该检查请求头中|Origin|的值。如果服务器不接受|Origin|主机的请求,应该在握手阶段返回一个http 403相应。
|Origin|可以防止恶意代码在受保护的应用中运行时发起的某些攻击。客户端可以通过|Origin|机制来确定是否需要为脚本程序授予通讯权限。这种机制不是防止非浏览器应用去建立Websocket连接,而是防止恶意的JavaScript发送一个虚拟的Websocket握手请求。
Websocket除了会攻击终端设备以外,还会攻击网络基础设施,比如代理服务器。
在协议的过程中,有一项实验用来模拟一类代理服务器攻击,这类攻击会污染一部分透明代理类(基于ip做转发的,不是普通的http代理服务器,比如带有http缓存功能的网关或者网桥设备,可以参考Talking to Yourself for Fun and Profit 中文版)。
为了防止中间代理服务器被攻击,现在对每一个客户端发送的数据进行掩码操作,这样攻击者就不能伪造http请求来攻击中间代理服务器了。
客户端必须为每个数据帧选择一个新的掩码key,不能预测下一个key的算法最安全。如果客户端使用了一个可以判断下一个key的算法,那么攻击者可能会使用某个数据,在与mask key进行掩码后刚好就是http请求,这样同样会对中间代理服务器进行缓存投毒。
如果数据帧已经在发送过程中,那么就不能再对数据进行修改。否则,攻击者的脚本可能在开始发送的时候会写一串0的数据,然后边发送边修改数据帧的值,比如改成http请求。
我们假定的攻击模型是客户端在发送数据时发送一个http请求,所以需要掩码的就是客户端发送到服务器的数据。服务器到客户端的数据可以看成是一个响应,所以没有必要对服务器的响应数据进行掩码。
应该对消息的大小进行限制,来避免攻击者会发送一个很大的消息或者分段的数据帧组合后出现很大的消息(2^60),这样容易使对面的服务器内存耗尽或者出现拒绝服务。
本协议不描述关于服务器如何认证客户端的相关规则。websocket服务器可以使用当前http服务器普遍使用的认证方式,比如cookies、http 认证、或者tls认证。
连接的保密性和完整性通过使用TLS协议来实现。websocket协议的实现必须支持TLS。
使用TLS的连接,保密程度完全取决于在TLS握手阶段协商的加密算法是否够强。为了达到期望的安全程度,客户端应该使用更强的TLS算法。Web Security Context: User Interface Guidelines描述相关算法。
客户端和服务器必须对接收到的数据进行检查。当终端接收到不能理解的或者接收到的数据违反了终端制定的安全准则再或者握手过程中遇到了之前没有交流过的某些值,终端可以直接关闭TCP连接。如果在握手成功之后,收到了无效的数据,终端应该发送一个Close帧和一个状态码,之后再执行_ Close the WebSocket Connection _流程。Close帧中写入状态码可以方便排查问题。如果在握手阶段接收到了无效数据,服务器可以返回Http相关状态码。
在发送文本数据的时候使用了错误的编码可能会引发一些安全方面的问题。本协议中的文本都是UTF-8编码。尽管协议指定了数据的长度,并且应用按照这个长度去读取消息,但是发送没有正确编码的数据仍然可能会导致数据丢失或者一些潜在的安全问题。
这里描述的握手过程不依赖SHA-1相关的安全属性。

