WebSocket协议
简介
该协议为了提供一种基于浏览器与服务器进行双向通信的应用程序,不依赖于打开多个HTTP连接。
第一章
1.1
在WebSocket之前,创建一个客户端和服务端的双向数据Web应用(例如IM应用和游戏应用)需要向服务端频繁发送不同于一般HTTP请求的HTTP轮询请求来从服务端上游更新数据。
这个方法有许多的问题:
服务端*使用大量的的潜在的TCP连接与客户端进行交互:一部分是用来发送数据,而另一部分是用来接收数据。
应用层无线传输协议(HTTP)开销较大,每一个客户端到服务端的消息都有一个HTTP头。
客户端脚本必须包含一个发送和接收对应的映射表来进行对应数据处理。
一个简单的解决方案是使用一个简单的TCP链接来进行双向数据传输。这就是WebSocket提供的能力。结合WebSocket的API,它能够提供一个可以替代HTTP轮询的方法来满足Web页面和远端服务器的双向数据通信。
相同的技术可以被用到许多的Web应用:游戏、股票应用、多人协作应用、与后端服务实时交互的用户接口等。
HTTP协议最初也不是用来做双向数据通信的。WebSocket协议尝试实现基于现有的HTTP基础服务来实现在现有环境中双向通信技术的目标;它在设计中仍然使用了HTTP的80和443端口,以及支持HTTP代理。然而,这个设计并没有限制WebSocket只能使用HTTP端口,在以后的实现中也可以使用一个简单的握手方式来使用特定的端口而不需要改动整个协议。最后一点很重要,因为双向消息的通信方式不是很符合标准HTTP的模式,可能导致在某些组件中出现异常的负载。
1.2 协议概览
这个协议有两个部分:握手和数据传输。
来自客户端的握手数据如下所示:
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
客户端请求的第一行(leading line)遵循了HTTP请求行的格式。
服务端的第一行(leading line)遵循了HTTP状态行的格式。
1.3 开始握手
开始握手为了与基于http的服务端软件和中介兼容,因此一个独立的端口既能够同时满足HTTP客户端与服务器进行交互,又能够满足WebSocket客户端与服务进行交互。最终,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
客户端在每一个握手的Hostheader里面包含了一个主机域名。所以客户端和服务端都可以校验哪些域名在使用中。
另外的header字段是用来确定WebSocket协议的选项。这个版本中提供的特定选项是子协议选择(Sec-WebSocket-Protocol)、客户端支持的扩展列表(Sec-WebSocket-Extensions)、Originheader字段等。请求header字段Sec-WebSocket-Protocol可以用来标识哪些子协议(基于WebSocket的应用高层协议)是客户端可以支持的。服务端会从中选择零个或者一个支持的协议并且在响应握手中输出它选择的那个协议。
1.4 结束握手
结束握手远比连接握手简单。
任何一端都可以发送一个包含特定关闭握手的控制帧数据。收到此帧后,另一端在不发送任何数据后会发送一个结束帧作为响应。收到另一端的结束帧后,最开始发送控制帧的端在没有数据需要发送时,就会安全的关闭此连接。
在发送了一个表明连接需要被关闭的控制帧后,这个客户端不会再发送任何的数据;在收到一个表明连接需要被关闭的控制帧后,这个客户端会丢弃此后的所有数据。
1.5 设计哲学
WebSocket协议设计的原理,将框架最小化,对框架的唯一的约束就是使这个协议是基于帧而不是流并且可以支持Unicode文本和二进制帧两者中的任意一种。
从概念上来看,WebSocket层是基于TCP实现的,增加了以下的内容:
- 增加了一个基于浏览器的同源策略模型
- 增加了一个地址和协议命名机制用以在同一个端口上支持多个服务,在同一个IP地址自持多个主机名
- 在TCP协议上分层构建框架机制回到TCP使用的IP包机制,但是没有长度限制
- 包含一个设计用于处理有代理和其他网络中介的情况的额外的结束握手协议
除此之外,WebSocket没有增加任何东西。基本上WebSocket的的目标是在约束的条件下向脚本提供尽可能接近原生的TCP的Web服务。它同时考虑了服务器在进行握手和处理有效的HTTP升级请求时,可以和HTTP共用一个服务。大家也可以使用其他协议来建立从客户端到服务端的消息通信,但WebSocket的协议的目的是为了提供一个相对简单的可以和HTTP共存,并且依赖于HTTP基础设施(如代理)的协议。这个非常接近TCP的协议因为基于安全的基础设施和针对性的能够简单使用和让事情变得更简单的补充(例如消息语义的补充),因此可以安全使用。
1.6 安全模型
1.7 与TCP和HTTP的关系
WebSocket协议是独立的基于TCP的协议。他和HTTP的唯一关系是建立连接的握手操作的升级请求是基于HTTP服务器的。
WebSocket默认使用80端口进行连接,而基于TLS(RFC2818)的WebSocket连接是基于443端口的。
1.8 建立连接
1.9 使用WebSocket协议的子协议
第三章
WebSocket URIs
ws-URI = ws:
//
host [ :
port ] path [ ?
query ]
wss-URI = wss:
//
host [ :
port ] path [ ?
query ]
端口字段是可选的,默认的ws
端口是80,而默认的wss
端口是443。
命中不论大小写的wss
方案字段就表明这个URI可以被称为安全的(已经设置安全标记)。
resource-name
字段(在4.1节也被称为/resouce name/字段)可以通过以下的数据串联起来:
/
,表示路径(path)字段为空
路径(path)字段?
,表示非空的查询参数(query)
空查询参数(query)
在WebSocket URIs的里,身份标识片段是没有意义的,而且禁止使用在这些URI里面。与任何的URI方案一样,#
字符不是表示片段(fragment)开始时,都必须编码为%23。
第四章 握手
客户端要求
第五章 数据帧
5.1 概览
在WebSocket协议中,数据是通过一系列数据帧来进行传输的。为了避免由于网络中介(例如一些拦截代理)或者一些在第10.3节讨论的安全原因,客户端必须在它发送到服务器的所有帧中添加掩码(Mask)(具体细节见5.3节)。(注意:无论WebSocket协议是否使用了TLS,帧都需要添加掩码)。服务端收到没有添加掩码的数据帧以后,必须立即关闭连接。在这种情况下,服务端可以发送一个在7.4.1节定义的状态码为1002(协议错误)的关闭帧。服务端禁止在发送数据帧给客户端时添加掩码。客户端如果收到了一个添加了掩码的帧,必须立即关闭连接。在这种情况下,它可以使用第7.4.1节定义的1002(协议错误)状态码。(这些规则可能会在将来的规范中放开)。
一个数据帧可以在开始握手完成之后和终端发送了一个关闭帧之前的任意一个时间通过客户端或者服务端进行传输(第5.5.1节)。
5.2 基础帧协议
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
? 表示这是消息的最后一个片段。第一个片段也有可能是最后一个片段。值为1代表最后一个片段
RSV1,RSV2,RSV3: 每个1 bit
? 必须设置为0,除非扩展了非0值含义的扩展。如果收到了一个非0值但是没有扩展任何非0值的含义,接收终端必须断开WebSocket连接。
Opcode: 4 bit
? 定义“有效负载数据”的解释。如果收到一个未知的操作码,接收终端必须断开WebSocket连接。下面的值是被定义过的。
? %x0 表示一个持续帧
? %x1 表示一个文本帧
? %x2 表示一个二进制帧
? %x3-7 预留给以后的非控制帧
? %x8 表示一个连接关闭包
? %x9 表示一个ping包
? %xA 表示一个pong包
? %xB-F 预留给以后的控制帧
Mask: 1 bit
? mask标志位,定义“有效负载数据”是否添加掩码。如果设置为1,那么掩码的键值存在于Masking-Key中,根据5.3节描述,这个一般用于解码“有效负载数据”。所有的从客户端发送到服务端的帧都需要设置这个bit位为1。
Payload length: 7 bits, 7+16 bits, or 7+64 bits
? 以字节为单位的“有效负载数据”长度,如果值为0-125,那么就表示负载数据的长度。如果是126,那么接下来的2个bytes解释为16bit的无符号整形作为负载数据的长度。如果是127,那么接下来的8个bytes解释为一个64bit的无符号整形(最高位的bit必须为0)作为负载数据的长度。多字节长度量以网络字节顺序表示(译注:应该是指大端序和小端序)。在所有的示例中,长度值必须使用最小字节数来进行编码,例如:长度为124字节的字符串不可用使用序列126,0,124进行编码。有效负载长度是指“扩展数据”+“应用数据”的长度。“扩展数据”的长度可能为0,那么有效负载长度就是“应用数据”的长度。
Masking-Key: 0 or 4 bytes
? 所有从客户端发往服务端的数据帧都已经与一个包含在这一帧中的32 bit的掩码进行过了运算。如果mask标志位(1 bit)为1,那么这个字段存在,如果标志位为0,那么这个字段不存在。在5.3节中会介绍更多关于客户端到服务端增加掩码的信息。
Payload data: (x+y) bytes
? “有效负载数据”是指“扩展数据”和“应用数据”。
Extension data: x bytes
? 除非协商过扩展,否则“扩展数据”长度为0 bytes。在握手协议中,任何扩展都必须指定“扩展数据”的长度,这个长度如何进行计算,以及这个扩展如何使用。如果存在扩展,那么这个“扩展数据”包含在总的有效负载长度中。
Application data: y bytes
? 任意的“应用数据”,占用“扩展数据”后面的剩余所有字段。“应用数据”的长度等于有效负载长度减去“扩展应用”长度。
5.3 客户端到服务端添加掩码
添加掩码的数据帧必须像5.2节定义的一样,设置frame-masked字段为1。
掩码值像第5.2节说到的完全包含在帧中的frame-masking-key上。它是用于对定义在同一节中定义的帧负载数据Payload data字段中的包含Extension data和Application data的数据进行添加掩码。
掩码字段是一个由客户端随机选择的32bit的值。当准备掩码帧时,客户端必须从允许的32bit值中选择一个新的掩码值。掩码值必须是不可被预测的;因此,掩码必须来自强大的熵源(entropy),并且给定的掩码不能让服务器或者代理能够很容易的预测到后续帧。掩码的不可预测性对于预防恶意应用作者在网上暴露相关的字节数据至关重要。RFC 4086讨论了安全敏感的应用需要一个什么样的合适的强大的熵源。
掩码不影响Payload data的长度。进行掩码的数据转换为非掩码数据,或者反过来,根据下面的算法即可。这个同样的算法适用于任意操作方向的转换,例如:对数据进行掩码操作和对数据进行反掩码操作所涉及的步骤是相同的。
表示转换后数据的八位字节的i(transformed-octet-i )是表示的原始数据的i(original-octet-i)与索引i模4得到的掩码值(masking-key-octet-j)经过异或操作(XOR)得到的:
j = i MOD 4
transfromed-octed-i = original-octet-i XOR masking-key-octet-j
5.4 消息分片
5.5 控制帧
控制帧是通过操作码最高位的值为1来进行区分的。当前已经定义的控制帧操作码包括0x8(关闭),0x9(心跳Ping)和0xA(心跳Pong)。操作码0xB-0xF没有被定义,当前被保留下来做为以后的控制帧。
控制帧是用于WebSocket的通信状态的。控制帧可以被插入到消息片段中进行传输。
所有的控制帧必须有一个126字节或者更小的负载长度,并且不能被分片。
5.5.1 关闭
控制帧的操作码值是0x8。
关闭帧可能包含内容(body)(帧的“应用数据”部分)来表明连接关闭的原因,例如终端的断开,或者是终端收到了一个太大的帧,或者是终端收到了一个不符合预期的格式的内容。如果这个内容存在,内容的前两个字节必须是一个无符号整型(按照网络字节序)来代表在7.4节中定义的状态码。跟在这两个整型字节之后的可以是UTF-8编码的的数据值(原因),数据值的定义不在此文档中。数据值不一定是要人可以读懂的,但是必须对于调试有帮助,或者能传递有关于当前打开的这条连接有关联的信息。数据值不保证人一定可以读懂,所以不能把这些展示给终端用户。
从客户端发送给服务端的控制帧必须添加掩码,具体见5.3节。
应用禁止在发送了关闭的控制帧后再发送任何的数据帧。
如果终端收到了一个关闭的控制帧并且没有在以前发送一个关闭帧,那么终端必须发送一个关闭帧作为回应。(当发送一个关闭帧作为回应时,终端通常会输出它收到的状态码)响应的关闭帧应该尽快发送。终端可能会推迟发送关闭帧直到当前的消息都已经发送完成(例如:如果大多数分片的消息已经发送了,终端可能会在发送关闭帧之前将剩余的消息片段发送出去)。然而,已经发送关闭帧的终端不能保证会继续处理收到的消息。
在已经发送和收到了关闭帧后,终端认为WebSocket连接以及关闭了,并且必须关闭底层的TCP连接。服务端必须马上关闭底层的TCP连接,客户端应该等待服务端关闭连接,但是也可以在收到关闭帧以后任意时间关闭连接。例如:如果在合理的时间段内没有收到TCP关闭指令。
如果客户端和服务端咋同一个时间发送了关闭帧,两个终端都会发送和接收到一条关闭的消息,并且应该认为WebSocket连接已经关闭,同时关闭底层的TCP连接。
5.5.2 心跳Ping
心跳Ping帧包含的操作码是0x9。
关闭帧可能包含“应用数据”。
如果收到了一个心跳Ping帧,那么终端必须发送一个心跳Pong 帧作为回应,除非已经收到了一个关闭帧。终端应该尽快恢复Pong帧。Pong帧将会在5.5.3节讨论。
终端可能会在建立连接后与连接关闭前中间的任意时间发送Ping帧。
注意:Ping帧可能是用于保活或者用来验证远端是否仍然有应答。
5.5.3 心跳Pong
心跳Ping帧包含的操作码是0xA。
5.5.2节详细说明了Ping帧和Pong帧的要求。
作为回应发送的Pong帧必须完整携带Ping帧中传递过来的“应用数据”字段。
如果终端收到一个Ping帧但是没有发送Pong帧来回应之前的pong帧,那么终端可能选择用Pong帧来回复最近处理的那个Ping帧。
Pong帧可以被主动发送。这会作为一个单项的心跳。预期外的Pong包的响应没有规定。
5.6 数据帧
数据帧(例如非控制帧)的定义是操作码的最高位值为0。当前定义的数据帧操作吗包含0x1(文本)、0x2(二进制)。操作码0x3-0x7是被保留作为非控制帧的操作码。
数据帧会携带应用层/扩展层数据。操作码决定了携带的数据解析方式:
文本
“负载字段”是用UTF-8编码的文本数据。注意特殊的文本帧可能包含部分UTF-8序列;然而,整个消息必须是有效的UTF-8编码数据。重新组合消息后无效的UTF-8编码数据处理见8.1节。
二进制
“负载字段”是任意的二进制数据,二进制数据的解析仅仅依靠应用层。
第六章 发送与接收消息
6.1 发送数据
为了通过 WebSocket 连接发送一条 WebSocket 消息,终端必须遵循以下几个步骤:
终端必须保证 WebSocket 连接处于 OPEN 状态(见第 4.1 节和第 4.2.2 节)。如果 WebSocket 连接的任意一端的状态发生了改变,终端必须中止以下步骤。
终端必须将数据按照第 5.2 节定义的 WebSocket 帧进行封装。如果需要发送的数据过大或者在终端希望开始发消息时,如果数据在整体性这一点上不可用,那么终端可能会选择通过在第 5.4 节中定义的一系列帧来进行封装。
包含数据的第一帧操作码(帧操作码)必须根据第 5.2 节中的内容设置的合适的值,以便接收者将数据解析为文本或者二进制数据。
最后一个包含数据的帧的 FIN ( FIN 帧)字段必须和第 5.2 节中定义的一样设置为 1 。
如果数据被发送到了客户端,数据帧必须和第 5.3 节中定义的一样添加掩码。
如果在 WebsSocket 连接中有协商扩展(第 9 章),在这些扩展中的定义和注意事项也许要额外考虑。
被格式化的帧必须通过底层的网络连接进行传输。
6.2 接收数据
为了接收 WebSocket 数据,终端需要监听底层网络连接。输入的数据必须通过第 5.2 节定义的 WebSocket 帧进行解析。如果收到了一个控制帧(第 5.5 节),那么这个帧必须如 5.5 节中定义的方式进行处理。如果收到的是一个数据帧,那么终端必须注意 5.2 节中的定义在操作码(帧操作码)中的数据类型。在这一帧中的“应用数据”被定义为消息的数据。如果帧中包含未分片的数据(第 5.4 节),那么就认为:一条 WebSocket 消息的数据和类型被收到了。如果帧是分片数据的一部分,那么随后的帧包含的“应用数据”连起来就是数据的格式。当通过 FIN 字段(FIN帧)表示的最后一个片段被收到时,我们可以说:一条 WebSocket 消息的数据(由片段组装起来的“应用数据”数据组成)和类型(注意分片消息的第一帧)已经被收到了。接下来的数据帧必须是属于一条新的 WebSocket 消息。
扩展(第 9 章)可能改变数据如何理解的方式,具体包括消息的内容边界。扩展,除了在“应用数据”之前添加“扩展数据”之外,也可以修改“应用数据”(例如压缩它)。
像第 5.3 节中说的那样,服务端在收到客户端的数据帧时必须去除掩码。
参考:https://segmentfault.com/a/1190000018217630