本文主要介绍WebSocket协议解决的问题、协议内容等相关知识
诞生
WebSocket是为了解决服务端和客户端双向通讯问题,提出的一种传输协议,使客户端和服务端可以互相推送、接收消息,做到真正的双工。
在WebSocket出现之前,往往需要客户端通过频繁的发送HTTP请求,来获取服务端的数据,这会导致一些问题:
- 线路层较高的开销,因为每次HTTP请求建立连接,每次客户端到服务器的消息都需要携带头信息。
- 客户端往往还需要维护一个队列来处理服务端的返回值。
- 频繁的请求会给服务端带来压力。
- 消息的获取不够及时。采用WebSocket协议可以解决这些问题。
协议介绍
如下图所示,WebSocket协议分为握手和数据传输两个阶段。
握手
客户端的握手消息:
代码语言:javascript复制GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 握手的随机数,用来生成掩码。
Origin: http://example.com // 访问源
Sec-WebSocket-Protocol: chat, superchat // 子协议的声明
Sec-WebSocket-Version: 13 // 协议版本
Upgrade: websocket
服务端的握手响应消息:
代码语言:javascript复制HTTP/1.1 101 Switching Protocols // 101状态代表连接建立成功
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK xOo=
Sec-WebSocket-Protocol: chat
Upgrade: websocket
握手过程中,WebSocke连接到服务器的端点,由HTTP的GET请求完成。既允许一个请求地址多个路径,也允许单个IP地址多个端口。WebSocket协议是一个独立的基于TCP的协议。它与HTTP唯一的关系是它的握手是由HTTP服务器解释为一个Upgrade请求。
消息
WebSocket的消息是使用帧序列来传输的,客户端必须使用掩码发送所有的帧。
使用掩码主要是考虑到安全问题,上文传输中提到的Sec-WebSocket-Key,就是编码中使用的,具体编码解码的细节我们在本文中就不介绍了。
一个数据帧各部分定义如下图:
- FIN :1bit ,表示是消息的最后一帧,如果消息只有一帧那么第一帧也就是最后一帧。
- RSV1,RSV2,RSV3:每个1bit,必须是0,除非扩展定义为非零。如果接受到的是非零值但是扩展没有定义,则需要关闭连接。
- Opcode:4bit,解释 Payload 数据,规定有以下不同的状态,如果是未知的,接收方必须马上关闭连接。状态如下:
- 0x00: 附加数据帧
- 0x01:文本数据帧
- 0x02:二进制数据帧
- 0x3-7:保留为之后非控制帧使用
- 0x8:关闭连接帧
- 0x9:ping
- 0xA:pong
- 0xB-F(保留为后面的控制帧使用)
- Mask:1bit,掩码,定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理。
- Masking-key:域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
- Payload_len: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:任意长度数据。包含有扩展定义数据和应用数据,如果没有定义扩展则没有此项,仅含有应用数据。
WebSocket中的帧分为两类:
- 数据帧,真正用来传输数据;
- 控制帧,用来控制连接的状态。控制帧主要有四种,控制帧由操作码确定,其中操作码最重要的位是1.控制帧的操作码包括0x8 (关闭帧), 0x9 (Ping帧),和0xA (Pong帧)。操作码0xB-0xF保留用于未来尚未定义的控制帧。Ping帧和Pong帧起到keepalive的作用
消息分片
一条逻辑消息可以分成多个单独的帧。接收端应该对它们进行缓冲,直到设置好fin位。因此,可以将字符串“Hello World”发送到11个包中,每个包的长度为6(报头长度) 1字节。控件包不允许分片。但是,规范希望能够处理乱序的控制帧。这是TCP包以任意顺序到达的情况。连接帧的逻辑大致如下:
- 接收第一帧
- 记住操作码
- 将帧有效负载连接在一起,直到 fin 位被设置
- 断言每个包的操作码是零
分片目的是发送长度未知的消息。如果不分片发送,即一帧,就需要缓存整个消息,计算其长度,构建frame并发送;使用分片的话,可使用一个大小合适的buffer,用消息内容填充buffer,填满即发送出去。
关闭会话
主动关闭:发送关闭帧来关闭会话。服务端和客户端都可以主动发送关闭帧。
- 发送关闭帧之后,当前端不能再发送数据。
- 接收到关闭帧后,不能再接收数据。被动关闭:TCP意外中断,WebSocket连接也会中断。
对于协议更详细的介绍可以参照:WebSocket的RFC文档
特性总结
- WebSocket的设计就是在web交互中,安全的把TCP的数据传输能力赋予客户端和服务端。
- 它是基于帧的,而不是流。每一帧可以是字符也可以是二进制数据(对应到javascrip的数据类型分别是是字符串和Uint8Array)。
- 客户端可以是浏览器,也可以自己实现,如果在浏览器里要符合同源策略的限制。
- 基于URI和子协议,支持同主机同端口上提供多类服务。
在Chrome浏览器中抓取WebSocket包
- 打开开发人员工具
- 在过滤栏选择WS
- 点击一条请求,可以看到如上图看到的内容
Chrome最终展示了每次请求的三个纬度:
- Data:消息内容;
- Length:消息内容长度;
- Time发送时间。
其中消息部分,绿色向上箭头表示消息由客户端发送到服务端,红色向下箭头表示消息由服务端发送到客户端。
不过使用Chrome抓包有局限性,看不到全部的帧信息,可以使用Wireshark抓包工具进行抓包。
引入WebSocket带来的影响
使用WebSocket协议也会对系统架构造成一些影响。如下图,非WebSocket协议下,我们的业务服务很容易扩展,只要保证服务无状态就可以了。
引入WebSocket后一种比较典型的架构设计,如下图所示:
引入WebSocket后,为了保证服务的可扩展性,我们往往需要做一些分层设计,把WebSocket协议层单独拆分,通过消息队列和业务服务解耦。这样就可以保证业务服务的可扩展性。 总之引入WebSocket会给系统带来复杂性。系统架构的设计,如何保证服务的无状态,广播消息的实现等等。
长链接除了WebSocket外还有一种解决方案:HTTP/2 SSE,后续有时间再分享。
参考资料
[1] https://websocket.org/ [2] https://gitee.com/geektime-geekbang/geektime-webprotoc [3] https://datatracker.ietf.org/doc/rfc6455/ [4]https://segmentfault.com/a/1190000017448270