5

WebSocket协议要点:握手与数据帧传输控制

 2 years ago
source link: https://www.zoucz.com/blog/2020/12/13/8731c3a0-3d22-11eb-90b5-eb40e9720ed0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

WebSocket协议要点:握手与数据帧传输控制

作者: 邹成卓 2020-12-13 17:06:47 分类: 应用层协议

标签: websocket

评论数:

前段时间做了个websocket的应用,基于刚开始工作那会儿对websocket的有限了解,认为websocket在握手、协议升级完成后,就直接利用http所使用的TCP连接来进行全双工通信。也就是说,把它理解成了个 类传输层协议我要说话

公司内网简单的搜了搜关于websocket文章(实际上讲的更多的是如何在公司的运维平台上跑、如何穿透网关、如何选择开发框架等技巧),也没搜太多资料就开干了。 我要说话

一天工夫搞定代码,其中大半天时间都在手撸TCP分包逻辑。完事后简单梳理下代码,发现所使用的开发组件,在message响应中,居然可以区分收到的数据是字符串还是二进制类型。瞬间就有点懵逼了,这框架是咋知道数据类型的呢?看来事情有哪里不太对。 我要说话

于是翻开了websocket的RFC文档,才发现这东西其实是个应用层协议,那大半天的分包逻辑其实是不用写的。我去,一口老血喷出来啊。所以起了心思在此记录一下。 我要说话

借用阮一峰老师的一张图,比较形象。
image.png我要说话

错误的理解:websocket协议通过http协议发起连接,然后协议升级后去掉http,用tcp来传输数据,是类传输层协议。 我要说话

正确的理解:websocket协议通过http协议发起连接,然后协议升级将http替换为websocket,用websocket数据帧传输数据,是应用层协议。 我要说话

WebSocket协议RFC文档 我要说话

image.png
参考rfc文档画的一个简单的生命周期,其中比较有意思的是,协议中提到了关闭连接时,最好由服务端发起TCP关闭流程,这样能更快的建立重连。由客户端发起的话,需要等待2MSL周期才能再次建立连接。 我要说话

下面分别整理一下握手阶段、数据帧传输阶段的知识点。我要说话

握手阶段,对客户端和服务端都有一些要求,分别分析。 我要说话

客户端发送握手请求时,有如下要求 我要说话

要求 描述 可选

http协议 http协议必须大于等于1.1版本,必须是get 必须

Header Host 必须有一个Host header 必须

Header Upgrade 必须有一个Upgrade header,它的值必须包含websocket 必须

Header Connection 必须有一个Connection header,它的值必须包含Upgrade 必须

Header Sec-WebSocket-Key 必须有一个Sec-WebSocket-Key header,它是一个16字节的随机值,base64编码的字符串 必须

Header Sec-WebSocket-Version 必须有一个Sec-WebSocket-Version header,值必须为13,代表ws协议的版本号 必选

Header Origin 从浏览器中发起的ws连接,必须携带此header,值是源地址ascci序列化后的结果。非浏览器来源则可能有可能没有 可选

Header Sec-WebSocket-Protocol 可能有Sec-WebSocket-Protocol header,标明子协议,字符的范围是U+0021到U+007E,但是不包含其中的定义在RFC2616中的分隔符 可选

Header Sec-WebSocket-Extensions 可能有Sec-WebSocket-Extensions header,表示客户端期望使用的协议级别的扩展 可选

其它Header 可能还会包含其他的文档中定义的header字段,如cookie(RFC6265)或者认证相关的header字段如Authorization字段(RFC2616) 可选

客户端处理握手请求响应的时候,有如下要求 我要说话

要求 描述 可选

握手返回码 返回码必须是101 Switching Protocols,若不是101则按对应返回码含义处理会话。 必须

Header Upgrade 响应中必须包含Upgrade header,且其值包含websocket,否则连接失败 必须

Header Connection 必须有一个Connection header,它的值必须包含Upgrade 必须

Header Sec-WebSocket-Accept 必须有一个Sec-WebSocket-Accept header 注① 必须

Header Sec-WebSocket-Extensions 可能有一个Sec-WebSocket-Extensions header,如果有,且request Sec-WebSocket-Extensions header中没有此header的值,则握手连接失败 可选

Header Sec-WebSocket-Protocol 可能有一个Sec-WebSocket-Protocol header,如果有,且request Sec-WebSocket-Protocol header中没有它的值,则握手连接失败 可选

注① 我要说话

