Labs 导读
在WebSocket出现之前,一个Web应用(即时聊天、多人协作)的客户端和服务端之间常见的双向数据交换方式有短轮询、长轮询、SSE(Server-Sent Events,服务器发送事件)。这些方式在效率和网络带宽利用率方面存在诸多问题。WebSocket协议应运而生,对外提供了简单的双向数据传输能力。
Part 01、介绍
WebSocket是一种在TCP连接上进行全双工通讯的网络通信协议。在2009年诞生,于2011年被IETF(The Internet Engineering Task Force,国际互联网工程任务组)定为标准并发布RFC 6455互联网标准跟踪文档,2016发布了RFC7936文档进行补充。WebSocket API同时也被W3C定为标准。
WebSocket协议设计之初是为了取代HTTP形式的通信,因为RFC6202中提到HTTP协议最初不是用来做双向数据通信的。WebSocket协议并没有完全舍弃HTTP,它基于HTTP基础服务在现有环境中实现了双向通信目标。正如RFC 6455中说的那样,WebSocke的设计哲学是最小约束的框架,唯一的约束就是协议是基于帧而不是流,并且支持Unicode文本和二进制帧两者。
Part 02、 握手
WebSocket协议分为建连握手、消息传输和断连握手三个部分,整体流程如下图所示。
2.1 建连握手-客户端
为了兼容HTTP服务器侧的应用程序和代理,客户端的建连握手(包括通过代理或通过TLS加密隧道进行的连接)是一个遵循RFC2616中定义的有效HTTP升级请求,客户端连接握手请求header部分字段如下图所示。此外,客户端一旦发送了的连接握手就必须等待来自服务器的响应。
- 请求URI
格式,ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]或者wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ],任何无效值都会造成建连失败
- 请求行
必须是GET方法,HTTP版本至少是1.1
- Upgrade
值必须是“websocket”,ASCII值,不区分大小写
- Connection
值必须包含“Upgrade”,ASCII值,不区分大小写
- Sec-WebSocket-Key
客户端为本次建连随机生成的16字节base64编码的字符串
- Origin
源地址,浏览器客户端必填,非浏览器客户端选填
- Sec-WebSocket-Protocol
客户端支持的一个或多个以逗号分隔的子协议,按优先级排序
- Sec-WebSocket-Version
客户端拟使用协议版本号,值必须为13。历史版本9、10、11和12不再作为有效值
- Sec-WebSocket-Extensions
客户端拟使用协议扩展。目前HyBi Working Group进行了多路复用扩展和压缩扩展,多路复用扩展实现共享底层TCP连接。压缩扩展为WebSocket协议增加了压缩功能,例如 x-webkit-deflate-frame
2.2 建连握手-服务端
当客户端与服务端建立WebSocket连接时,服务端必须回复客户端建连握手请求,握手回复header部分字段如下图所示。
- 状态行
HTTP/1.1 101 Switching Protocols,表示接受客户端建连。若服务器想要停止处理客户端的握手,可返回例如401这样的错误代码的HTTP响应
- Upgrade
值必须是“websocket”
- Connection
值必须包含“Upgrade”
- Sec-WebSocket-Accept
若服务端接受客户端连接,生成该值。先将客户端请求头的 Sec-WebSocket-Key值与RFC4122文档中定义的全局唯一标识“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼接,然后进行SHA-1哈希再进行base64-encoded得到该值
- Sec-WebSocket-Protocol
服务端拟使用的协议,该值从客户端发送的Sec-WebSocket-Protocol中选择,若服务端都不支持,值为空
- Sec-WebSocket-Extensions
服务端拟使用协议扩展
2.3 断接握手
客户端和服务端都可以发送包含指定控制序列的控制帧(Close控制帧)以开始关闭握手。一方在接收到关闭控制帧时,只需发送一个关闭帧作为响应,然后关闭连接。存在拦截代理等情况下,TCP关闭握手并不总是可靠的端到端握手,上述关闭握手过程旨在补充TCP关闭握手(FIN/ACK)。
Part 03、 数据传输
客户端一旦和服务端连接握手成功,双方就可以开始数据传输了。这是一个双向通信信道,在遵循RFC 6455规范中“消息”概念的基础上,双方均可以独立地随意发送数据。一条消息包含一个或者多个数据帧(不一定对应于网络层中的消息),Websocket帧格式如下图所示。
3.1 帧结构
- FIN
1位,表示是否是一条消息的最后一个分片。
- RSV1, RSV2, RSV3
1位,扩展功能未使用的情况下默认值为0。
- Opcode
4位,定义“Playload data”数据类型。
0(十进制):连续帧
1:文本帧
2:二进制帧
3-7:预留非控制帧
8:连接关闭帧
9:心跳ping帧
10:心跳pong帧
11-15:预留控制帧
- MASK
1位,是否屏蔽“Playload data”,1是,0否。
- Payload length
7位,或者7+16位,或者7+64位,表示Payload data的长度。具体地,Payload length小于125,数据长度用Payload length表示;Payload length等于126,数据长度用Payload length后面16位表示;Payload length等于127,数据长度用Payload length后面64位表示。
- Masking-key
32位,存放客户端发送的掩码。为了防止代理缓存污染攻击,RFC6455中要求掩码必须来自强大的熵源,不可被预测。常规算法以字节为步长遍历载荷数据, 对于载荷数据的第i个字节, 做i对4取模得到j,掩码覆盖后的载荷数据的第i个字节的值为原第i个字节与Masking-Key的第j个字节做按位异或操作。
- Payload data
载荷数据分为扩展数据和应用数据两种,扩展数据在握手阶段协商是否使用,应用数据在扩展数据之后。
3.2 控制帧
控制帧由Opcode值确定,协议当前定义的控制帧的操作码包括 0x8 (Close)、0x9(Ping)、和0xA(Pong)。控制帧必须有一个小于等于125字节的有效载荷长度,对于Close控制帧有效载荷的前2个字节表示状态码,剩余字节表示关闭原因,如下图所示。
3.3 消息分片
消息分片指将概念上的一条“消息”通过多个数据帧发送。消息分片允许发送未知大小的消息,而不必缓冲整条消息。同时,消息分片结合多路复用协议的扩展,可以分割消息为更小的分段以共享输出通道。
协议中分片消息开始帧的FIN位为0,opcode位为非0表示该帧为某消息分片,中间帧FIN位为0,opcode位为0,最后通过FIN位为1,opcode为0标识分片结束。协议要求分片数据帧按顺序发送到另一端。
Part 04、总结
WebSocke是设计在TCP层之上,不需要考虑数据长度,数据粘包拆包。也能通过扩展功能与HTTP/2多路复用结合,充分利用带宽。开发者只需在服务端和客户端代码中按序处理消息分片逻辑。