WebSocket协议的握手和数据帧和关闭连接

之前总结了关于Websocket协议的握手连接方式等其他细节,现在对socket连接建立后的数据帧传输和关闭细节总结。

一、数据帧格式

数据传输使用的是一系列数据帧,出于安全考虑和避免网络截获,客户端发送的数据帧必须进行掩码处理后才能发送到服务器,不论是否是在TLS安全协议上都要进行掩码处理。服务器如果没有收到掩码处理的数据帧时应该关闭连接,发送一个1002的状态码。服务器不能将发送到客户端的数据进行掩码处理,如果客户端收到掩码处理的数据帧必须关闭连接。

基本的数据帧为一个opcode、一个payload长度和发送的应用数据,根据ABNF的定义,详细信息如下图

\

这里使用的是数据存储的位(bit),当进行加密的时候,最终要的一位就是最左边的第一个。

FIN :1bit ,表示是消息的最后一帧,如果消息只有一帧那么第一帧也就是最后一帧。 RSV1,RSV2,RSV3:每个1bit,必须是0,除非扩展定义为非零。如果接受到的是非零值但是扩展没有定义,则需要关闭连接。 Opcode:4bit,解释Payload数据,规定有以下不同的状态,如果是未炸ky"http://www.it165.net/qq/" target="_blank" class="keylink">qq1xKOsvdPK1be9sdjQ68Ltyc+52LHVway906Gj17TMrMjnz8KjujB4MCi4vbzTyv2+3dahKSAmbmJzcDsgJm5ic3A7MHgxKM7Esb7K/b7d1qEpICZuYnNwOyZuYnNwOzB4Mii2/r341sbK/b7d1qEpICZuYnNwOyAmbmJzcDsweDMtNyixo8H0zqrWrrrzt8e/2NbG1qHKudPDKSAmbmJzcDsweEItRiixo8H0z qq 688PmtcS/2NbG1qHKudPDKSAmbmJzcDsgJm5ic3A7MHg4KLnYsdXBrL3T1qEpICZuYnNwOzB4OShwaW5nKSZuYnNwOyZuYnNwOzB4QShwb25nKQombmJzcDsmbmJzcDsmbmJzcDtNYXNro7oxYml0o6zR2sLro6y2qNLlcGF5bG9hZMr9vt3Kx7fxvfjQ0MHL0drC67SmwO2jrMjnufvKxzGx7cq+vfjQ0MHL0drC67SmwO2howo8cHJlIGNsYXNzPQ=="brush:java;">Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。 Payload length:7位,7 + 16位,7+64位,payload数据的长度,如果是0-125,就是真实的payload长度,如果是126,那么接着后面的2个字节对应的16位无符号整数就是payload数据长度;如果是127,那么接着后面的8个字节对应的64位无符号整数就是payload数据的长度。 Masking-key:0到4字节,如果MASK位设为1则有4个字节的掩码解密密钥,否则就没有。 Payload data:任意长度数据。包含有扩展定义数据和应用数据,如果没有定义扩展则没有此项,仅含有应用数据。

二、客户端到服务器端掩码处理

前面说过客户端发送到服务器端的数据必须进行掩码处理,掩码的密钥是一个32位的随机值,客户端随机选取密钥必须是不可猜测的。这个掩码处理后并不影响Payload数据的长度。服务器收到掩码处理后的数据后,解码需要使用如下的算法进行: j = i mod 4(i 是传输数据中的十进制的索引下标) 转换后的数据 d = original  ^ mask[j] 也就是将Payload原始数据的每个字符的顺序下标与4去摸,然后将此原始数据字符与掩码的前面去摸后的相应位置的字符进行异或操作即可。这个算法对于加密和解密的操作都是一样的。

三、消息分片

分片目的是发送长度未知的消息。如果不分片发送,即一帧,就需要缓存整个消息,计算其长度,构建frame并发送;使用分片的话,可使用一个大小合适的buffer,用消息内容填充buffer,填满即发送出去。

分片规则:

1.一个未分片的消息只有一帧(FIN为1,opcode非0)

2.一个分片的消息由起始帧(FIN为0,opcode非0),若干(0个或多个)帧(FIN为0,opcode为0),结束帧(FIN为1,opcode为0)。

3.控制帧可以出现在分片消息中间,但控制帧本身不允许分片。

4.分片消息必须按次序逐帧发送。

5.如果未协商扩展的情况下,两个分片消息的帧之间不允许交错。

6.能够处理存在于分片消息帧之间的控制帧

7.发送端为非控制消息构建长度任意的分片

8.client和server兼容接收分片消息与非分片消息

9.控制帧不允许分片,中间媒介不允许改变分片结构(即为控制帧分片)

10.如果使用保留位,中间媒介不知道其值表示的含义,那么中间媒介不允许改变消息的分片结构

11.如果协商扩展,中间媒介不知道,那么中间媒介不允许改变消息的分片结构,同样地,如果中间媒介不了解一个连接的握手信息,也不允许改变该连接的消息的分片结构

12.由于上述规则,一个消息的所有分片是同一数据类型(由第一个分片的opcode定义)的数据。因为控制帧不允许分片,所以一个消息的所有分片的数据类型是文本、二进制、opcode保留类型中的一种。

需要注意的是,如果控制帧不允许夹杂在一个消息的分片之间,延迟会较大,比如说当前正在传输一个较大的消息,此时的ping必须等待消息传输完成,才能发送出去,会导致较大的延迟。为了避免类似问题,需要允许控制帧夹杂在消息分片之间。

数据帧示例:

未掩码处理的文本单数据帧:  0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains "Hello")

掩码处理的文本单数据帧:      0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58

分片未掩码处理的文本消息:   0x01 0x03 0x48 0x65 0x6c (contains "Hel")

 0x80 0x02 0x6c 0x6f (contains "lo")

未掩码处理的Ping请求和掩码处理的响应: 

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)

64K的二进制数据:0x82 0x7F 0x0000000000010000 [65536 bytes of binary data]

四、发送和接收数据

1、发送

端点必须确保WebSocket连接处于OPEN状态。如果在任何时刻WebSocket连接的状态改变了,端点必须终止以下步骤。 端点必须封装/data/到一个WebSocket帧。如果要发送的数据太大或如果在端点想要开始发生数据时数据作为一个整体不可用,端点可以交替地封装数据到一系列的帧中。 第一个包含数据的帧的操作码(帧-opcode)必须设置为适当的值用于接收者解释数据是文本还是二进制数据。 包含数据的最后帧的FIN位(帧-fin)必须设置位1。 如果数据正由客户端发送,帧被掩码。 如果任何扩展已经协商用于WebSocket连接,额外的考虑可以按照这些扩展定义来应用。 已成形的帧必须在底层网络连接之上传输。

2、接收

为了接收WebSocket数据,端点监听底层网络连接。传入数据必须解析为WebSocket帧。当接收到一个数据帧时,端点必须注意由操作码(帧-opcode)定义的数据的/type/。这个帧的“应用数据”被定义为消息的/data/。如果帧由一个未分片的消息组成,这是说已经接收到一个WebSocket消息,其类型为/type/且数据为/data/。如果帧是一个分片消息的一部分,随后数据帧的“应用数据”连接在一起形成/data/。当接收到由FIN位(帧-fin)指示的最后的片段时,这是说已经接收到一个WebSocket消息,其数据为/data/(由连续片段的“应用数据”组成)且类型为/type/(分配消息的第一个帧指出)。随后的数据帧必须被解释为属于一个新的WebSocket消息。

扩展可以改变数据如何读的语义,尤其包括什么组成一个消息的边界。扩展,除了在负载中的“应用数据”之前添加“扩展数据”外,也可以修改“应用数据”(例如压缩它)。服务器必须为从客户端接收到的数据帧移除掩码。

 

五、Websocket关闭

通信的两端中任意一端关闭都可以关闭socket连接,关闭时应该清楚所有的TCP连接资源和TLS回话的资源,同时要丢弃所有的可能接收的字节数据。首先关闭的一方一般都应该是服务器端,然后处于TIME_WAIT状态。

为了使用一个状态码关闭websocket,一端必须发送一个关闭的控制帧,当两端都发送了关闭数据帧时,双方都要关闭所有的连接资源。控制帧为一个“状态码”和一个“原因说明”,当关闭之后,双方处于CLOSED状态。



WebSocket是定义服务器和客户端如何通过Web通信的一种网络协议。协议是通信的议定规则。组成互联网的协议组由IETF(互联网工程任务组)发布。IETF发布评议请求(Request for Comments,RFC),精确地规定了协议(包括RFC 6455):WebSocket协议。RFC 6455于2011年12月发布,包含了实现WebSocket客户端和服务器时必须遵循的规则。