Sec-WebSocket-Accept的计算:
Sec-WebSocket-Accept = base64(sha1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
将握手请求header中的Sec-WebSocket-Key和字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼起来,计算sha1,以base64编码输出,即为Sec-WebSocket-Accept的值。 我要说话

Sec-WebSocket-Accept的作用:
提供基础的防护,减少恶意连接、意外连接。我要说话

  1. http客户端误请求,或者某些http的扫描器连接到ws协议端口,服务端可以直接拒绝。
  2. 客户端确保服务端理解实现的是websocket协议,若服务端实现的是http协议但是也返回了应答,则可以拒绝并断开。
  3. 防止代理缓存。例如反向代理缓存了ws协议路径的缓存,后面的ws握手直接给返回缓存内容,无意义。

服务端处理握手请求时,有如下要求,如不满足要求则返回错误码,400 bad request 我要说话

要求 描述 可选

http协议 http协议必须大于等于1.1版本,必须是get 必须

Header Host 请求host 必选

Header Upgrade Upgrade header,必须含websocket 必须

Header Connection Connection header 必须含Upgrade 必须

Header Sec-WebSocket-Key Sec-WebSocket-Key header,16字节,参见握手请求部分 必须

Header Sec-WebSocket-Version Sec-WebSocket-Version header,必须为13 必须

Header Origin 请求来源 可选

Header Sec-WebSocket-Protocol Sec-WebSocket-Protocol header,子协议 可选

Header Sec-WebSocket-Extensions Sec-WebSocket-Extensions header,协议扩展 可选

其它Header 如cookie(RFC6265)或者认证相关的header字段如Authorization字段(RFC2616) 可选

若握手请求符合要求,则构造应答请求 我要说话

要求 描述 可选

协议连接 握手成功后则建立TCP或者TLS连接 必须

认证 可以用cookie或者authorization头进行认证,返回401等返回码 可选

重定向 返回3xx等 可选

构造返回信息 构造含origin 、key 、version、resource、subprotocol、extensions的响应内容,详见4.2.2 必须

构造同意连接响应 返回响应码101、Upgrade头、Connection头、按客户端说明中注①的方式生成Sec-WebSocket-Accept头 必须

数据帧传输

websocket协议提供了一套应用层数据编解码方法,这样就不需要业务自己去分包,处理粘包问题了。
数据帧详情见rfc第5节,这里对其数据帧编码及传输控制,逐个详细理解一下。
一个websocket数据传输帧的完整编码结构如下: 我要说话

image.png我要说话

是否尾帧,占1bit。值取0和1,0表示非尾帧,1表示尾帧。
这个字段非常重要,在一个较大应用层数据分多帧传输时,可以用来标记何时收到尾帧,配合后面的payload length完成分包逻辑。 我要说话

RSV1、RSV2、RSV3

协议扩展字段,各占1bit。如果连接没有协议扩展定义这三个标记位,那么它们必须都为0。若无扩展定义且非0,则应该断开连接。 我要说话

opcode

操作码码,占4bit。操作码分为数据类型标记和控制码,含义如下 我要说话

值 类型 含义

0 类型标记 持续帧,表示此帧的payload数据应该接续在前面到达的帧payload后面,与FIN配合来拼接得到完整应用数据

1 类型标记 文本帧,表示这帧的payload是一个文本,注意此文本需要用UTF-8来解析,如果按UTF-8解析失败,则无论是客户端还是服务端,都应该立即断开连接

2 类型标记 二进制帧

3-7 类型标记 预留给以后的其它非控制帧

8 控制码 连接关闭

9 控制码 ping包 注②

10 控制码 pong包

11-15 控制码 预留给以后其它的控制帧

若操作码使用了未定义的,则应该断开连接。我要说话

注② 我要说话

ping包和pong包是协议内设计的心跳包,可以随时发送而不扰乱分片payload组装的过程。一端在收到ping控制指令后,应该立即回一个pong指令。
这个设计在协议连接穿透网关时比较有用,有的网关可能有持续多久无数据则断掉连接的设计,此时若想做连接保活,就不用构造业务心跳包来保活了,直接用ping包即可。 我要说话

是否使用掩码,占1bit。表示是否使用掩码,0为不使用,1为使用。
如果设置为1,那么Masking-key中将存储有随机生成的掩码,协议规定从客户端发送到服务端的数据必须使用掩码。其目的是为了安全,防止“代理缓存污染攻击”,其攻击原理这里不深究,可以自行搜索。 我要说话

Payload len

载荷字节长度,占7bit,或者7+16bit,或者7+64bit。这个字段也非常重要,用来处理最基本的分包问题,此字段决定了底层的TCP/TLS连接在二进制收包后,如何分离出一个个的完整websocket frame。进一步,配合FIN包可以分离出完整的应用层message。 我要说话

前7bit值 后续16bit值 后续64bit值 含义

0~125 无 无 长度为0~125字节的载荷

126 解析为uint16 无 表示长度为126 ~ 65535字节的载荷

127 无 解析为uint64 表示长度为65536 ~ uint64最大值字节的载荷

Masking-key

掩码,占0~4字节。若前面的mask设置的是1,则掩码占4字节。客户端向服务端发送数据的时候必须添加掩码。
掩码算法:
以字节为单位,假设原始payload中一个字节的数据为original-octet-i,转换后的字节数据为transformed-octet-i,掩码字节索引为index,最终使用的掩码为mask,那么 我要说话

  1. 计算最终使用的掩码索引index = original-octet-i MOD 4
  2. 取出Masking-key在index位置的字节作为掩码mask = Masking-key[index]
  3. 将原始数据与mask做异或运算,得到转换后的数据transformed-octet-i = original-octet-i XOR mask

此算法是可逆的,由transformed-octet-i得到original-octet-i,重复上面的步骤即可。 我要说话

Payload Data

Payload数据,占Payload len长度。
值得注意的是,若握手的过程中定义了协议扩展,那么扩展的数据也必须包含在Payload len长度中,即Payload Data = Extension Data + Applicatino Data。 我要说话

定义扩展的时候,需要扩展自己定义如何计算长度,例如取前Payload Data4个字节作为扩展数据长度等。 我要说话

若未定义协议扩展,则Extension Data的长度为0,Payload Data全部是Applicatino Data。我要说话

本文链接:https://www.zoucz.com/blog/2020/12/13/8731c3a0-3d22-11eb-90b5-eb40e9720ed0/我要说话

☞ 参与评论我要说话


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK