websocket 介绍
背景
在 Websocket 诞生之前,服务器的数据需要传达至客户端,通常需要由客户端轮询或长轮询(Long-Polling)的方式获取。
WebSocket 协议主要为了解决基于 HTTP/1.x 的 Web 应用无法实现服务端向客户端主动推送的问题, 为了兼容现有的设施, WebSocket 协议使用与 HTTP 协议相同的端口, 并使用 HTTP Upgrade 机制来进行 WebSocket 握手, 当握手完成之后, 通信双方便可以按照 WebSocket 协议的方式进行交互。
简介
WebSocket 使用 TCP 作为传输层协议, WebSocket 使得客户端和服务器之间保持长连接,通过 Websocket 握手建立 Websocket 连接,通过 Websocket 挥手 关闭 Websocket 连接,可采用 ping-pong 保活,使用 帧 传输数据。
在 WebSocket 协议中, 帧 (frame) 是通信双方数据传输的基本单元, 与其它网络协议相同, frame 由 Header 和 Payload 两部分构成, frame 有多种类型, frame 的类型由其头部的 Opcode 字段来指示。
WebSocket 的 frame 可以分为两类:
- 用于传输控制信息的 frame (如通知对方关闭 WebSocket 连接),
- 用于传输应用数据的 frame, 使用 WebSocket 协议通信的双方都需要首先进行握手,
注意:只有当握手成功之后才开始使用 frame 传输数据
WebSocket 支持在 TCP 上层引入 TLS,ws 和 wss 的关系就类似 http 和 https 的关系。
websocket 握手
客户端发起:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
- Sec-WebSocket-Key:必选项, 由客户端随机生成的 16 字节值, 然后做 base64 编码
- Sec-WebSocket-Version : 必选项,表示 WebSocket 协议的版本
- Sec-WebSocket-Protocol :可选项,通信的子协议
- Sec-WebSocket-Extensions :可选项,扩展协议
服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat
服务器若支持 WebSocket 协议, 并同意与客户端握手, 则应返回 101 的 HTTP 状态码, 表示同意协议升级, 同时设置 Upgrade 字段的值为 websocket, 并将 Connection 字段的值设置为 Upgrade 。
- Sec-WebSocket-Accept:经过服务器确认,并且加密转换(签名)的 Sec-WebSocket-Key;
Sec-WebSocket-Accept 的计算方法:
- 将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
- 通过 SHA1 计算出摘要,并转成 base64 字符串。
客户端对服务器响应的校验:
- 检查服务端返回的状态码是否为 101
- 检查服务端返回的响应是否包含 Upgrade 字段
- 检查 Upgrade 字段的值是否为 websocket(大小写不敏感)
- 校验服务端返回的 Sec-WebSocket-Accept 字段的值是否合法(注:采用相同的Sec-WebSocket-Accept 的计算方法,确保服务器返回的 Sec-WebSocket-Accept 和 客户端本地生成的 Sec-WebSocket-Accept 一致)
- 若 Sec-WebSocket-Protocol 存在,则校验服务端返回的 Header 中包含的 Sec-WebSocket-Protocol 中的值是否属于客户端发起的 Sec-WebSocket-Protocol 的值列表中的值
- 若 Sec-WebSocket-Extensions 存在,则校验服务端返回的 Header 中包含的 Sec-WebSocket-Extensions 中的值是否属于客户端发起的 Sec-WebSocket-Extensions 的值列表中的值
注意:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端 / 服务端是否合法的 ws 客户端、ws 服务端,其实并没有实际性的保证。
websocket 帧
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
- FIN, 长度为 1 比特, 该标志位用于指示当前的 frame 是消息的最后一个分段。( WebSocket 支持将长消息切分为若干个 frame 发送, 长消息的最后一个 frame 的 FIN 字段为 1,其他 frame 的 FIN 字段都为 0)
- RSV 1 ~ 3, 这三个字段为保留字段, 每个字段的长度为 1 比特, 只有在 WebSocket 扩展时用, 若不启用扩展, 则该三个字段应置为 1, 若接收方收到 RSV 1 ~ 3 不全为 0 的 frame, 并且双方没有协商使用 WebSocket 协议扩展, 则接收方应立即终止 WebSocket 连接
- Opcode, 长度为 4 比特, 该字段将指示 frame 的类型, RFC 6455 定义的 Opcode 共有如下几种:
- 0x0, continuation frame (延续帧,text frame或binary frame后接一个或多个continuation frame,需要组合起来才完整)
- 0x1, text frame (文本帧)
- 0x2, binary frame (二进制帧)
- 0x3 ~ 7, 目前保留, 以后将用作更多的非控制类 frame
- 0x8, close frame, 用于关闭 WebSocket 连接
- 0x9, ping frame (心跳保活帧)
- 0xA, pong frame (心跳保活应答帧)
- 0xB ~ F, 目前保留, 以后将用作更多的控制类 frame
continuation frame 举例:在传递数据时, 会先收到一个 binary frame, 它的 FIN 是0, 后续的数据会以 continuation frame 的形式发送, 直到最后一个frame的 FIN位是 1 的 continuation frame 结束, 中间不会穿插其它的 frame 。同理, text frame 也是如此实现。
- Mask, 长度为 1 比特, 该字段是一个标志位, 用于指示 frame 的数据 (Payload) 是否使用掩码掩盖, RFC 6455 规定当且仅当由客户端向服务端发送的 frame, 需要使用掩码覆盖, 掩码覆盖主要为了解决代理缓存污染攻击 (详见 RFC 6455 Section 10.3)
- Payload Len, 以字节为单位指示 frame Payload 的长度, 该字段的长度为 7 比特 或 7 + 16 比特 或 7 + 64 比特。
- 当 Payload 的实际长度在 [0, 125] 时, 则 Payload Len 字段的长度为 7 比特, 它的值直接代表了 Payload 的实际长度;
- 当 Payload 的实际长度为 126 时, 则 Payload Len 后跟随的 16 位将被解释为 16-bit 的无符号整数, 该整数的值指示 Payload 的实际长度;
- 当 Payload 的实际长度为 127 时, 其后的 64 比特将被解释为 64-bit 的无符号整数, 该整数的值指示 Payload 的实际长度。
关于 Payload Len 如何实现:先读 7 bit 识别是以上3种的哪一种长度类型,再决定是否向后读 16 bit 或 64 bit 。
Masking-key, 该字段为可选字段, 当 Mask 标志位为 1 时, 代表这是一个掩码覆盖的 frame, 此时 Masking-key 字段存在, 其长度为 32 位, RFC 6455 规定所有由客户端发往服务端的 frame 都必须使用掩码覆盖, 即 对于所有由客户端发往服务端的 frame, 该字段都必须存在 , 该字段的值是由客户端使用熵值足够大的随机数发生器生成。若 Mask 标识位 0, 则 frame 中将设置 Masking-key 。
Payload, 该字段的长度是任意的, 该字段即为 frame 的数据部分, 若通信双方协商使用了 WebSocket 扩展, 则该扩展数据 (Extension data) 也将存放在此处, 扩展数据 + 应用数据, 它们的长度和为 Payload Len 字段指示的值。
WebSocket closing handshake
- websocket handshake 从一端发送一个
close control frame
开始。 - 发送一个
close control frame
或收到一个close control frame
都意味着 websocket handshake 开始,并且 websocket connection 进入CLOSING
状态。 - 收到
close control frame
的一端需答复对端一个close control frame
,并关闭 TCP connection。 - 收到
close control frame
答复的端,关闭 TCP connection,完成 TCP 挥手后,TCP connection 关闭。 - 当 TCP connection 关闭后,websocket connection 关闭,并且 websocket connection 进入
CLOSED
状态。 - 如果 TCP conncetion 是在 WebSocket closing handshake 之后完成,那么 Websocket connection 可以说是 干净地关闭的。
websocket server
nodejs 的 websocket server
ws 是一个第三方的 websocket 通信模块,需要安装 npm i ws
const WebSocket = require('ws')
const WebSocketServer = WebSocket.Server;
// wss is WebSocket.Server
wss.on("connection", function(ws, request) {
// ws is WebSocket
ws.on("message", (data, isBinary) => {
if (isBinary) {
console.log("recv binary data");
ws.send("recv binary data success", {mask: false, binary: true, compress: false, fin: true}, (error) => {
if (error) {
console.log("send data callback : error = ${error}");
}
});
} else {
console.log("recv text data");
ws.send("recv text data success", {mask: false, binary: false, compress: false, fin: true}, (error) => {
if (error) {
console.log("send data callback : error = ${error}");
}
});
}
});
ws.on("ping", (data) => {
// keepalive
ws.pong(data, false, (err) => {
if (err) {
console.log("pong error=${err}");
}
});
console.log("pong");
});
ws.on("pong", (data) => {
// keepalive
console.log("ping");
});
ws.on("close", (code, reason) => {
console.log("websocket close");
});
});
nginx 反向代理
《Nginx官方文档:WebSocket proxying》
为了将客户机和服务器之间的连接从 HTTP/1.1转换为 WebSocket,使用 HTTP/1.1中提供的协议切换机制。
HTTP/1.1中提供的协议切换机制:客户端通过请求中的 "Upgrade" header请求协议切换。
本来在 nginx 中,如上所述,包括“Upgrade” 和 “Connection”在内的 hop-by-hop headers 不会从客户机传递到被代理的服务器,因此,为了让被代理的服务器知道客户机将协议切换到 WebSocket 的意图,必须显式地传递这些消息头:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
nginx 配置:
# upstream in http config field
upstream WebsocketServer {
127.0.0.1:1106;
}
# location in server config field
location /webscoket_test/ {
proxy_pass WebsocketServer;
proxy_http_version 1.1;
proxy_read_timeout 3600s; # 超时设置,可采用心跳ping/pong保活
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
扩展知识
End-to-end headers
- End-to-end headers 将被传输到请求或响应的最终接收者。
- 该类型的消息头会被代理转发
- 该类型的消息头会被缓存
Hop-by-hop headers
- Hop-by-hop headers 只对单个传输级别的连接有意义,并且不由缓存存储或由代理转发。
- 该类型的消息头不会被代理转发
- 该类型的消息头不会被缓存
来源:hop-by-hop
HTTP/1.1 中 hop-to-hop 类型的消息头:
- Connection
- Keep-Alive
- Proxy-Authenticate
- Proxy-Authorization
- TE
- Trailers
- Transfer-Encoding
- Upgrade
注意
- 该升级机制只是 HTTP/1.1 有效,HTTP/2 已不支持该机制
- nginx 配置代理转发,默认不转发 hop-to-hop 类型的消息头 。
- nginx 会断开长时间没有数据传输的连接
测试工具
wscat
oktools
websocket client
JavaScript 的 websocket client
<!DOCTYPE html>
<html>
<head>
<title>websocket</title>
</head>
<body>
<script type="text/javascript">
// 浏览器提供 WebSocket 对象
var ws = new WebSocket('ws://localhost:1106')
// 发送
ws.onopen = function() {
ws.send('hello')
}
// 接收
ws.onmessage = function(message) {
alert(message.data)
if (message.data === 'hello') {
ws.close()
}
}
</script>
</body>
</html>