websocket基本上是一个很简单的协议, 主要流程非常少, 实现起来也很简单。

为简单起见, 下面只分析握手和数据帧的报文.

一. 握手(handshake).

握手协议由客户端发起, 服务器响应, 一来一回就完成了. 基本上是为了兼容现有的http基础设施.

下面是一个客户端发起的握手请求:

47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A GET./.HTTP/1.1..  
55 70 67 72 61 64 65 3A 20 77 65 62 73 6F 63 6B Upgrade:.websock 
65 74 0D 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A 20 et..Connection:. 
55 70 67 72 61 64 65 0D 0A 48 6F 73 74 3A 20 31 Upgrade..Host:.1 
39 32 2E 31 36 38 2E 38 2E 31 32 38 3A 31 33 30 92.168.8.128:130 
30 0D 0A 4F 72 69 67 69 6E 3A 20 6E 75 6C 6C 0D 0..Origin:.null. 
0A 50 72 61 67 6D 61 3A 20 6E 6F 2D 63 61 63 68 .Pragma:.no-cach 
65 0D 0A 43 61 63 68 65 2D 43 6F 6E 74 72 6F 6C e..Cache-Control 
3A 20 6E 6F 2D 63 61 63 68 65 0D 0A 53 65 63 2D :.no-cache..Sec- 
57 65 62 53 6F 63 6B 65 74 2D 4B 65 79 3A 20 64 WebSocket-Key:.d 
33 35 39 46 64 6F 36 6F 6D 79 71 66 78 79 59 46 359Fdo6omyqfxyYF 
37 59 61 63 77 3D 3D 0D 0A 53 65 63 2D 57 65 62 7Yacw==..Sec-Web 
53 6F 63 6B 65 74 2D 56 65 72 73 69 6F 6E 3A 20 Socket-Version:. 
31 33 0D 0A 53 65 63 2D 57 65 62 53 6F 63 6B 65 13..Sec-WebSocke 
74 2D 45 78 74 65 6E 73 69 6F 6E 73 3A 20 78 2D t-Extensions:.x- 
77 65 62 6B 69 74 2D 64 65 66 6C 61 74 65 2D 66 webkit-deflate-f 
72 61 6D 65 0D 0A 55 73 65 72 2D 41 67 65 6E 74 rame..User-Agent 
3A 20 4D 6F 7A 69 6C 6C 61 2F 35 2E 30 20 28 57 :.Mozilla/5.0.(W 
69 6E 64 6F 77 73 20 4E 54 20 36 2E 31 3B 20 57 indows.NT.6.1;.W 
4F 57 36 34 29 20 41 70 70 6C 65 57 65 62 4B 69 OW64).AppleWebKi 
74 2F 35 33 37 2E 33 36 20 28 4B 48 54 4D 4C 2C t/537.36.(KHTML, 
20 6C 69 6B 65 20 47 65 63 6B 6F 29 20 43 68 72 .like.Gecko).Chr 
6F 6D 65 2F 33 32 2E 30 2E 31 36 35 33 2E 30 20 ome/32.0.1653.0. 
53 61 66 61 72 69 2F 35 33 37 2E 33 36 0D 0A 0D Safari/537.36... 
0A  

0D 0A 0D 0A, 也就是用"\r\n\r\n"收尾, 这和http头没什么区别. 转换成字符串就是:

GET / HTTP/1.1  
Upgrade: websocket 
Connection: Upgrade 
Host: 192.168.8.128:1300 
Origin: null 
Pragma: no-cache 
Cache-Control: no-cache 
Sec-WebSocket-Key: d359Fdo6omyqfxyYF7Yacw== 
Sec-WebSocket-Version: 13 
Sec-WebSocket-Extensions: x-webkit-deflate-frame 
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1653.0 Safari/537.36 

