一、WebSocket简介
WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据,本质上一种计算机网络应用层 的协议,用来弥补http协议在持久通信能力上的不足。
WebSocket特点
- 建立在 TCP 协议之上,从 HTTP 协议升级而来。
- 与 HTTP 协议良好兼容新。默认端口是 80 和 443,握手阶段采用 HTTP 协议。
- 数据格式比较轻量,通信效率高,性能开销小。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务端通信。
- 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
- 可以支持扩展,定了扩展协议。
- 保持连接状态,websocket 是一种有状态的协议,通信就可以省略部分状态信息。
- 实时性更强,因为是双向通信协议,所以服务端可以随时向客户端发送数据。
WebSocket出现背景
在 WebSocket 协议出现以前,创建一个和服务端进双通道通信的 web 应用,需要依赖HTTP协议,进行不停的轮询(短轮询、长轮询,一般在浏览器上就是使用 setInerval 或 setTimeout),这会导致一些问题:
WebSocket 与 HTTP 的区别
- 连接方式:HTTP协议是单向连接,仅能由客户端发起,服务器无法主动向客户端推送信息。相反,WebSocket是双向的全双工协议,客户端和服务器都能主动发送数据。
- 状态:HTTP是无状态的,每次请求都需要重新连接,请求结束后连接就断开。而WebSocket是有状态的,一旦连接建立,客户端和服务器就会保持连接直至主动断开。
- 连接长度:HTTP协议是短连接,每次请求都需要建立新的连接,请求结束后连接就断开。与之不同,WebSocket是持久连接,一旦连接建立,就可以进行多次数据的发送和接收,减少了频繁建立和断开连接的开销。
- 协议开头 :HTTP协议的URL以http://或https://开头,而WebSocket的URL以ws://或wss://开头。
- 信息交互方式 :建立了WebSocket连接后,服务器可以在任何时候发送信息到客户端,不需要等待客户端发起的请求。
二、WebSocket原理
1. 建立连接
协议升级
通过HTTP/HTTPS协议发起一次HTTP请求,请求的时候带上这几个 header,将HTTP 升级到 WebSocket ,客户端将协议升级为:Upgrade=WebSocket 头,并通过Connection=Upgrade 来发起请求。
第三个 header 则是保证安全用的一个 key,这个头的值是一个经过Base64编码的随机值,这个随机值的目的是为了在WebSocket协议中防止所谓的"缓存污染攻击"(Cache Poisoning Attacks),保证了WebSocket连接的唯一性和安全性。
javascript
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Ia3dQjfWrAug/6qm7mTZOg==
回应请求
然后,服务器收到这个请求后进行回应,同样通过HTTP/HTTPS协议,即状态码101 Switching Protocol(表示服务器理解并同意客户端的协议切换请求)来进行升级。
javascript
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: JkE58n3uIigYDMvC+KsBbGZsp1A=
其中Sec-WebSocket-Accept是依据key值按照特定的算法生成一个新的字符串,并经过SHA-1的散列运算,然后再进行Base64编码的结果,客户端收到服务器返回的Sec-WebSocket-Accept,会和自己本地按照同样的算法生成的值作对比,只有当这两个值一致,才算完成了握手,然后WebSocket才能建立连接并进行数据通信。
javascript
const crypto = require('crypto');
function hashKey(key) {
const sha1 = crypto.createHash('sha1');
// 字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 是固定的
sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
return sha1.digest('base64');
}
缓存污染攻击
缓存污染攻击是指攻击者可能试图复用(或称之为缓存)之前捕获的WebSocket握手请求和响应的过程,以达成未经授权的WebSocket连接。
- 攻击者捕获了之前的WebSocket握手请求和响应,这包括了Sec-WebSocket-Key和Sec-WebSocket-Accept。
- 然后,攻击者试图通过复用这个捕获的过程,即发送相同的握手请求(或称之为缓存的握手请求),以试图建立一个WebSocket连接。
- 但是,由于Sec-WebSocket-Key在每次新的WebSocket握手请求中都是随机生成的,而服务端生成的Sec-WebSocket-Accept是和Sec-WebSocket-Key有关的。这样就导致了攻击者即便复用之前的Sec-WebSocket-Key,服务端返回的Sec-WebSocket-Accept也会和之前的不一样。
换言之,WebSocket的这种机制防止了攻击者通过缓存和复用握手过程中的Sec-WebSocket-Key和Sec-WebSocket-Accept来进行攻击,这就避免了所谓的"缓存污染攻击"。
2. 数据通信
数据交换步骤
- 发送方将数据打包成帧:发送方先将数据分段,每一段数据打包成一个帧。对于文本数据,发送方还需将数据编码为UTF-8。
- 发送方发送帧给接收方:发送方通过WebSocket连接将帧发送给接收方。
- 接收方解析帧:接收方接收到帧后,先解析帧头,获取帧的元数据,然后提取帧体中的数据。对于文本数据,接收方还需将数据解码为原始的文本。
- 接收方组装完整的消息 :接收方将接收到的所有帧按照发送次序组装成完整的消息。
数据分片
在WebSocket 协议中,客户端与服务端数据交换的最小信息单位叫做帧(frame),由1 个或多个帧按照次序组成一条完整的消息(message)。WebSocket 的每条消息可能被切分为多个数据帧,当 WebSocket 的接收方接收到一个数据帧时,会根据 FIN 值来判断是否收到消息的最后一个数据帧。
FIN = 1 时,表示为消息的最后一个数据帧;FIN = 0 时,则不是消息的最后一个数据帧,接收方还要继续监听接收剩余数据帧。注意,接收方是在整个接收过程中逐步组装消息 的,而不是等到所有帧都接收完毕后再组装。
opcode 表示数据传输的类型,0x01 表示文本类型的数据;0x02 表示二进制类型的数据;0x00 比较特殊,表示延续帧(continuation frame),意思就是完整数据对应的数据帧还没有接收完。
分帧目的:
- 保证数据的可靠性和完整性:将数据分为一帧帧能够更好地保证数据的可靠性和完整性。在传输过程中,任何帧如果发生错误,都能够被准确地检测出来,并请求重新发送。这样避免了单一大数据块在出现错误时需要整体重传的问题。
- 提高传输效率:帧的存在使得数据在传输过程中可以进行流控制,即使在糟糕的网络条件下也可以更平滑地进行数据发送和接收,提高了传输效率。
- 允许数据的并发传输:帧的存在使得二进制和文本数据可以并行交错发送,而无需等待前一消息发送完毕再发送下一消息(服务器和客户端不需要等待当前帧处理完成就可以开始下一个帧的处理。)。
- **支持实时通信:**WebSocket旨在支持浏览器与服务器的实时通信。通过使用分帧技术,可以及时将小段数据快速发送给对方,而无需等待整个大数据块的接收或发送完成,确保了低延迟的实时数据交互。
数据帧格式
数据传输的格式是二进制和文本。二进制帧的Payload数据直接就是原始的二进制数据,而文本帧需要将Payload数据解码为UTF-8格式的文本。
每个WebSocket帧都包括一个帧头和一个帧体。帧头包含了帧的元数据,如FIN位(表示这是消息的最后一个帧),操作码,帧的长度等。帧体则包含了实际要传输的数据。统一格式如下:
lua
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 ... |
+---------------------------------------------------------------+
opcode 操作码:
- %x0:表示一个延续帧(continuation frame)。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
- %x1:表示这是一个文本帧(frame),text frame
- %x2:表示这是一个二进制帧(frame),binary frame
- %x3-7:保留的操作代码,用于后续定义的非控制帧。
- %x8:表示连接断开。connection close
- %x9:表示这是一个 ping 操作。a ping
- %xA:表示这是一个 pong 操作。a pong
- %xB-F:保留的操作代码,用于后续定义的控制帧。
3. 维持连接
心跳机制
当一个客户端和服务器之间通过WebSocket建立起连接后,心跳机制是一种用来保持该连接并检查连接健康状态的机制,工作方式如下:
- Ping消息:客户端(或者服务器)会周期性地向对方发送一个特殊的帧,这称为Ping帧。Ping帧并不携带任何应用级别的数据,而仅仅是一个用于检查连接状态的标志。具体的发送周期通常可以根据实际情况设定,例如可以设置每隔10秒发送一次Ping帧。
- Pong消息:当另一方接收到Ping帧后,会立即回应一个Pong帧。这是一个自动的操作,当收到Ping帧后,WebSocket的API会自动发送Pong帧。像Ping帧一样,Pong帧也不携带任何应用级别的数据。
- 超时判断:如果在设定的时间内没有接收到Pong帧,例如设定的时间是5秒,那么就认为对方已经离线或者连接出现问题。于是,可以执行相应的操作,例如断开连接,或者发出提醒。
注:WebSocket中,控制帧(如Ping帧和Pong帧)的处理优先级高于数据帧。当同时收到数据帧和控制帧时,不论数据帧的处理是否完成,控制帧都会被优先处理。
javascript
const WebSocket = require('ws');
const url = 'ws://example.com/socket';
const PING_INTERVAL = 10000; // 设置ping间隔为10秒
let socket;
let isAlive = false;
let pingIntervalID;
// 心跳函数
function heartbeat() {
console.log('发送Ping消息');
socket.ping(); // 发送Ping消息
}
// 建立WebSocket连接
function connect() {
socket = new WebSocket(url);
// 连接成功时的事件处理程序
socket.onopen = () => {
console.log('WebSocket连接已建立');
// 当连接打开时,将isAlive设为true
isAlive = true;
// 开始ping间隔
pingIntervalID = setInterval(() => {
if (isAlive) {
isAlive = false;
heartbeat();
} else {
console.log('pong响应超时,连接终止');
// 如果在设定的间隔内没有收到pong响应,就终止连接或者依据业务要求重新连接
socket.terminate();
}
}, PING_INTERVAL);
};
// 收到pong响应的事件处理程序
socket.onpong = () => {
console.log('收到pong消息');
isAlive = true;
};
// 收到消息的事件处理程序
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 连接关闭的事件处理程序
socket.onclose = () => {
console.log('WebSocket连接已关闭');
clearInterval(pingIntervalID);
};
}
// 发送消息的函数
function sendMessage(message) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
} else {
console.log('WebSocket连接未打开');
}
}
connect(); // 启动connect函数
// 示范用法:发送消息
setTimeout(() => {
sendMessage('你好,WebSocket!');
}, 5000);
常用API
- 创建WebSocket对象:let socket = new WebSocket(url, protocols);
- url 参数是WebSocket服务的URL。这必须是绝对URL,并且必须以ws://或wss://为前缀(对应于HTTP和HTTPs)。
- protocols 参数是可选择的,用于定义子协议,这样服务器可以选择一个来通信。
- WebSocket事件:包含以下重要的事件处理器:
- socket.onopen:当连接成功建立后,触发此事件。通常用于一些连接成功后的初始化工作,例如发送一些初次登录或验证的信息。
- socket.onmessage:当从服务器接收到数据时,触发此事件。
- socket.onclose:当连接关闭时,触发此事件。
- socket.onerror:当连接发生错误时,触发此事件。例如,当服务器突然断电,或者服务器关闭了未完成的连接。
- 发送数据 :socket.send(data);
使用该方法,可以向服务器发送数据。数据可以是文本或者Blob对象或ArrayBuffer。 - 关闭连接 :socket.close(code, reason);
这里的code和reason参数都是可选的,分别代表关闭的状态码和原因。 - WebSocket对象的属性:包含:
- socket.readyState:返回当前连接的状态(只读),可以是:CONNECTING(0, 正在连接), OPEN(1, 连接已打开并准备好进行通信), CLOSING(2, 连接正在关闭),CLOSED(3, 连接已关闭或无法打开)。
- socket.bufferedAmount:返回还未发出的二进制数据字节数(只读)。这个属性在处理过大、阻塞数据的问题上非常有用。
三、参考资料
一文吃透 WebSocket 原理 刚面试完,趁热赶紧整理 - 掘金
WebSocket:5分钟从入门到精通 - 掘金
深入浅出Websocket(一)Websocket协议 - 掘金
掘金小册
WebSocket|概念、原理、用法及实践 - 掘金
www.cnblogs.com/jiujuan/p/1...