Vite 的热模块替换(HMR,Hot Module Replacement)能够在修改代码后,仅更新受影响的模块,而无需刷新整个页面,从而保留应用状态。
Vite HMR 是一个典型的服务端-客户端协作系统:
- 服务端:监听文件变化,重新编译模块,通过 WebSocket 推送更新消息。
- 客户端:接收消息,动态请求新模块,执行 HMR 回调,完成模块替换。
整个过程基于 原生 ES 模块,无需打包,按需编译,因此速度极快。
HTTP
node关于网络编程有 net、dgram、http、https
根据 TCP/IP 模型,层次关系如下:
| 层次 | 协议/模块 | 说明 |
|---|---|---|
| 应用层 | http / https | 定义应用程序如何交换数据(如 HTTP 请求/响应)。 |
| 表示层(TLS) | tls | 提供加密、身份验证,保障数据传输安全。 |
| 传输层 | net(TCP) | 提供可靠的、面向连接的字节流传输。 |
| 网络层 | IP | 路由和寻址(Node.js 中不直接暴露)。 |
创建 TCP 服务器
在 Node.js 中,使用内置的 net 模块可以轻松构建 TCP 服务器。TCP 是面向连接的、可靠的传输层协议,适合自定义协议通信、实时数据传输等场景。
const net = require('net');
// 创建 TCP 服务器
const server = net.createServer((socket) => {
console.log('客户端已连接,地址:', socket.remoteAddress, '端口:', socket.remotePort);
// 接收客户端数据
socket.on('data', (data) => {
const message = data.toString().trim();
console.log('收到消息:', message);
// 回显消息(可选)
socket.write(`服务端收到:${message}\n`);
});
// 连接关闭
socket.on('end', () => {
console.log('客户端已断开');
});
// 错误处理
socket.on('error', (err) => {
console.error('Socket 错误:', err.message);
});
});
// 监听端口
const PORT = 8080;
server.listen(PORT, () => {
console.log(`TCP 服务器运行在端口 ${PORT}`);
});
net.createServer([options][, connectionListener])
connectionListener 是连接事件 connection 的侦听器。也可以采用下面这种方式进行侦听。
const net = require('net');
const server = net.createServer();
server.on('connection', (socket) => {
console.log('客户端已连接,地址:', socket.remoteAddress, '端口:', socket.remotePort);
// 接收客户端数据
socket.on('data', (data) => {
const message = data.toString().trim();
console.log('收到消息:', message);
// 回显消息(可选)
socket.write(`服务端收到:${message}\n`);
});
// 连接关闭
socket.on('end', () => {
console.log('客户端已断开');
});
// 错误处理
socket.on('error', (err) => {
console.error('Socket 错误:', err.message);
});
});
// 监听端口
const PORT = 8080;
server.listen(PORT, () => {
console.log(`TCP 服务器运行在端口 ${PORT}`);
});
客户端
const net = require('net');
const client = net.createConnection({ port: 8080 }, () => {
console.log('已连接到服务器');
client.write('Hello, Server!');
});
client.on('data', (data) => {
console.log('服务端回复:', data.toString());
client.end(); // 发送完一次后关闭连接
});
client.on('error', (err) => {
console.error('客户端错误:', err.message);
});
client.on('close', () => {
console.log('连接已关闭');
});
WebSocket
在 WebSocket 出现之前,实现实时通信主要依赖以下技术:
- HTTP 轮询:客户端定时向服务器发送请求,检查是否有新数据。缺点:大量无效请求,延迟高。
- 长轮询:客户端发起请求,服务器保持连接直到有数据才返回,然后客户端立即发起下一次请求。虽然减少了请求次数,但仍有 HTTP 头部开销,且连接维持成本高。
- Server-Sent Events (SSE) :服务器单向推送,但无法双向通信。
WebSocket 通过一次 HTTP 握手,将连接升级为全双工、低延迟的持久连接,从根本上解决了实时通信的痛点。
浏览器 Websocket(原生API)
<script>
const socket = new WebSocket("ws://localhost:5177");
socket.onopen = (event) => {
console.log("-----连接成功-----", event);
};
socket.addEventListener("message", (event) => {
console.log("收到消息", event);
socket.send("client 已接收到消息!");
});
socket.addEventListener("error", (error) => {
console.log("错误", error);
});
socket.addEventListener("close", (event) => {
console.log("关闭", event);
});
</script>
当浏览器通过 WebSocket 与服务端通信时,Network 面板中不会为每一条消息单独显示新的请求。WebSocket 连接一旦建立(握手请求会显示在 Network 的 WS 标签中),后续的消息传输都以帧(frame) 的形式在同一个持久连接上进行,不会产生新的 HTTP 请求记录。
浏览器发送 WebSocket 请求的唯一时机是 JavaScript 代码显式执行 new WebSocket(url) 时。除此之外,浏览器不会自动发起 WebSocket 连接。
因此,接收服务端消息时,Network 中不会新增 ws 请求,但会在已建立连接的 Messages 面板中显示消息内容。这是 WebSocket 协议的特性:它是一个长连接,而非每个消息独立请求。
支持发送哪些数据类型?
浏览器 WebSocket 的 send 方法严格按照 WebSocket API 规范,只接受以下数据类型:
DOMString(字符串)BlobArrayBufferArrayBufferView(如Uint8Array)
如果传入一个普通 JavaScript 对象(如
{ foo: 'bar' }),浏览器并不会报错,而是会自动调用对象的toString方法,将其转换为字符串"[object Object]"后发送。因此,看到“能发送对象”的假象,实际发送的是无意义的字符串,而不是期望的 JSON 结构。
Node.js WebSocket 服务端(基于ws库)
const WebSocket = require("ws");
const wss = new WebSocket.Server({
port: 5177,
perMessageDeflate: true,
});
wss.on("connection", (ws, req) => {
// 1、验证来源 origin
// 2、验证协议
// 发送欢迎消息
ws.send(JSON.stringify({ type: "welcome", message: "连接成功!" }));
// 监听客户端消息
ws.on("message", (data) => {
console.log("来自client的消息", data.toString());
});
ws.on("close", () => {
console.log("关闭");
});
sendMessageToAll(ws);
console.log("connection success");
ws.on("error", (error) => {
console.log("错误", error);
});
});
const totle = 10;
let num = 0;
let intervalId;
function sendMessageToAll(ws) {
intervalId = setInterval(() => {
num++;
if (num >= totle) {
clearInterval(intervalId);
intervalId = null;
return;
}
ws.send(
JSON.stringify({
type: "time",
message: "num" + num + "当前时间:" + new Date().toLocaleString(),
}),
);
}, 1000);
}
创建 WebSocket 服务器时,只能从三种模式中选择一种:
- 监听指定端口。
- 附着到已有 HTTP/HTTPS 服务器。
- 不监听任何连接(手动处理升级)
// 模式1:监听指定端口
const wss = new WebSocket.Server({ port: 8080 });
// 模式2: 附着到已有 HTTP 服务器
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// 模式3: 监听连接(手动处理升级)
const wss = new WebSocket.Server({ noServer: true });
// 然后自己监听 server 的 'upgrade' 事件,调用 wss.handleUpgrade
Websocket 协议 与 传输
WebSocket 握手(升级协议)
客户端与服务器建立 WebSocket 连接时,会先发起一个标准的 HTTP 请求,这个请求包含一个Upgrade头,表明客户端希望从 HTTP 协议升级到 WebSocket 协议。服务器如果支持 WebSocket,会以 101 状态码响应,完成握手过程。
与普通的HTTP请求协议略有区别的部分在于如下这些协议头:
# 表示请求服务器端升级协议为WebSocket
Upgrade: websocket
Connection: Upgrade
# 用于安全校验
Sec-WebSocket-Key:dGh1IHNhbXBsZSBub25jZQ=-
# 指定协议和版本号
Sec-WebSocket-Protocol: chat,superchat
Sec-WebSocket-Version: 13
// 构建响应头:创建 101 状态码的响应头,指示协议切换
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${digest}`
];
报文告知客户端正在更换协议,更新应用层协议为 WebSocket 协议,并在当前的套接字连接上应用新协议。
剩余的字段分别表示服务器端基于 Sec-WebSocket-Key 生成的字符串和选中的子协议。
客户端将会校验 Sec-WebSocket-Accept 的值,如果成功,将开始接下来的数据传输。
WebSocket 关闭握手过程 (关闭websocket连接)
一、任意一方发起关闭
- 端点(客户端或服务器)发送一个 Close 控制帧(opcode =
0x08)。 - 该帧可以包含一个 状态码(如
1000表示正常关闭)和可选的 关闭原因(UTF-8 文本,不超过 125 字节)。 - 发送后,该端点不再发送任何数据帧,但必须继续接收数据直到收到对方的 Close 帧。
二、对方回复确认
- 接收方收到 Close 帧后,必须立即回复一个 Close 帧,且回复帧的状态码应与接收到的相同(通常如此)。 2.回复的 Close 帧发送完成后,接收方也可以关闭底层 TCP 连接。
三、关闭 TCP 连接
- 双方发送完 Close 帧后,等待一段时间(通常默认 2 秒)确保对方已收到,然后主动关闭 TCP 连接。
- 若一方在发送 Close 帧后未收到对方的 Close 帧(例如网络故障),会超时后强制关闭连接。
WebSocket 关闭帧中的状态码是一个 2 字节无符号整数,用于说明连接关闭的原因。根据 RFC 6455 及后续规范,常见状态码如下:
ws 库源码 Sender 消息发送器
lib/sender.js
/**
* Sends a close message to the other peer.
* 负责发送 WebSocket 关闭控制帧,
* 用于优雅地关闭 WebSocket 连接,是 WebSocket 协议关闭握手的重要组成部分。
*
* @param {Number} [code] The status code component of the body 关闭状态码,用于表示关闭的原因
* @param {(String|Buffer)} [data] The message component of the body 关闭原因的附加数据,通常是字符串或 Buffer
* @param {Boolean} [mask=false] Specifies whether or not to mask the message 是否对数据进行掩码处理
* @param {Function} [cb] Callback 发送完成后的回调函数
* @public
*/
close(code, data, mask, cb) {
let buf;
// 如果 code 未定义,使用空缓冲区
if (code === undefined) {
buf = EMPTY_BUFFER;
// 状态码验证:验证 code 是有效数字且是有效的状态码
} else if (typeof code !== 'number' || !isValidStatusCode(code)) {
throw new TypeError('First argument must be a valid error code number');
// 无附加数据:如果 data 未定义或长度为 0,只写入 2 字节状态码
} else if (data === undefined || !data.length) {
buf = Buffer.allocUnsafe(2);
buf.writeUInt16BE(code, 0);
// 有附加数据
} else {
const length = Buffer.byteLength(data); // 计算数据长度
// 验证长度不超过 123 字节(因为控制帧总长度不能超过 125 字节,2 字节状态码 + 123 字节数据)
if (length > 123) {
throw new RangeError('The message must not be greater than 123 bytes');
}
// 分配缓冲区并写入状态码和数据
buf = Buffer.allocUnsafe(2 + length);
buf.writeUInt16BE(code, 0);
// 处理字符串和 Buffer 类型的数据
if (typeof data === 'string') {
buf.write(data, 2);
} else {
buf.set(data, 2);
}
}
// 构建关闭帧的选项
const options = {
[kByteLength]: buf.length,
fin: true, // FIN 位:设置为 true,因为控制帧必须是完整的(不能分片)
generateMask: this._generateMask,
mask,
maskBuffer: this._maskBuffer,
opcode: 0x08, // 操作码:设置为 0x08,表示 Close 控制帧
readOnly: false,
rsv1: false // RSV1 位:设置为 false,因为 Close 控制帧不使用压缩
};
// 如果发送器状态不是默认状态,将任务加入队列
if (this._state !== DEFAULT) {
this.enqueue([this.dispatch, buf, false, options, cb]);
// 否则直接调用 sendFrame 发送关闭帧
} else {
this.sendFrame(Sender.frame(buf, options), cb);
}
}
Websocket 有哪些帧类型?
WebSocket 协议定义了多种帧类型,通过 操作码(Opcode) 区分,主要分为数据帧和控制帧两类。
所有帧都遵循统一的帧格式(FIN、RSV、Opcode、Mask、Payload len 等)。控制帧的载荷长度不能超过 125 字节,且不能分片。
一、数据帧(用于传输应用数据)
0x0连续帧:用于分片(一个消息拆分为多个帧,除首帧外的后续帧)。0x1文本帧:携带 UTF-8 编码的文本数据。0x2二进制帧:携带任意二进制数据(如 ArrayBuffer、Blob)。
二、控制帧(用于协议控制)
0x8关闭帧:发起关闭握手,可携带状态码和关闭原因。0x9Ping 帧:心跳探测,接收方必须回复 Pong 帧。0xAPong 帧:对 Ping 的响应,载荷须与 Ping 相同。
ws 库源码 Sender 消息发送器
负责根据 WebSocket 协议规范构建 WebSocket 帧,将数据转换为符合协议要求的二进制格式。
lib/sender.js
static frame(data, options) {
let mask; // 存储掩码
let merge = false; // 是否将数据合并到一个缓冲区
let offset = 2; // 帧头的偏移量,默认为 2(基本帧头长度)
let skipMasking = false; // 是否跳过掩码操作
// 1、处理掩码
if (options.mask) {
mask = options.maskBuffer || maskBuffer; // 取掩码缓冲区
// 生成掩码
if (options.generateMask) {
options.generateMask(mask);
// 从随机池中获取掩码
} else {
if (randomPoolPointer === RANDOM_POOL_SIZE) {
/* istanbul ignore else */
if (randomPool === undefined) {
//
// This is lazily initialized because server-sent frames must not
// be masked so it may never be used.
//
randomPool = Buffer.alloc(RANDOM_POOL_SIZE);
}
randomFillSync(randomPool, 0, RANDOM_POOL_SIZE);
randomPoolPointer = 0;
}
mask[0] = randomPool[randomPoolPointer++];
mask[1] = randomPool[randomPoolPointer++];
mask[2] = randomPool[randomPoolPointer++];
mask[3] = randomPool[randomPoolPointer++];
}
// 检查零掩码:如果掩码全为 0,跳过掩码操作
skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0;
offset = 6; // 调整偏移量:掩码占用 4 字节,所以偏移量增加到 6
}
// 2、数据长度处理
let dataLength;
// 字符串处理
if (typeof data === 'string') {
// 如果不需要掩码或跳过掩码,且提供了 kByteLength,使用提供的长度
// 将字符串转换为 Buffer 并获取长度
if (
(!options.mask || skipMasking) &&
options[kByteLength] !== undefined
) {
dataLength = options[kByteLength];
} else {
data = Buffer.from(data);
dataLength = data.length;
}
// 其他类型数据处理:直接获取长度
} else {
dataLength = data.length;
// 决定是否合并数据(当需要掩码、数据只读且不跳过掩码时)
merge = options.mask && options.readOnly && !skipMasking;
}
// 3、负载长度处理
let payloadLength = dataLength;
// 如果数据长度 ≥ 65536(即 64KB)
if (dataLength >= 65536) {
// 使用 8 字节扩展长度,payloadLength 设为 127
offset += 8;
payloadLength = 127;
// 如果数据长度 > 125 但 < 65536(即 64KB)
// 使用 2 字节扩展长度,payloadLength 设为 126
} else if (dataLength > 125) {
offset += 2;
payloadLength = 126;
}
// 4、构建帧头
// 创建一个指定大小的 Buffer,但不会初始化内存,用于存储帧头和数据
const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset);
/* 帧第一字节结构
0 1 2 3 4 5 6 7
+-+-+-+-+-------+
|F|R|R|R| opcode|
|I|S|S|S| |
|N|V|V|V| |
| |1|2|3| |
+-+-+-+-+-------+
*/
// 设置第一个字节(有8位)
// 0x80(二进制 1000 0000)
// opcode 只占低 4 位
// 按位或 | 操作
target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
// 0x40 是二进制 0100 0000,对应 RSV1 位(第二个最高位)
if (options.rsv1) target[0] |= 0x40;
/* 帧第二字节结构
0 1 2 3 4 5 6 7
+-+-+-+-+-------+
|M| Payload len |
|A| (7) |
|S| |
|K| |
+-+-+-+-+-------+
*/
// 设置第二个字节:设置 payloadLength(有8位,低7位有效位)
target[1] = payloadLength;
// 需要两个字节
if (payloadLength === 126) {
// 大端序是指高位字节存储在内存的低地址处,低位字节存储在高地址处。
// 将 dataLength 作为一个 16 位无符号整数,按大端序(Big-Endian,网络字节序)
// 写入 Buffer 的第 2 字节(即偏移量为 2 的位置)
target.writeUInt16BE(dataLength, 2);
// 需要八个字节
} else if (payloadLength === 127) {
// 将 buffer 索引 2 和 3 的两个字节设置为 0
// 对应于 8 字节扩展长度的前 2 个字节(高 16 位)
// 之所以清空,是因为实际长度 dataLength 用 48 位足以表示(最大支持 2^48 - 1 ≈ 281 万亿字节),高 16 位无需使用。
target[2] = target[3] = 0;
// writeUIntBE 是 Buffer 的方法,
// 用于按大端序写入一个整数,参数依次为:值、起始偏移量、字节数(1~6)。
target.writeUIntBE(dataLength, 4, 6); // 从偏移量 4 开始写入 6 个字节
}
// 处理掩码
// 1、无掩码情况:直接返回帧头和数据
if (!options.mask) return [target, data];
// 2、有掩码情况:设置掩码位 0x80 的二进制是 1000 0000
// 按位或操作会强制该位为 1,而低 7 位保持不变
target[1] |= 0x80;
/*
1、对于普通帧(payload length ≤ 125),帧头总共 6 字节:
字节 0:FIN+RSV+Opcode
字节 1:MASK+Payload len
字节 2-5:掩码密钥(4 字节)
字节 6 开始:载荷数据
*/
// 在 WebSocket 帧中,掩码密钥紧跟在扩展长度之后
target[offset - 4] = mask[0];
target[offset - 3] = mask[1];
target[offset - 2] = mask[2];
target[offset - 1] = mask[3];
// 如果跳过掩码,返回帧头和数据
if (skipMasking) return [target, data];
// 背景:当客户端发送帧时,必须对载荷数据应用掩码
// 如果合并数据,将掩码应用到数据并返回单个缓冲区
if (merge) {
// data: 原始数据(未掩码)
// mask: 4 字节掩码密钥
// target: 目标缓冲区, 将掩码后的数据写入 target 缓冲区
// offset: 目标缓冲区的起始偏移量
// dataLength: 要处理的数据长度
applyMask(data, mask, target, offset, dataLength);
// 只有一个完整的 Buffer(帧头 + 掩码后的数据)。调用者可直接将此 Buffer 写入 socket。
return [target];
}
// 将掩码应用到数据并返回帧头和掩码后的数据
// data: 原始数据(未掩码)
// mask: 4 字节掩码密钥
// data: 目标缓冲区, 将掩码后的数据写入 data 缓冲区
// dataLength: 要处理的数据长度
applyMask(data, mask, data, 0, dataLength);
// 帧头缓冲区和掩码后的载荷缓冲区分离。调用者需要自行合并或分别发送。
return [target, data];
}
为什么需要心跳检测?
心跳检测是一种保持连接活性的机制,通过定期发送少量数据(心跳包)来确认对方是否存活,并及时发现死连接。
- 检测对端是否存活:网络中断、进程崩溃等可能导致对方悄无声息地消失,心跳包可以快速发现。
- 防止中间设备断开连接:路由器、防火墙、负载均衡器等可能认为长时间无数据的连接已失效,主动断开。心跳包能保持连接活跃。
- 资源释放:及时关闭无效连接,释放服务端和客户端的资源。
心跳检测过程?
- 服务端主动发送 Ping(Node.js 的
ws库支持ws.ping())。 - 浏览器自动响应 Pong(浏览器 WebSocket API 无法主动发 Ping,但会自动回复收到的 Ping)。
- 如果服务端在超时时间内未收到 Pong,则认为连接已断开,触发
close或error事件,并可执行重连逻辑。
当协议层 Ping/Pong 无法满足需求(例如浏览器环境需要主动发送心跳,或需要携带业务数据),可以在应用层实现:
- 客户端定期发送
{ type: 'ping' }文本消息。 - 服务端回复
{ type: 'pong' }。 - 若一段时间未收到响应,则认为连接失效。
1、常见间隔:30 秒。
2、超时时间:通常为心跳间隔的 1.5-2 倍(如 30 秒心跳,超时设为 45~60 秒)。
ws库 处理 ping/pong 帧
ping(data, mask, cb) {
let byteLength;
let readOnly;
// 数据处理:根据数据类型进行转换
if (typeof data === 'string') {
// 对于字符串数据,直接计算字节数
byteLength = Buffer.byteLength(data);
readOnly = false;
} else if (isBlob(data)) {
byteLength = data.size;
readOnly = false;
} else {
data = toBuffer(data);
byteLength = data.length;
readOnly = toBuffer.readOnly;
}
// 数据长度校验:Ping 数据负载长度必须小于等于 125 字节
if (byteLength > 125) {
throw new RangeError('The data size must not be greater than 125 bytes');
}
// 构建 Ping 控制帧的选项
const options = {
[kByteLength]: byteLength,
// FIN 位:设置为 true,因为控制帧必须是完整的(不能分片)
fin: true,
generateMask: this._generateMask,
mask,
maskBuffer: this._maskBuffer,
opcode: 0x09, // 操作码:设置为 0x09,表示 Ping 控制帧
readOnly,
// RSV1 位:设置为 false,因为 Ping 帧不使用压缩
rsv1: false
};
// 选择处理路径
// 1、Blob 数据:需要先获取 Blob 数据的原始数据,然后进行掩码处理
if (isBlob(data)) {
// 状态非 DEFAULT:表示发送器当前正在处理其他发送任务(例如发送大数据帧尚未完成),
// 则将当前发送请求入队,避免并发冲突。
if (this._state !== DEFAULT) {
this.enqueue([this.getBlobData, data, false, options, cb]);
} else {
this.getBlobData(data, false, options, cb);
}
// 2、处于忙碌状态,将任务加入队列
} else if (this._state !== DEFAULT) {
this.enqueue([this.dispatch, data, false, options, cb]);
// 3、直接发送 Ping 控制帧
} else {
// 通过 sendFrame 写入底层 socket
this.sendFrame(Sender.frame(data, options), cb);
}
}
pong(data, mask, cb) {
let byteLength;
let readOnly;
// 数据处理:根据数据类型进行转换
if (typeof data === 'string') {
byteLength = Buffer.byteLength(data);
readOnly = false;
} else if (isBlob(data)) {
byteLength = data.size;
readOnly = false;
} else {
data = toBuffer(data);
byteLength = data.length;
readOnly = toBuffer.readOnly;
}
// 协议合规性检查:根据 WebSocket 协议规范,控制帧的负载长度不能超过 125 字节
if (byteLength > 125) {
throw new RangeError('The data size must not be greater than 125 bytes');
}
// 构建 Pong 控制帧的选项
const options = {
[kByteLength]: byteLength,
// FIN 位:设置为 true,因为控制帧必须是完整的(不能分片)
fin: true,
generateMask: this._generateMask,
mask,
maskBuffer: this._maskBuffer,
// 操作码:设置为 0x0a,表示 Pong 控制帧
opcode: 0x0a,
readOnly,
// RSV1 位:设置为 false,因为 Pong 帧不使用压缩
rsv1: false
};
// 选择处理路径:根据数据类型选择不同的处理逻辑
if (isBlob(data)) {
// 如果发送器状态不是默认状态,将任务加入队列
if (this._state !== DEFAULT) {
this.enqueue([this.getBlobData, data, false, options, cb]);
} else {
this.getBlobData(data, false, options, cb);
}
// 如果发送器状态不是默认状态,将任务加入队列
} else if (this._state !== DEFAULT) {
this.enqueue([this.dispatch, data, false, options, cb]);
} else {
// 直接发送 Pong 帧
this.sendFrame(Sender.frame(data, options), cb);
}
}
websocket 客户端发送的请求中 请求头有哪些?
一、必须的请求头
Upgrade,表明客户端希望升级到 WebSocket 协议。 示例值websocket。Connection,与Upgrade配合使用,指示当前连接需要升级。示例值Upgrade。Sec-WebSocket-Key,客户端随机生成的 16 字节 Base64 编码值,用于服务端校验握手合法性。Sec-WebSocket-Version,指定 WebSocket 协议版本,必须为13(RFC 6455)。
二、可选的请求头
Origin,请求来源,用于防止跨站 WebSocket 劫持(CSWSH)。服务端可据此进行安全校验。Sec-WebSocket-Protocol,客户端支持的子协议列表(用逗号分隔),服务端可从中选择一个通过响应头返回。用于应用层协议协商。Sec-WebSocket-Extensions,客户端希望使用的扩展(如压缩)。服务端可选择一个或全部,通过响应头返回。Host,服务端主机名和端口,与 HTTP 标准一致。Cookie,用于携带会话 Cookie,实现身份认证。Authorization,携带认证信息。
websocket 服务端响应头有哪些?
Upgrade,确认升级协议。Connection,确认连接升级。Sec-WebSocket-Accept,根据客户端Sec-WebSocket-Key计算得出。Sec-WebSocket-Protocol,如果客户端请求了子协议,服务端选择其中一个返回。Sec-WebSocket-Extensions,如果客户端请求了扩展,服务端确认使用的扩展。
Websocket 扩展 | permessage-deflate
1、WHY ? WebSocket 协议通过扩展(Extensions) 机制来增强其核心功能,例如压缩数据以节省带宽、多路复用以减少连接数。
2、WHAT ?permessage-deflate 是 WebSocket 协议中最常用的扩展,它使用 DEFLATE 压缩算法(同 gzip)对 WebSocket 消息的载荷(payload) 进行压缩,从而显著减少网络传输的数据量,节省带宽并提升速度。
3、HOW?浏览器端的 WebSocket API 默认会尝试协商并使用 permessage-deflate,只要服务器端也支持。不需要在客户端代码中做任何特殊配置。
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
const WebSocket = require("ws");
const wss = new WebSocket.Server({
port: 5177,
perMessageDeflate: true,
});
ws 库源码
'use strict';
const WebSocket = require('./lib/websocket');
WebSocket.createWebSocketStream = require('./lib/stream');
WebSocket.Server = require('./lib/websocket-server');
WebSocket.Receiver = require('./lib/receiver');
WebSocket.Sender = require('./lib/sender');
WebSocket.WebSocket = WebSocket;
WebSocket.WebSocketServer = WebSocket.Server;
module.exports = WebSocket;
lib/websocket-server.js
/**
* Class representing a WebSocket server.
*
* @extends EventEmitter
*/
class WebSocketServer extends EventEmitter {
/**
* Create a `WebSocketServer` instance.
*
* @param {Object} options Configuration options
* @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
* multiple times in the same tick
* @param {Boolean} [options.autoPong=true] Specifies whether or not to
* automatically send a pong in response to a ping
* @param {Number} [options.backlog=511] The maximum length of the queue of
* pending connections
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
* track clients
* @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
* wait for the closing handshake to finish after `websocket.close()` is
* called
* @param {Function} [options.handleProtocols] A hook to handle protocols
* @param {String} [options.host] The hostname where to bind the server
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
* size
* @param {Boolean} [options.noServer=false] Enable no server mode
* @param {String} [options.path] Accept only connections matching this path
* @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
* permessage-deflate
* @param {Number} [options.port] The port where to bind the server
* @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
* server to use
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
* not to skip UTF-8 validation for text and close messages
* @param {Function} [options.verifyClient] A hook to reject connections
* @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket`
* class to use. It must be the `WebSocket` class or class that extends it
* @param {Function} [callback] A listener for the `listening` event
*/
constructor(options, callback) {
super();
options = {
allowSynchronousEvents: true, // 是否允许同步事件触发
autoPong: true, // 是否自动发送 pong 响应
maxPayload: 100 * 1024 * 1024, // 最大消息大小,单位字节
skipUTF8Validation: false, // 是否跳过 UTF-8 验证
perMessageDeflate: false, // 是否启用消息压缩
handleProtocols: null, // 协议处理函数,用于处理升级请求
clientTracking: true, // 是否跟踪客户端连接
closeTimeout: CLOSE_TIMEOUT, // 关闭握手时的超时时间,单位毫秒
verifyClient: null, // 验证客户端连接的函数,用于拒绝连接
noServer: false, // 是否启用无服务器模式
backlog: null, // use default (511 as implemented in net.js)
server: null, // 预创建的 HTTP/S 服务器实例
host: null, // 主机名或 IP 地址,用于绑定服务器
path: null, // 路径,用于匹配 WebSocket 连接请求
port: null, // 端口号,用于绑定服务器
WebSocket,
...options
};
// 确保用户在创建 WebSocket 服务器时,只能从三种模式中选择一种:
// 1、监听指定端口、
// 2、附着到已有 HTTP/HTTPS 服务器、
// 3、不监听任何连接(手动处理升级)
if (
// 条件1:用户未指定端口,未指定 server,未指定 noServer
(options.port == null && !options.server && !options.noServer) ||
// 条件2:用户指定了端口,且指定了 server 或 noServer
(options.port != null && (options.server || options.noServer)) ||
// 条件3:用户指定了 server,且指定了 noServer
(options.server && options.noServer)
) {
// 有且只能指定一个选项,否则抛出 TypeError
// 说明:用户只能选择一种模式,不能同时指定 port、server 和 noServer
// 说明:用户可以指定 port 或 server,但不能同时指定 port 和 server
throw new TypeError(
'One and only one of the "port", "server", or "noServer" options ' +
'must be specified'
);
}
// 一、通过 port 创建新的 HTTP 服务器
if (options.port != null) {
// 通过 { port } 选项创建服务器时,会自动创建一个 HTTP 服务器并监听指定端口,
// 同时该 HTTP 服务器对所有非 WebSocket 请求返回 426 Upgrade Required 状态码
// 这个 HTTP 处理函数只会处理那些没有升级到 WebSocket 的普通 HTTP 请求。
this._server = http.createServer((req, res) => {
const body = http.STATUS_CODES[426];
// 对于普通 HTTP 请求,返回 426 Upgrade Required 状态码,并附带纯文本响应体 "Upgrade Required"。
res.writeHead(426, {
'Content-Length': body.length,
'Content-Type': 'text/plain'
});
res.end(body);
});
this._server.listen(
options.port,
options.host,
options.backlog,
callback
);
// 二、使用现有的 server 实例
} else if (options.server) {
this._server = options.server;
}
if (this._server) {
const emitConnection = this.emit.bind(this, 'connection');
this._removeListeners = addListeners(this._server, {
listening: this.emit.bind(this, 'listening'),
error: this.emit.bind(this, 'error'),
// 处理 HTTP 升级请求,调用 handleUpgrade 方法
upgrade: (req, socket, head) => {
this.handleUpgrade(req, socket, head, emitConnection);
}
});
}
// 消息压缩
if (options.perMessageDeflate === true) options.perMessageDeflate = {};
// 客户端跟踪
if (options.clientTracking) {
this.clients = new Set();
this._shouldEmitClose = false;
}
this.options = options;
this._state = RUNNING;
}
/**
* Returns the bound address, the address family name, and port of the server
* as reported by the operating system if listening on an IP socket.
* If the server is listening on a pipe or UNIX domain socket, the name is
* returned as a string.
*
* @return {(Object|String|null)} The address of the server
* @public
*/
address() {
if (this.options.noServer) {
throw new Error('The server is operating in "noServer" mode');
}
if (!this._server) return null;
return this._server.address();
}
/**
* Stop the server from accepting new connections and emit the `'close'` event
* when all existing connections are closed.
* 停止服务器接受新连接并在所有现有连接关闭后触发 close 事件。
*
* @param {Function} [cb] A one-time listener for the `'close'` event
* @public
*/
close(cb) {
// 如果服务器已关闭,直接触发 close 事件
if (this._state === CLOSED) {
if (cb) {
// 注册一个一次性的 close 事件监听器
this.once('close', () => {
cb(new Error('The server is not running'));
});
}
// 触发 close 事件
process.nextTick(emitClose, this);
return;
}
// 注册一个一次性的 close 事件监听器
if (cb) this.once('close', cb);
// 如果服务器已关闭,直接返回
if (this._state === CLOSING) return;
// 设置服务器状态为关闭中
this._state = CLOSING;
// 无服务器或外部服务器模式处理
if (this.options.noServer || this.options.server) {
if (this._server) {
// 移除监听器并设置为 null
this._removeListeners();
this._removeListeners = this._server = null;
}
if (this.clients) {
if (!this.clients.size) {
process.nextTick(emitClose, this);
} else {
this._shouldEmitClose = true;
}
} else {
process.nextTick(emitClose, this);
}
// 有服务器模式处理
} else {
const server = this._server;
this._removeListeners();
this._removeListeners = this._server = null;
//
// The HTTP/S server was created internally. Close it, and rely on its
// `'close'` event.
//
server.close(() => {
emitClose(this);
});
}
}
/**
* See if a given request should be handled by this server instance.
*
* @param {http.IncomingMessage} req Request object to inspect
* @return {Boolean} `true` if the request is valid, else `false`
* @public
*/
shouldHandle(req) {
if (this.options.path) {
const index = req.url.indexOf('?');
const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
if (pathname !== this.options.path) return false;
}
return true;
}
/**
* Handle a HTTP Upgrade request.
* 负责处理 HTTP 升级请求,将标准 HTTP 连接转换为 WebSocket 连接,是实现 WebSocket 握手的关键环节。
*
* @param {http.IncomingMessage} req The request object
* @param {Duplex} socket The network socket between the server and client
* @param {Buffer} head The first packet of the upgraded stream
* @param {Function} cb Callback
* @public
*/
handleUpgrade(req, socket, head, cb) {
// 为 socket 添加错误监听器,确保连接错误时能够正确处理
socket.on('error', socketOnError);
// 提取关键请求头信息 sec-websocket-key、upgrade、sec-websocket-version
const key = req.headers['sec-websocket-key']; // 用于安全握手的随机密钥
const upgrade = req.headers.upgrade; // 升级协议标识
const version = +req.headers['sec-websocket-version']; // WebSocket 协议版本
// 非 GET 方法,直接返回 405 状态码
if (req.method !== 'GET') {
const message = 'Invalid HTTP method';
abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
return;
}
// 非 WebSocket 升级请求,直接返回 400 状态码
if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
const message = 'Invalid Upgrade header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
// Sec-WebSocket-Key 头验证:确保 Sec-WebSocket-Key 头存在且值符合要求
if (key === undefined || !keyRegex.test(key)) {
const message = 'Missing or invalid Sec-WebSocket-Key header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
// 版本要求 13 或 8
if (version !== 13 && version !== 8) {
const message = 'Missing or invalid Sec-WebSocket-Version header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, {
'Sec-WebSocket-Version': '13, 8'
});
return;
}
// 路径不匹配
if (!this.shouldHandle(req)) {
abortHandshake(socket, 400);
return;
}
// 处理子协议选择
// 获取 Sec-WebSocket-Protocol 头值
const secWebSocketProtocol = req.headers['sec-websocket-protocol'];
let protocols = new Set();
if (secWebSocketProtocol !== undefined) {
try {
// 解析 Sec-WebSocket-Protocol 头值
protocols = subprotocol.parse(secWebSocketProtocol);
} catch (err) {
const message = 'Invalid Sec-WebSocket-Protocol header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
}
// 扩展处理
// 获取 Sec-WebSocket-Extensions 头值
const secWebSocketExtensions = req.headers['sec-websocket-extensions'];
const extensions = {};
// 如果启用了消息压缩且请求包含扩展头
if (
this.options.perMessageDeflate &&
secWebSocketExtensions !== undefined
) {
const perMessageDeflate = new PerMessageDeflate(
this.options.perMessageDeflate,
true,
this.options.maxPayload
);
try {
// 解析 Sec-WebSocket-Extensions 头值
const offers = extension.parse(secWebSocketExtensions);
// 如果客户端支持消息压缩,创建并配置 PerMessageDeflate 实例
if (offers[PerMessageDeflate.extensionName]) {
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
}
} catch (err) {
const message =
'Invalid or unacceptable Sec-WebSocket-Extensions header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
}
//
// Optionally call external client verification handler.
// 客户端验证
// 如果设置了验证函数,调用它进行验证
if (this.options.verifyClient) {
// 验证信息:提供包含 origin、安全性和请求对象的信息
const info = {
origin:
req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
secure: !!(req.socket.authorized || req.socket.encrypted),
req
};
if (this.options.verifyClient.length === 2) {
// 验证函数参数为 2 个,调用它进行验证
this.options.verifyClient(info, (verified, code, message, headers) => {
// 验证失败,返回 401 状态码
if (!verified) {
return abortHandshake(socket, code || 401, message, headers);
}
// 验证成功,继续升级握手
this.completeUpgrade(
extensions,
key,
protocols,
req,
socket,
head,
cb
);
});
return;
}
// 验证函数参数为 3 个,调用它进行验证
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
}
// 完成升级握手
this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
}
/**
* Upgrade the connection to WebSocket.
*
* @param {Object} extensions The accepted extensions
* @param {String} key The value of the `Sec-WebSocket-Key` header
* @param {Set} protocols The subprotocols
* @param {http.IncomingMessage} req The request object
* @param {Duplex} socket The network socket between the server and client
* @param {Buffer} head The first packet of the upgraded stream
* @param {Function} cb Callback
* @throws {Error} If called more than once with the same socket
* @private
*/
completeUpgrade(extensions, key, protocols, req, socket, head, cb) {
//
// Destroy the socket if the client has already sent a FIN packet.
// 连接有效性检查:确保 socket 仍可读可写,否则销毁连接
//
if (!socket.readable || !socket.writable) return socket.destroy();
// 重复处理检查:通过 kWebSocket 标记检查 socket 是否已被处理过,避免重复升级
if (socket[kWebSocket]) {
throw new Error(
'server.handleUpgrade() was called more than once with the same ' +
'socket, possibly due to a misconfiguration'
);
}
// 服务器状态检查:确保服务器处于运行状态,否则返回 503 错误
if (this._state > RUNNING) return abortHandshake(socket, 503);
// 2、生成握手协议
// 生成安全摘要:使用 SHA-1 算法对客户端提供的 key 和 GUID 进行哈希,生成 Sec-WebSocket-Accept 头
const digest = createHash('sha1')
.update(key + GUID)
.digest('base64');
// 构建响应头:创建 101 状态码的响应头,指示协议切换
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${digest}`
];
// 3、创建 WebSocket 实例 ()
const ws = new this.options.WebSocket(null, undefined, this.options);
// 3、处理子协议选择
if (protocols.size) {
//
// Optionally call external protocol selection handler.
//
const protocol = this.options.handleProtocols
? this.options.handleProtocols(protocols, req)
: protocols.values().next().value;
if (protocol) {
// 更新响应头:将选中的协议添加到响应头
headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
// 设置协议:在 WebSocket 实例上设置选中的协议
ws._protocol = protocol;
}
}
// 4、处理扩展
if (extensions[PerMessageDeflate.extensionName]) {
// 处理压缩扩展:如果启用了消息压缩,格式化扩展参数并添加到响应头
const params = extensions[PerMessageDeflate.extensionName].params;
const value = extension.format({
[PerMessageDeflate.extensionName]: [params]
});
headers.push(`Sec-WebSocket-Extensions: ${value}`);
// 设置扩展:在 WebSocket 实例上设置选中的扩展
ws._extensions = extensions;
}
//
// Allow external modification/inspection of handshake headers.
// 触发 headers 事件:允许外部代码修改或检查握手响应头
//
this.emit('headers', headers, req);
// 发送响应:将握手响应头写入 socket
socket.write(headers.concat('\r\n').join('\r\n'));
// 移除错误监听器:移除之前添加的临时错误监听器
socket.removeListener('error', socketOnError);
// 绑定 socket:将 socket 绑定到 WebSocket 实例
ws.setSocket(socket, head, {
allowSynchronousEvents: this.options.allowSynchronousEvents,
maxPayload: this.options.maxPayload,
skipUTF8Validation: this.options.skipUTF8Validation
});
if (this.clients) {
// 添加到客户端集合:如果启用了客户端跟踪,将新连接添加到 clients Set
this.clients.add(ws);
// 设置关闭处理:监听 WebSocket 关闭事件,从客户端集合中移除,并在需要时触发服务器关闭事件
ws.on('close', () => {
this.clients.delete(ws);
if (this._shouldEmitClose && !this.clients.size) {
process.nextTick(emitClose, this);
}
});
}
// 通知完成:调用回调函数,传递 WebSocket 实例和请求对象
cb(ws, req);
}
}
lib/websocket.js
/**
* Class representing a WebSocket.
*
* @extends EventEmitter
*/
class WebSocket extends EventEmitter {
/**
* Create a new `WebSocket`.
*
* @param {(String|URL)} address The URL to which to connect
* @param {(String|String[])} [protocols] The subprotocols
* @param {Object} [options] Connection options
*/
constructor(address, protocols, options) {
super();
this._binaryType = BINARY_TYPES[0]; // 二进制数据类型
this._closeCode = 1006; // 关闭码
this._closeFrameReceived = false; // 是否已接收关闭帧
this._closeFrameSent = false; // 是否已发送关闭帧
this._closeMessage = EMPTY_BUFFER; // 关闭消息缓冲区
this._closeTimer = null; // 关闭定时器
this._errorEmitted = false; // 是否已触发错误 事件
this._extensions = {}; // 扩展列表
this._paused = false; // 是否已暂停接收数据
this._protocol = ''; // 子协议
this._readyState = WebSocket.CONNECTING; // 连接状态
this._receiver = null; // 接收器实例
this._sender = null; // 发送器实例
this._socket = null; // 底层 socket 实例
// 客户端模式判断:当 address 不为 null 时,进入客户端模式
if (address !== null) {
this._bufferedAmount = 0; // 已缓冲数据量
this._isServer = false; // 是否为服务器端
this._redirects = 0; // 重定向次数
if (protocols === undefined) {
protocols = [];
} else if (!Array.isArray(protocols)) {
if (typeof protocols === 'object' && protocols !== null) {
options = protocols;
protocols = [];
} else {
protocols = [protocols];
}
}
// 初始化客户端模式
initAsClient(this, address, protocols, options);
// 服务器模式初始化
} else {
this._autoPong = options.autoPong; // 是否自动回复 PONG 帧
this._closeTimeout = options.closeTimeout; // 关闭超时时间
this._isServer = true; // 是否为服务器端
}
}
/**
* For historical reasons, the custom "nodebuffer" type is used by the default
* instead of "blob".
*
* @type {String}
*/
get binaryType() {
return this._binaryType;
}
set binaryType(type) {
if (!BINARY_TYPES.includes(type)) return;
this._binaryType = type;
//
// Allow to change `binaryType` on the fly.
//
if (this._receiver) this._receiver._binaryType = type;
}
/**
* @type {Number}
*/
get bufferedAmount() {
if (!this._socket) return this._bufferedAmount;
return this._socket._writableState.length + this._sender._bufferedBytes;
}
/**
* @type {String}
*/
get extensions() {
return Object.keys(this._extensions).join();
}
/**
* @type {Boolean}
*/
get isPaused() {
return this._paused;
}
/**
* @type {Function}
*/
/* istanbul ignore next */
get onclose() {
return null;
}
/**
* @type {Function}
*/
/* istanbul ignore next */
get onerror() {
return null;
}
/**
* @type {Function}
*/
/* istanbul ignore next */
get onopen() {
return null;
}
/**
* @type {Function}
*/
/* istanbul ignore next */
get onmessage() {
return null;
}
/**
* @type {String}
*/
get protocol() {
return this._protocol;
}
/**
* @type {Number}
*/
get readyState() {
return this._readyState;
}
/**
* @type {String}
*/
get url() {
return this._url;
}
/**
* Set up the socket and the internal resources.
* 用于设置 WebSocket 连接的底层 socket,完成从 HTTP 连接到 WebSocket 连接的转换
*
* @param {Duplex} socket The network socket between the server and client
* @param {Buffer} head The first packet of the upgraded stream
* @param {Object} options Options object
* @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
* multiple times in the same tick
* @param {Function} [options.generateMask] The function used to generate the
* masking key
* @param {Number} [options.maxPayload=0] The maximum allowed message size
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
* not to skip UTF-8 validation for text and close messages
* @private
*/
setSocket(socket, head, options) {
// 初始化接收器,负责接收和解析 WebSocket 帧
const receiver = new Receiver({
// 允许同步事件
allowSynchronousEvents: options.allowSynchronousEvents,
binaryType: this.binaryType, // 二进制类型
extensions: this._extensions, // 扩展
isServer: this._isServer, // 是否为服务器端
maxPayload: options.maxPayload, // 最大消息大小
skipUTF8Validation: options.skipUTF8Validation // 是否跳过 UTF-8 验证
});
// 初始化发送器,负责发送 WebSocket 帧
const sender = new Sender(socket, this._extensions, options.generateMask);
// 保存引用,建立双向引用
this._receiver = receiver;
this._sender = sender;
this._socket = socket;
receiver[kWebSocket] = this;
sender[kWebSocket] = this;
socket[kWebSocket] = this;
// 注册接收器事件监听器
receiver.on('conclude', receiverOnConclude); // 处理连接结束
receiver.on('drain', receiverOnDrain); // 处理缓冲区清空
receiver.on('error', receiverOnError); // 处理错误
receiver.on('message', receiverOnMessage); // 处理消息
receiver.on('ping', receiverOnPing); // 处理 ping 帧
receiver.on('pong', receiverOnPong); // 处理 pong 帧
// 注册发送器错误处理
sender.onerror = senderOnError;
//
// These methods may not be available if `socket` is just a `Duplex`.
//
if (socket.setTimeout) socket.setTimeout(0); // 设置超时时间为 0,禁用超时
if (socket.setNoDelay) socket.setNoDelay(); // 禁用 Nagle 算法
// 如果 head 中有数据(通常是 HTTP 升级后的剩余数据),将其推回 socket 缓冲区,以便接收器处理
if (head.length > 0) socket.unshift(head);
// 监听底层 socket 事件
socket.on('close', socketOnClose);
socket.on('data', socketOnData);
socket.on('end', socketOnEnd); // 处理 socket 结束
socket.on('error', socketOnError);
this._readyState = WebSocket.OPEN;
this.emit('open'); // 触发 open 事件,通知连接已打开
}
/**
* Emit the `'close'` event.
* 用于触发 WebSocket 关闭事件并清理相关资源
*
* @private
*/
emitClose() {
// 无 socket 情况处理
if (!this._socket) {
this._readyState = WebSocket.CLOSED; // 标记为已关闭
// 触发事件:触发 close 事件,传递关闭码和关闭消息
this.emit('close', this._closeCode, this._closeMessage);
return;
}
if (this._extensions[PerMessageDeflate.extensionName]) {
// 清理扩展
this._extensions[PerMessageDeflate.extensionName].cleanup();
}
this._receiver.removeAllListeners(); // 移除监听器
this._readyState = WebSocket.CLOSED; // 标记为已关闭
// 触发事件:触发 close 事件,传递关闭码和关闭消息
this.emit('close', this._closeCode, this._closeMessage);
}
/**
* 用于正常关闭 WebSocket 连接,通过发送关闭帧并等待对方确认,实现标准的 WebSocket 关闭握手过程。
* Start a closing handshake.
*
* +----------+ +-----------+ +----------+
* - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
* | +----------+ +-----------+ +----------+ |
* +----------+ +-----------+ |
* CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING
* +----------+ +-----------+ |
* | | | +---+ |
* +------------------------+-->|fin| - - - -
* | +---+ | +---+
* - - - - -|fin|<---------------------+
* +---+
*
* @param {Number} [code] Status code explaining why the connection is closing
* @param {(String|Buffer)} [data] The reason why the connection is
* closing
* @public
*/
close(code, data) {
// 如果 WebSocket 已关闭,直接返回
if (this.readyState === WebSocket.CLOSED) return;
// 如果 WebSocket 正在连接中,
if (this.readyState === WebSocket.CONNECTING) {
const msg = 'WebSocket was closed before the connection was established';
// 中断握手过程,传递错误信息
abortHandshake(this, this._req, msg);
return;
}
// 如果 WebSocket 正在关闭中,
if (this.readyState === WebSocket.CLOSING) {
if (
// 已发送了关闭帧
this._closeFrameSent &&
// 已经收到了对方的关闭帧 或者 接收器发生了错误
(this._closeFrameReceived || this._receiver._writableState.errorEmitted)
) {
// 结束底层 socket 连接
this._socket.end();
}
return;
}
this._readyState = WebSocket.CLOSING;
// 发送关闭帧
this._sender.close(code, data, !this._isServer, (err) => {
//
// This error is handled by the `'error'` listener on the socket. We only
// want to know if the close frame has been sent here.
//
// 如果发送过程中出现错误,直接返回
if (err) return;
this._closeFrameSent = true; // 标记为已发送关闭帧
if (
this._closeFrameReceived || // 已经收到了对方的关闭帧
this._receiver._writableState.errorEmitted // 接收器发生了错误
) {
// 结束底层 socket 连接
this._socket.end();
}
});
// 设置关闭定时器
setCloseTimer(this);
}
/**
* Pause the socket.
*
* @public
*/
pause() {
// 如果 WebSocket 正在连接中或已关闭,直接返回
if (
this.readyState === WebSocket.CONNECTING ||
this.readyState === WebSocket.CLOSED
) {
return;
}
this._paused = true; // 标记为已暂停
// 暂停 socket 的写入操作
this._socket.pause();
}
/**
* Send a ping.
* 用于发送 WebSocket ping 帧,
* 主要用于测试连接是否活跃、作为心跳机制的一部分,以及防止连接因超时而被关闭。
*
* @param {*} [data] The data to send
* @param {Boolean} [mask] Indicates whether or not to mask `data`
* @param {Function} [cb] Callback which is executed when the ping is sent
* @public
*/
ping(data, mask, cb) {
// 抛出错误:如果 WebSocket 正在连接中,不能发送 ping 消息
if (this.readyState === WebSocket.CONNECTING) {
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
}
// 参数重载处理
if (typeof data === 'function') {
cb = data; // 如果 data 是一个函数,将其作为回调函数
data = mask = undefined; // 将 data 和 mask 设置为 undefined
} else if (typeof mask === 'function') {
cb = mask; // 如果 mask 是一个函数,将其作为回调函数
mask = undefined; // 将 mask 设置为 undefined
}
// 如果 data 是一个数字,将其转换为字符串
if (typeof data === 'number') data = data.toString();
// 如果 WebSocket 不是已打开状态,将数据发送到关闭队列
if (this.readyState !== WebSocket.OPEN) {
sendAfterClose(this, data, cb);
return;
}
// 如果 mask 未定义,根据是否是服务器端来决定默认值
// 客户端:mask = true(客户端发送的帧需要掩码)
// 服务器端:mask = false(服务器发送的帧不需要掩码)
if (mask === undefined) mask = !this._isServer;
// 发送 ping 帧
this._sender.ping(data || EMPTY_BUFFER, mask, cb);
}
/**
* Send a pong.
* 用于发送 WebSocket pong 帧,
* 主要用于响应 ping 帧或作为心跳机制的一部分,维持 WebSocket 连接的活跃状态。
*
* @param {*} [data] The data to send
* @param {Boolean} [mask] Indicates whether or not to mask `data`
* @param {Function} [cb] Callback which is executed when the pong is sent
* @public
*/
pong(data, mask, cb) {
// 抛出错误:如果 WebSocket 正在连接中,不能发送 pong 消息
if (this.readyState === WebSocket.CONNECTING) {
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
}
// 参数重载处理
if (typeof data === 'function') {
cb = data; // 如果 data 是一个函数,将其作为回调函数
data = mask = undefined; // 将 data 和 mask 设置为 undefined
} else if (typeof mask === 'function') {
cb = mask; // 如果 mask 是一个函数,将其作为回调函数
mask = undefined; // 将 mask 设置为 undefined
}
// 如果 data 是一个数字,将其转换为字符串
if (typeof data === 'number') data = data.toString();
// 如果 WebSocket 不是已打开状态,将数据发送到关闭队列
if (this.readyState !== WebSocket.OPEN) {
sendAfterClose(this, data, cb);
return;
}
// 如果 mask 未定义,根据是否是服务器端来决定默认值
// 客户端:mask = true(客户端发送的帧需要掩码)
// 服务器端:mask = false(服务器发送的帧不需要掩码)
if (mask === undefined) mask = !this._isServer;
this._sender.pong(data || EMPTY_BUFFER, mask, cb);
}
/**
* Resume the socket.
*
* @public
*/
resume() {
// 连接状态检查:如果 WebSocket 已关闭或正在连接,直接返回
if (
this.readyState === WebSocket.CONNECTING ||
this.readyState === WebSocket.CLOSED
) {
return;
}
// 将暂停状态设置为 false
this._paused = false;
// 如果接收缓冲区不需要刷新,恢复 socket 连接
if (!this._receiver._writableState.needDrain) this._socket.resume();
}
/**
* Send a data message.
*
* @param {*} data The message to send
* @param {Object} [options] Options object
* @param {Boolean} [options.binary] Specifies whether `data` is binary or
* text
* @param {Boolean} [options.compress] Specifies whether or not to compress
* `data`
* @param {Boolean} [options.fin=true] Specifies whether the fragment is the
* last one
* @param {Boolean} [options.mask] Specifies whether or not to mask `data`
* @param {Function} [cb] Callback which is executed when data is written out
* @public
*/
send(data, options, cb) {
if (this.readyState === WebSocket.CONNECTING) {
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
}
if (typeof options === 'function') {
cb = options;
options = {};
}
if (typeof data === 'number') data = data.toString();
if (this.readyState !== WebSocket.OPEN) {
sendAfterClose(this, data, cb);
return;
}
const opts = {
binary: typeof data !== 'string',
mask: !this._isServer,
compress: true,
fin: true,
...options
};
if (!this._extensions[PerMessageDeflate.extensionName]) {
opts.compress = false;
}
this._sender.send(data || EMPTY_BUFFER, opts, cb);
}
/**
* Forcibly close the connection.
* 强制终止websocket连接,不经过正常的关闭握手过程,直接销毁底层的 socket 连接。
*
* @public
*/
terminate() {
// 连接状态检查:如果 WebSocket 已关闭或正在连接,直接返回
if (this.readyState === WebSocket.CLOSED) return;
if (this.readyState === WebSocket.CONNECTING) {
const msg = 'WebSocket was closed before the connection was established';
abortHandshake(this, this._req, msg);
return;
}
// 如果 WebSocket 已经建立连接
if (this._socket) {
this._readyState = WebSocket.CLOSING;
// 销毁底层的 socket 连接
this._socket.destroy();
}
}
}