以下内容转载自 https://www.huaweicloud.com/articles/4157e9b5a58ef15e29d71f76b08e1b92.html
websocket协议规范
作者:二郎666 时间: 2019-11-02 08:06:49
标签:网络通信协议websocketwebsocket协议客户端vbwebsocket教程
《websocket协议详解》教程分三篇:
文章上方有详细的规范、源码链接。邮箱:952125505@qq.com ,QQ交流群:715895604
本文与websocket协议规范有冲突的地方,请以规范为准。(文章上方有websocket中文协议规范的链接)
握手
首先由客户端发起连接请求,服务端验证客户端握手数据并返回服务器握手数据,客户端验证服务端握手数据,双方完成握手。
客户端:请求协议升级
首先,客户端发起协议升级请求。(采用标准 HTTP 报文格式,且只支持GET方法。)
GET / HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: [LocalHost:80]
Origin: [LocalIP]
Pragma: no -cache
cache -Control: no -cache
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
Sec-WebSocket-Version: 13
Sec -WebSocket - Extensions: x -webkit - deflate - Frame;permessage-deflate; client_max_window_bits
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.2171.99 Safari/537.36
各参数含义:
- Connection: Upgrade:表示要升级协议
- Upgrade: websocket:表示要升级到 websocket 协议。
- Sec-WebSocket-Version: 13:表示 websocket 的版本。
- Sec-WebSocket-Key:阻止无效websocket协议连接。
需要指出Sec-WebSocket-Key的生成算法,伪代码:base64编码(16个随机字节)
其它部分没有什么好介绍的,跟http头信息一样,可以查看http相关资料。
服务端:响应协议升级
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
返回状态码101表示协议升级成功(从http升级到websocket)。此后,所有的数据都按照websocket协议进行。如果返回其他状态码代表请求失败,必须要关闭连接(不要问为什么,规定)。具体状态码含义可以查看http相关资料。
Sec-WebSocket-Accept用于客户端验证服务器的合法性,如果验证数据不一致,客户端就要关闭连接。伪代码:base64编码(sha1加密( Sec-WebSocket-Key +"258EAFA5-E914-47DA-95CA-C5AB0DC85B11") ),其中258EAFA5-E914-47DA-95CA-C5AB0DC85B11是固定的,代表websocket全局唯一标识符。
至此,客户端与服务端已经完成协议升级。
数据帧
websocket中,数据是由一个个帧组成的,帧是组成一条数据的基本单位。
数据帧的大小是由数据帧类型决定的,而不是统一固定大小的数据单位。怎么理解,一个数据帧可以是一个bit大小,也可以N个bit大小。不同类型的帧大小可能是不一样的,但同一类型的帧大小是固定的。一条数据中包含多个帧,它们是由一个个不同类型的帧组成的,但不能含有两个或两个以上相同类型的帧。也就是说一个帧类型,在一条数据中只能出现一次。
每一种帧类型都代表一种特定含义,它们大小固定,顺序固定。
如下图,FIN就是一个数据帧类型,大小是1bit,位置是第1位。而opcode也是一种数据帧类型,它的大小却是4bit,位置是第5位。它们的大小和位置顺序是websocket定义好的,不能改变。
下面这幅图很好的说明了WebSocket 数据帧的格式,第一行代表数据大小,每一小格代表1bit单位。数据帧按照图中从左到右,从上到下的顺序排列。
每一条websocket数据,都是按照上图所示的规则进行排列的。在本文中,我们把一条数据叫做消息。下面我们结合上图,详细的讲解各帧的含义。下文中未标注可以为空值的数据帧,都不能为空值。(如果讲解与websocket规范有冲突,请以规范为准)
FIN:1 bit
如果是0,表示这不是消息的最后一个分片。
如果是1,表示这是消息的最后一个分片。
分片我们可以理解为一条消息的一个部分,只要你愿意,可以随意将一条消息分为N个分片。比如“这是一条消息”是一条消息,则我们可以随意把它划分为 “这是”、“一”、“条”、“消息”,共4个分片。但是注意,websocket协议中没有分片顺序标识符,为了保证接收端能接收到正确的消息,分片要按严格按照前后顺序发送。
只有一个分片的消息不叫分片发送,很好理解,因为此时分片就是消息本身,一次即可发送完毕。只有包含两个或两个分片以上的消息才叫分片发送。
FIN用来判断消息是否接收完,如果是1,表示是一条消息的接收完毕。
RSV1, RSV2, RSV3:每个 1 bit
必须是0,除非一个扩展协商为非0值定义含义。如果收到一个非0值且没有协商的扩展定义这个非0值的含义,接收端就必须断开连接。
一般情况下,这三个帧全部定义为0就完事了。因为非0的情况下,对方会告诉你这些自定义值的含义,比如RSV1=1,代表对面很蛋疼的含义。当然如果你也要蛋疼自定义一些非0值,当我啥也没说。
Opcode: 4 bits
定义了“负载数据”的解释。如果收到一个未知的操作码,接收端点必须断开连接。
值 | 含义 |
0 | 继续帧,表示消息分片模式 |
1 | 文本帧,表示文本格式传输 |
2 | 二进制帧,表示二进制格式传输 |
3-7 | 保留(目前还未定义的意思),用于定义未来的非控制帧 |
8 | 关闭帧,表示关闭连接 |
9 | Ping帧,一般主动发送ping给对方,确认对方状态 |
A | Pong帧,一般发送了ping给对方,对方就回复pong |
B-F | 保留,用于定义未来的控制帧 |
负载数据,可以理解为我们要传送的文字、视频、图片等数据。比如上面说的“这是一条消息”就是负载数据,对于分片发送模式,负载数据就是当前的分片,比如“这”。
关闭帧:只要收到关闭帧,必须立即关闭连接。发送关闭帧时,可以在扩展数据中附带状态码和应用数据中添加自定义数据。状态码含义见文章最后列表。这里你只需记住收到关闭帧就必须断开连接,其他的概念不理解也不要紧,下面我们有详细介绍。
客户端和服务端建立连接之后,可能遇到,双方长时间没有数据往来,被消息中间件断开连接的情况,或一方发生其它意外连接关闭而没通知对方的情况。为保证通讯的正常进行,就需要不定时的确认对方状态。websocket为我们提供了ping pong 心跳机制来避免这种意外的发生。
- 发送方 ->接收方:ping
- 接收方 ->发送方:pong
着重说一下Opcode=0,继续帧,表示当前分片消息不是消息的第一个分片,是一个继续分片。也就是说一条消息有两个或两个以上分片,才能出现Opcode=0的情况。结合FIN帧,我们就推导出当前消息是否是是分片发送,第一个分片,中间分片,最后一个分片。公式如下:
一条消息发送:
FIN=1且Opcode<>0
分片消息发送:
- 第一个分片: FIN=0 Opcode=1 或 Opcode=2
- 中间的分片: FIN=0 Opcode=0
- 最后一个分片: FIN=1 Opcode=0
Mask: 1 bit
如果0,不需掩码处理。(不掩码处理也就无需掩码键,所以消息中masking-key为空值,也就是不包含masking-key帧)
如果1,需要掩码处理。(掩码处理必须要有掩码键,所以masking-key帧不能为空值)
根据websocket定义:
客户端发送数据需要进行掩码处理,接收数据无需反掩码操作
服务端发送数据无需进行掩码处理,接收数据需要反掩码操作
掩码与反掩码采用一样的算法,伪代码为:
j =i mod 4
返回数据(i)=原始数据 (i) xor masking-key(j)
i分别代表返回数据和原始数据的第i个字节(byte),j代表masking-key(掩码键)的第j个字节。
掩码键生成算法见下面Masking-key介绍
Payload length: 7 bits, 7 + 16 bits 或者 7 + 64 bits
“负载数据”的长度,单位是字节(byte).
如果 0-125,这是负载长度。
如果 126,之后的2个字节(16 位)表示负载数据长度。
如果 127,之后的8个字节(64位)表示负载数据长度。而且最高有效位必须是0。
要确定负载数据长度,首先先判断第一个字节的值,如果>0且<125,那么这个值就是负载数据的长度,为0时候,就代表负载数据长度为0,也就是不包含负载数据。如果=126,那么就取这个字节后面的两个字节作为负载数据的长度。如果=127,就取后面8个字节表示数据的长度。
需要注意多字节长度数量以网络字节顺序来表示,也就是大端模式(按字节顺序的从左向右读取数据)。以第一个字节等于126为例,根据上面介绍,我们知道要取后面两个字节作为负载数据长度,假设两个字节的值分别为A0,01(16进制)。那么负载数据长度就是A001
Masking-key: 0 or 32 bits
指掩码键,可以为空值。客户端发送到服务器的所有帧必须通过一个包含在帧中的 32 位值来掩码。
如果 mask 位设置为 1,则该字段存在。
如果 mask 位设置为 0,则该字段缺失。
详细信息见上面介绍的Mask帧介绍
Masking-key的生成算法也很简单,就是由4个随机字节组成。
Payload data: (x+y) bytes
指负载数据,可以为空值
“负载数据”=“扩展数据”+“应用数据”。
以上是负载数据的权威解释,前面说负载数据可以理解为文字、图片、视频的解释不确切,这样解释只是为了让读者更好的进入状态,通过这里我们知道,它只是代表无扩展数据时候的负载数据。
Extension data: x bytes
指“扩展数据”,排在负载数据最前面。可以为空值
通常是0字节,除非已经协商了一个扩展。任何扩展必须指定“扩展数据” 的长度,或长度是如何计算的,以及扩展如何使用,必须在打开阶段握手期间协商。
如果存在,“扩展数据”包含在总负载数据长度中。
其实就是自定义一个协议,如果有扩展数据,扩展数据就加在应用数据前面,并且要协商好扩展数据长度如何计算。
Application data: y bytes
指“应用数据”,“扩展数据”之后帧的剩余部分,可以为空值。
“应用数据” = “负载数据” - “扩展数据”
状态码
当收到一个关闭帧的时候,可能附带关闭的状态码,含义如下表
状态码 | 名称 | 描述 |
---|---|---|
0–999 | 保留段, 未使用. | |
1000 | CLOSE_NORMAL | 正常关闭; 无论为何目的而创建, 该链接都已成功完成任务. |
1001 | CLOSE_GOING_AWAY | 终端离开, 可能因为服务端错误, 也可能因为浏览器正从打开连接的页面跳转离开. |
1002 | CLOSE_PROTOCOL_ERROR | 由于协议错误而中断连接. |
1003 | CLOSE_UNSUPPORTED | 由于接收到不允许的数据类型而断开连接 (如仅接收文本数据的终端接收到了二进制数据). |
1004 | 保留. 其意义可能会在未来定义. | |
1005 | CLOSE_NO_STATUS | 保留. 表示没有收到预期的状态码. |
1006 | CLOSE_ABNORMAL | 保留. 用于期望收到状态码时连接非正常关闭 (也就是说, 没有发送关闭帧). |
1007 | Unsupported Data | 由于收到了格式不符的数据而断开连接 (如文本消息中包含了非 UTF-8 数据). |
1008 | Policy Violation | 由于收到不符合约定的数据而断开连接. 这是一个通用状态码, 用于不适合使用 1003 和 1009 状态码的场景. |
1009 | CLOSE_TOO_LARGE | 由于收到过大的数据帧而断开连接. |
1010 | Missing Extension | 客户端期望服务器商定一个或多个拓展, 但服务器没有处理, 因此客户端断开连接. |
1011 | Internal Error | 客户端由于遇到没有预料的情况阻止其完成请求, 因此服务端断开连接. |
1012 | Service Restart | 服务器由于重启而断开连接. |
1013 | Try Again Later | 服务器由于临时原因断开连接, 如服务器过载因此断开一部分客户端连接. |
1014 | 由 WebSocket 标准保留以便未来使用. | |
1015 | TLS Handshake | 保留. 表示连接由于无法完成 TLS 握手而关闭 (例如无法验证服务器证书). |
1016–1999 | 由 WebSocket 标准保留以便未来使用. | |
2000–2999 | 由 WebSocket 拓展保留使用. | |
3000–3999 | 可以由库或框架使用.不应由应用使用. 可以在 IANA 注册, 先到先得. | |
4000–4999 | 可以由应用使用. |