其中有一对重要的kv, 就是Sec-WebSocket-Key: d359Fdo6omyqfxyYF7Yacw==, 看上去是一个base64编码后的结果, 服务器需要对这个sec-key作一些处理, 并返回握手响应, 这个处理是:

  1. byte[] sha = sha1(("d359Fdo6omyqfxyYF7Yacw==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes());  
  2. System.out.println(new String(Base64.getEncoder().encode(sha)));  

也就是原封不动的拿着这个sec-key和另一个神奇的字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"相连, 再经过sha1摘要

算法处理, 最后再经过base64编码输出即可, 上面的输出结果应该是: pLO2KC7b5t0TZl1E6A3sqJ6EzU4=

服务器在收到握手请求后, 如果愿意提供服务, 则返回一个握手响应, 如下:

  1. HTTP/1.1 101 Switching Protocols  
  2. Connection: Upgrade  
  3. Upgrade: WebSocket  
  4. Sec-WebSocket-Accept: pLO2KC7b5t0TZl1E6A3sqJ6EzU4=  

遵循http的规则, 字节流上一样是要以"\r\n\r\n"收尾.

二. 数据帧

rfc6455上叫做非控制帧, 除了非控制帧之外, 就是控制帧. 包括connection close, ping, pong等帧, 这里只讲非控制帧, 也就是数据帧.

数据帧从长度上可以分为三种. 帧中的静荷数据(payload data)长度小于0x7E的为小帧, 静荷数据长度 >=0x7E又<=0x10000的为中帧,

再长的叫大帧.

数据帧从类型上暂时可以分为两种, 文本帧和二进制帧.

例子:

a). 一个从客户端发向服务端的小帧.

  1. 82 B0 6A F7 C6 30 0A D9 C6 34 D4 18 78 C1 6E F5 ..j..0...4..x.n.  
  2. C6 30 6C D5 CC 10 23 87 AF 48 3C A2 9C 64 01 C4 .0l...#..H<..d..  
  3. AE 59 04 C5 B1 5B 35 85 A3 41 18 B0 F5 5C 13 8E .Y...[5..A...\..  
  4. 92 42 02 84 85 53                               .B...S  

82

二进制为: 1000 0010, 最高位(FIN)为1, 表示这是最后一帧, 第一个帧也可能是最后一帧. 身后还有三位为预留. 低位四0010为操作码.

也就是0x02, 表示这是一个二进制帧, 0x01为文本帧.

B0

二进制为: 1011 0000, 最高位(MASK)为1, 表示当前帧的静荷数据部分使用了掩码, 事实上, rfc6455规定从客户端发往服务器端的数据帧

必需使用掩码, 反过来, 从服务器发回来的, 则必需不使用掩码. 低7位为静荷数据长度字段, 这里是011 0000, 也就是0x30, 从上面的报文上

看, 这个0x30没有包含后面的掩码.

6A F7 C6 30

掩码, 掩码总是四个字节.

0A D9 C6...一直到最后为经过掩码加工后的静荷数据. 要回到数据本来的面目, 使用下面的算法:

  1. byte by[] = new byte[]{0x82, 0xB0, 0x6A, 0xF7, 0xC6, 0x30, 0x0A....};  
  2. byte mask[] = new byte[] { 0x6A, (byte) 0xF7, (byte) 0xC6, 0x30 };  
  3. for (int i = 6 /* 越过掩码. */; i < by.length; i++)  
  4.     by[i] = (byte) (by[i] ^ mask[(i - 6) % 4]);  

得到的结果应该是:

  1. 82 B0 6A F7 C6 30 60 2E 00 04 BE EF BE F1 04 02 ..j..0`.........  
  2. 00 00 06 22 0A 20 49 70 69 78 56 55 5A 54 6B 33 ..."..IpixVUZTk3  
  3. 68 69 6E 32 77 6B 5F 72 65 71 72 47 33 6C 79 79 hin2wk_reqrG3lyy  
  4. 54 72 68 73 43 63                               TrhsCc  

b). 一个从服务器发给客户端的小帧.

  1. 82 29 61 27 01 04 BE EF BE F1 05 02 00 00 06 1B .)a'............  
  2. 0A 08 55 3B 02 19 39 35 E2 44 12 0F 21 EC BC 47 ..U;..95.D..!..G  
  3. 02 F3 EC 70 ED 5B 7B 07 C7 F4 D0                ...p.[{....  

更简单了, 还是82, 最后一帧, 二进制帧, 29, 0010 1001, 无掩码, 也就是身后全长为0x29.

c). 未使用掩码的中帧.

81 7E 01 00 66 77 88 ..., 帧长为 0x0100, 也就是256个字节.

d). 未使用掩码的大帧.

82 7F 00 00 00 00 11 22 33 44 66 77 88 ..., 帧长为0x0000000011223344, 直接跳过4字节, 而使用8字节来表示长度, 非常暴力.

这里需要注意的是, websocket要求使用最小帧原则, 也就是静荷数据长度小于0x7E帧, 不能使用中帧或大帧的来表示. 长度小于

0x10000的帧也不能用大帧来表示.

相关文章
相关标签/搜索