5.数据帧——WebSocket协议翻译
最新内容请跟踪我的Github:https://github.com/zhangkaitao/websocket-protocol
5.1概述
在WebSocket协议中,数据使用帧序列来传输。为避免混淆网络中间件(例如拦截代理)和出于安全原因,第10.3节进一步讨论,客户端必须掩码(mask)它发送到服务器的所有帧(更多详细信息请参见5.3节)。(注意不管WebSocket协议是否运行在TLS至上,掩码都要做。) 当收到一个没有掩码的帧时,服务器必须关闭连接。在这种情况下,服务器可能发送一个定义在7.4.1节的状态码1002(协议错误)的Close帧。服务器必须不掩码发送到客户端的所有帧。如果客户端检测到掩码的帧,它必须关闭连接。在这种情况下,它可能使用定义在7.4.1节的状态码1002(协议错误)。(这些规则可能在未来规范中放宽。)
基本帧协议定义了带有操作码(opcode)的帧类型、负载长度、和用于“扩展数据”与“应用数据”及它们一起定义的“负载数据”的指定位置。某些字节和操作吗保留用于未来协议的扩展。
一个数据帧可以被客户端或者服务器在打开阶段握手完成之后和端点发送Close帧之前的任何时候传输(5.5.1节)。
5.2基本帧协议
用于数据传输部分的报文格式是通过本节中详细描述的ABNF来描述。(注意,不像本文档的其他章节,本节中的ABNF是在位(bit)组上操作。每一个位组的长度在注释中指出。在编码报文时,最重要的位是在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 bit
必须是0,除非一个扩展协商为非零值定义含义。如果收到一个非零值且没有协商的扩展定义这个非零值的含义,接收端点必须_失败WebSokcket连接_。
Opcode: 4 bits
定义了“负载数据”的解释。如果收到一个未知的操作码,接收端点必须_失败WebSocket连接_。定义了以下值。
- %x0 代表一个继续帧
- %x1 代表一个文本帧
- %x2 代表一个二进制帧
- %x3-7 保留用于未来的非控制帧
- %x8 代表连接关闭
- %x9 代表ping
- %xA 代表pong
- %xB-F 保留用于未来的控制帧
Mask: 1 bit
定义是否“负载数据”是掩码的。如果设置为1,一个掩码键出现在masking-key,且这个是用于根据5.3节解掩码(unmask)“负载数据”。从客户端发送到服务器的所有帧有这个位设置为1。
Payload length: 7 bits, 7+16 bits, 或者 7+64 bits
“负载数据”的长度,以字节为单位:如果0-125,这是负载长度。如果126,之后的两字节解释为一个16位的无符号整数是负载长度。如果127,之后的8字节解释为一个64位的无符号整数(最高有效位必须是0)是负载长度。多字节长度数量以网络字节顺序来表示。注意,在所有情况下,最小数量的字节必须用于编码长度,例如,一个124字节长的字符串的长度不能被编码为序列126,0,124。负载长度是“扩展数据”长度+“应用数据”长度。“扩展数据”长度可能是零,在这种情况下,负载长度是“应用数据”长度。
Masking-key: 0 or 4 bytes
客户端发送到服务器的所有帧通过一个包含在帧中的32位值来掩码。如果mask位设置为1,则该字段存在,如果mask位设置为0,则该字段缺失。详细信息请参见5.3节 客户端到服务器掩码。
Payload data: (x+y) bytes
“负载数据”定义为“扩展数据”连接“应用数据”。
Extension data: x bytes
“扩展数据”是0字节除非已经协商了一个扩展。任何扩展必须指定“扩展数据”的长度,或长度是如何计算的,以及扩展如何使用必须在打开阶段握手期间协商。 如果存在,“扩展数据”包含在总负载长度中。
Application data: y bytes
任意的“应用数据”,占用“扩展数据”之后帧的剩余部分。“应用数据”的长度等于负载长度减去“扩展数据”长度。
基本帧协议是由以下ABNF[RFC5234]正式定义。重要的是要注意这个数据是二进制表示的,而不是ASCII字符。因此,一个1位长度的字段取值为%x0 / %x1 是表示为单个位,其值为0或1,不是以ASCII编码代表字符“0”或“1”的完整的字节(8位位组)。4位长度的字段值介于%0-F之间,是通过4位表示的,不是通过ASCII字符或这些值的完整字节(8位位组)。[RFC5234]没有指定字符编码:“规则解析为最终值的字符串,有时候被称为字符。在ABNF中,一个字符仅仅是一个非负整数。在某些上下文中,一个值到一个字符集的特定映射(编码)将被指定。” 在这里,指定的编码是二进制编码,每一个最终值是编码到指定数量的比特中,每个字段是不同的。
ws-frame = frame-fin ; 1位长度 frame-rsv1 ; 1位长度 frame-rsv2 ; 1位长度 frame-rsv3 ; 1位长度 frame-opcode ; 4位长度 frame-masked ; 1位长度 frame-payload-length ; 或者 7、 7+16、 ; 或者7+64 位长度 [ frame-masking-key ] ; 32位长度 frame-payload-data ; n*8位长度; n>=0 frame-fin = %x0 ; 这条消息后续还有更多的帧 / %x1 ; 这条消息的最终帧 ; 1位长度 frame-rsv1 = %x0 / %x1 ; 1位长度,必须是0,除非协商其他 frame-rsv2 = %x0 / %x1 ; 1位长度,必须是0,除非协商其他 frame-rsv3 = %x0 / %x1 ; 1位长度,必须是0,除非协商其他 frame-opcode = frame-opcode-non-control / frame-opcode-control / frame-opcode-cont frame-opcode-cont = %x0 ; 帧继续 frame-opcode-non-control= %x1 ; 文本帧 / %x2 ; 二进制帧 / %x3-7 ; 4位长度,保留用于未来的非控制帧 frame-opcode-control = %x8 ; 连接关闭 / %x9 ; ping / %xA ; pong / %xB-F ; 保留用于未来的控制帧 ; 4位长度 frame-masked = %x0 ; 帧没有掩码,没有frame-masking-key / %x1 ; 帧被掩码,存在frame-masking-key ; 1位长度 frame-payload-length = ( %x00-7D ) / ( %x7E frame-payload-length-16 ) / ( %x7F frame-payload-length-63 ) ; 分别7, 7+16, or 7+64位长度 frame-payload-length-16 = %x0000-FFFF ; 16位长度 frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF ; 64位长度 frame-masking-key = 4( %x00-FF ) ; 仅当frame-masked 是 1时存在 ; 32位长度 frame-payload-data = (frame-masked-extension-data frame-masked-application-data) ; 当frame-masked是1 / (frame-unmasked-extension-data frame-unmasked-application-data) ; 当frame-masked是0 frame-masked-extension-data = *( %x00-FF ) ; 保留用于未来扩展 ; n*8 位长度,n >= 0 frame-masked-application-data = *( %x00-FF ) ; n*8 位长度,n >= 0 frame-unmasked-extension-data = *( %x00-FF ) ; 保留用于未来扩展 ; n*8 位长度,n >= 0 frame-unmasked-application-data = *( %x00-FF ) ; n*8 位长度,n >= 0
5.3.客户端到服务器掩码
一个掩码的帧必须有5.2节定义的字段frame-masked设置为1。 掩码键完全包含在帧中,5.2节定义的frame-masking-key。它用于掩码定义在相同章节的frame-payload-data 中的“负载数据”,其包含“扩展数据”和“应用数据”。
掩码键是由客户端随机选择的32位值。当准备一个掩码的帧时,客户端必须从允许的32位值集合中选择一个新的掩码键。掩码键需要是不可预测的;因此,掩码键必须来自一个强大的熵源,且用于给定帧的掩码键必须不容易被服务器/代理预测用于后续帧的掩码键。掩码键的不可预测性对防止恶意应用的作者选择出现在报文上的字节是必要的。RFC 4086 [RFC4086]讨论了什么需要一个用于安全敏感应用的合适的熵源。
掩码不影响“负载数据”的长度。变换掩码数据到解掩码数据,或反之亦然,以下算法被应用。相同的算法应用,不管转化的方向,例如,相同的步骤即应用到掩码数据也应用到解掩码数据。
变换数据的八位位组i ("transformed-octet-i")是原始数据的八位位组i("original-octet-i")异或(XOR)i取模4位置的掩码键的八位位组("masking-key-octet-j"):
j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
负载长度,在帧中以frame-payload-length表示,不包括掩码键的长度。它是“负载数据”的长度,例如,跟在掩码键后边的字节数。
5.4.分片(Fragmentation)
分片的主要目的是允许当消息开始但不必缓冲该消息时发送一个未知大小的消息。如果消息不能被分片,那么端点将不得不缓冲整个消息以便在首字节发生之前统计出它的长度。对于分片,服务器或中间件可以选择一个合适大小的缓冲,当缓冲满时,写一个片段到网络。 第二个分片的用例是用于多路复用,一个逻辑通道上的一个大消息独占输出通道是不可取的,因此多路复用需要可以分割消息为更小的分段来更好的共享输出通道。(注意,多路复用扩展在本文档中没有描述)
除非另有扩展指定,帧没有语义含义。一个中间件可能合并且/或分割帧,如果客户端和服务器没有协商扩展;或如果已协商了一些扩展,但中间件理解所有协商的扩展且知道如何去合并且/或分割在这些扩展中存在的帧。这方面的一个含义是,在没有扩展情况下,发送者和接收者必须不依赖于特定帧边界的存在。
以下规则应用到分片:
- 一个没有分片的消息由单个带有FIN位设置(5.2节)和一个非0操作码的帧组成。
一个分片的消息由单个带有FIN位清零(5.2节)和一个非0操作码的帧组成,跟随零个或多个带有FIN位清零和操作码设置为0的帧,且终止于一个带有FIN位设置且0操作码的帧。一个分片的消息概念上是等价于单个大的消息,其负载是等价于按顺序串联片段的负载;然而,在存在扩展的情况下,这个可能不适用扩展定义的“扩展数据”存在的解释。例如,“扩展数据”可能仅在首个片段开始处存在且应用到随后的片段,或 “扩展数据”可以存在于仅用于到特定片段的每个片段。在没有“扩展数据”的情况下,以下例子展示了分片如何工作。 例子:对于一个作为三个片段发送的文本消息,第一个片段将有一个0x1操作码和一个FIN位清零,第二个片段将有一个0x0操作码和一个FIN位清零,且第三个片段将有0x0操作码和一个FIN位设置。
控制帧(参见5.5节)可能被注入到一个分片消息的中间。控制帧本身必须不被分割。
消息分片必须按发送者发送顺序交付给收件人。
片段中的一个消息必须不能与片段中的另一个消息交替,除非已协商了一个能解释交替的扩展。
一个端点必须能处理一个分片消息中间的控制帧。
一个发送者可以位非控制消息创建任何大小的片段。
客户端和服务器必须支持接收分片和非分片的消息。
由于控制帧不能被分片,一个中间件必须不尝试改变控制帧的分片。
如果使用了任何保留的位值且这些值的意思对中间件是未知的,一个中间件必须不改变一个消息的分片。
在一个连接上下文中,已经协商了扩展且中间件不知道协商的扩展的语义,一个中间件必须不改变任何消息的分片。同样,没有看见WebSocket握手(且没被通知有关它的内容)、导致一个WebSocket连接的一个中间件,必须不改变这个链接的任何消息的分片。
由于这些规则,一个消息的所有分片是相同类型,以第一个片段的操作码设置。因为控制帧不能被分片,用于一个消息中的所有分片的类型必须或者是文本、或者二进制、或者一个保留的操作码。
注意:如果控制帧不能被插入,一个ping延迟,例如,如果跟着一个大消息将是非常长的。因此,要求在分片消息的中间处理控制帧。
实现注意:在没有任何扩展时,一个接收者不必按顺序缓冲整个帧来处理它。例如,如果使用了一个流式API,一个帧的一部分能被交付到应用。但是,请注意这个假设可能不适用所有未来的WebSocket扩展。
5.5.控制帧
控制帧由操作码确定,其中操作码最重要的位是1。当前定义的用于控制帧的操作码包括0x8 (Close)、0x9(Ping)、和0xA(Pong)。 操作码0xB-0xF保留用于未来尚未定义的控制帧。
控制帧用于传达有关WebSocket的状态。控制帧可以插入到分片消息的中间。
所有控制帧必须有一个125字节的负载长度或更少, 必须不被分段。
5.5.1.Close
关闭(Close)帧包含0x8操作码。
关闭帧可以包含内容体(“帧的“应用数据”部分)指示一个关闭的原因,例如端点关闭了、端点收到的帧太大、或端点收到的帧不符合端点期望的格式。如果有内容体,内容体的头两个字节必须是2字节的无符号整数(按网络字节顺序)代表一个在7.4节的/code/值定义的状态码。跟着2字节的整数,内容体可以包含UTF-8编码的/reason/值,本规范没有定义它的解释。数据不必是人类可读的但可能对调试或传递打开连接的脚本相关的信息是有用的。由于数据不保证人类可读,客户端必须不把它显示给最终用户。
客户端发送到服务器的关闭帧必须根据5.3节被掩码。
在应用发送关闭帧之后,必须不发送任何更多的数据帧。
如果一个端点接收到一个关闭帧且先前没有发送一个关闭帧,端点必须在响应中发送一个关闭帧。(当在响应中发生关闭帧时,端点通常回送它接收到的状态码) 它应该根据实际情况尽快这样做。端点可以延迟发送关闭帧知道它当前消息发送了(例如,如果一个分片消息的大多数已经发送了,端点可以发送剩余的片段在发送一个关闭帧之前)。但是,不保证一个已经发送关闭帧的端点将继续处理数据。 发送并接收一个关闭消息后,一个端点认为WebSocket连接关闭了且必须关闭底层的TCP连接。服务器必须立即关闭底层TCP连接,客户端应该等待服务器关闭连接但可能在发送和接收一个关闭消息之后的任何时候关闭连接,例如,如果它没有在一个合理的时间周期内接收到服务器的TCP关闭。
如果客户端和服务器同时都发送了一个关闭消息,两个端点都将发送和接收一个关闭消息且应该认为WebSocket连接关闭了并关闭底层TCP连接。
5.5.2. Ping
Ping帧包含0x9操作码。
Ping帧可以包含“应用数据”。
当收到一个Ping帧时,一个端点必须在响应中发送一个Pong帧,除非它早已接收到一个关闭帧。它应该尽可能快地以Pong帧响应。Pong帧在5.5.3节讨论。
一个端点可以在连接建立之后并在连接关闭之前的任何时候发送一个Ping帧。 注意:一个Ping即可以充当一个keepalive,也可以作为验证远程端点仍可响应的手段。
5.5.3. Pong
Pong帧包含一个0xA操作码。
5.5.2节详细说明了应用Ping和Pong帧的要求。
一个Pong帧在响应中发送到一个Ping帧必须有在将回复的Ping帧的消息内容体中发现的相同的“应用数据”。
如果端点接收到一个Ping帧且尚未在响应中发送Pong帧到之前的Ping帧,端点可以选择仅为最近处理的Ping帧发送一个Pong帧。
一个Pong帧可以未经请求的发送。这个充当单向的心跳(heartbeat)。到未经请求的Pong帧的一个响应是不期望的。
5.6.数据帧
数据帧(例如,非控制帧)由操作码最高位是0的操作码标识。当前为数据帧定义的操作码包括0x1(文本)、0x2(二进制)。操作码0x3-0x7保留用于未来尚未定义的非控制帧。
数据帧携带应用层和/或扩展层数据。操作码决定了数据的解释:
Text
“负载数据”是编码为UTF-8的文本数据。注意,一个特定的文本帧可能包括部分UTF-8序列;不管怎么样,整个消息必须包含有效的UTF-8。重新组装的消息中的无效的UTF-8的处理描述在8.1节。
Binary
“负载数据”是随意的二进制数据,其解释仅仅是在应用层。
5.7.示例
未掩码文件消息的单个帧
0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (包含 "Hello")
掩码的文本消息的单个帧
0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (包含 "Hello")
一个分片的未掩码的文本消息
0x01 0x03 0x48 0x65 0x6c (包含 "Hel")
0x80 0x02 0x6c 0x6f (包含 "lo")
未掩码的Ping请求和掩码的Ping响应
0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f
(包含内容体"Hello"、但内容体的内容是随意的)
0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58
(包含内容体"Hello"、匹配ping的内容体)
单个未掩码帧中的256字节的二进制消息
0x82 0x7E 0x0100 [256字节的二进制数据]
单个未掩码帧中的64KB的二进制消息
0x82 0x7F 0x0000000000010000 [65536字节的二进制数据]
5.8.可扩展性
协议被设计为允许扩展,这将增加功能到基础协议。端点的一个连接必须在打开阶段握手期间协商使用的任何扩展。本规范提供了用于扩展的操作码0x3到0x7和0xB到0xF、“扩展数据”字段、和帧-rsv1、帧rsv2、和帧rsv3帧头位。9.1节进一步讨论了扩展协商。以下是一些预期使用的扩展。这个列表是不完整的也不规范的。
- “扩展数据”可以放置在“负载数据”中的“应用数据”之前。
- 保留的位可以分配给需要的每个帧。
- 保留的操作码值能被定义。
- 如果需要更多的操作码值,保留的位可以分配给操作码字段。
- 一个保留的位或一个“扩展”操作码可以定义以从“负载数据”中分配额外的位来定义更大的操作码或更多的每帧位。