总结
=============================================================
从转行到现在,差不多两年的时间,虽不能和大佬相比,但也是学了很多东西。我个人在学习的过程中,习惯简单做做笔记,方便自己复习的时候能够快速理解,现在将自己的笔记分享出来,和大家共同学习。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
个人将这段时间所学的知识,分为三个阶段:
第一阶段:HTML&CSS&JavaScript基础
第二阶段:移动端开发技术
第三阶段:前端常用框架
-
推荐学习方式:针对某个知识点,可以先简单过一下我的笔记,如果理解,那是最好,可以帮助快速解决问题;如果因为我的笔记太过简陋不理解,可以关注我以后我还会继续分享。
-
大厂的面试难在,针对一个基础知识点,比如JS的事件循环机制,不会上来就问概念,而是换个角度,从题目入手,看你是否真正掌握。所以对于概念的理解真的很重要。
2.1 构造函数
WebSocket 构造函数的语法为:
const myWebSocket = new WebSocket(url [, protocols]);
相关参数说明如下:
- url:表示连接的 URL,这是 WebSocket 服务器将响应的 URL。
- protocols(可选):一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议。比如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互。如果不指定协议字符串,则假定为空字符串。
当尝试连接的端口被阻止时,会抛出 SECURITY_ERR
异常。
2.2 属性
WebSocket 对象包含以下属性:
每个属性的具体含义如下:
-
binaryType:使用二进制的数据类型连接。
-
bufferedAmount(只读):未发送至服务器的字节数。
-
extensions(只读):服务器选择的扩展。
-
onclose:用于指定连接关闭后的回调函数。
-
onerror:用于指定连接失败后的回调函数。
-
onmessage:用于指定当从服务器接受到信息时的回调函数。
-
onopen:用于指定连接成功后的回调函数。
-
protocol(只读):用于返回服务器端选中的子协议的名字。
-
readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态:
- CONNECTING — 正在连接中,对应的值为 0;
- OPEN — 已经连接并且可以通讯,对应的值为 1;
- CLOSING — 连接正在关闭,对应的值为 2;
- CLOSED — 连接已关闭或者没有连接成功,对应的值为 3。
-
url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径。
2.3 方法
- close([code[, reason]]):该方法用于关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作。
- send(data):该方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值 。若数据无法传输(比如数据需要缓存而缓冲区已满)时,套接字会自行关闭。
2.4 事件
使用 addEventListener() 或将一个事件监听器赋值给 WebSocket 对象的 oneventname 属性,来监听下面的事件。
- close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置。
- error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置。
- message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置。
- open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置。
介绍完 WebSocket API,我们来举一个使用 WebSocket 发送普通文本的示例。
2.5 发送普通文本
在以上示例中,我们在页面上创建了两个 textarea,分别用于存放 待发送的数据 和 服务器返回的数据。当用户输入完待发送的文本之后,点击 发送 按钮时会把输入的文本发送到服务端,而服务端成功接收到消息之后,会把收到的消息原封不动地回传到客户端。
// const socket = new WebSocket("ws://echo.websocket.org");
// const sendMsgContainer = document.querySelector("#sendMessage");
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("连接未建立,还不能发送消息");
return;
}
if (message) socket.send(message);
}
当然客户端接收到服务端返回的消息之后,会把对应的文本内容保存到 接收的数据 对应的 textarea 文本框中。
// const socket = new WebSocket("ws://echo.websocket.org");
// const receivedMsgContainer = document.querySelector("#receivedMessage");
socket.addEventListener("message", function (event) {
console.log("Message from server ", event.data);
receivedMsgContainer.value = event.data;
});
为了更加直观地理解上述的数据交互过程,我们使用 Chrome 浏览器的开发者工具来看一下相应的过程:
以上示例对应的完整代码如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket 发送普通文本示例</title>
<style>
.block {
flex: 1;
}
</style>
</head>
<body>
<h3>阿宝哥:WebSocket 发送普通文本示例</h3>
<div style="display: flex;">
<div class="block">
<p>即将发送的数据:<button onclick="send()">发送</button></p>
<textarea id="sendMessage" rows="5" cols="15"></textarea>
</div>
<div class="block">
<p>接收的数据:</p>
<textarea id="receivedMessage" rows="5" cols="15"></textarea>
</div>
</div>
<script>
const sendMsgContainer = document.querySelector("#sendMessage");
const receivedMsgContainer = document.querySelector("#receivedMessage");
const socket = new WebSocket("ws://echo.websocket.org");
// 监听连接成功事件
socket.addEventListener("open", function (event) {
console.log("连接成功,可以开始通讯");
});
// 监听消息
socket.addEventListener("message", function (event) {
console.log("Message from server ", event.data);
receivedMsgContainer.value = event.data;
});
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("连接未建立,还不能发送消息");
return;
}
if (message) socket.send(message);
}
</script>
</body>
</html>
其实 WebSocket 除了支持发送普通的文本之外,它还支持发送二进制数据,比如 ArrayBuffer 对象、Blob 对象或者 ArrayBufferView 对象:
const socket = new WebSocket("ws://echo.websocket.org");
socket.onopen = function () {
// 发送UTF-8编码的文本信息
socket.send("Hello Echo Server!");
// 发送UTF-8编码的JSON数据
socket.send(JSON.stringify({ msg: "我是阿宝哥" }));
// 发送二进制ArrayBuffer
const buffer = new ArrayBuffer(128);
socket.send(buffer);
// 发送二进制ArrayBufferView
const intview = new Uint32Array(buffer);
socket.send(intview);
// 发送二进制Blob
const blob = new Blob([buffer]);
socket.send(blob);
};
以上代码成功运行后,通过 Chrome 开发者工具,我们可以看到对应的数据交互过程:
下面阿宝哥以发送 Blob 对象为例,来介绍一下如何发送二进制数据。
Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。
对 Blob 感兴趣的小伙伴,可以阅读 “你不知道的 Blob” 这篇文章。
2.6 发送二进制数据
在以上示例中,我们在页面上创建了两个 textarea,分别用于存放 待发送的数据 和 服务器返回的数据。当用户输入完待发送的文本之后,点击 发送 按钮时,我们会先获取输入的文本并把文本包装成 Blob 对象然后发送到服务端,而服务端成功接收到消息之后,会把收到的消息原封不动地回传到客户端。
当浏览器接收到新消息后,如果是文本数据,会自动将其转换成 DOMString 对象,如果是二进制数据或 Blob 对象,会直接将其转交给应用,由应用自身来根据返回的数据类型进行相应的处理。
数据发送代码
// const socket = new WebSocket("ws://echo.websocket.org");
// const sendMsgContainer = document.querySelector("#sendMessage");
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("连接未建立,还不能发送消息");
return;
}
const blob = new Blob([message], { type: "text/plain" });
if (message) socket.send(blob);
console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`);
}
当然客户端接收到服务端返回的消息之后,会判断返回的数据类型,如果是 Blob 类型的话,会调用 Blob 对象的 text() 方法,获取 Blob 对象中保存的 UTF-8 格式的内容,然后把对应的文本内容保存到 接收的数据 对应的 textarea 文本框中。
数据接收代码
// const socket = new WebSocket("ws://echo.websocket.org");
// const receivedMsgContainer = document.querySelector("#receivedMessage");
socket.addEventListener("message", async function (event) {
console.log("Message from server ", event.data);
const receivedData = event.data;
if (receivedData instanceof Blob) {
receivedMsgContainer.value = await receivedData.text();
} else {
receivedMsgContainer.value = receivedData;
}
});
同样,我们使用 Chrome 浏览器的开发者工具来看一下相应的过程:
通过上图我们可以很明显地看到,当使用发送 Blob 对象时,Data 栏位的信息显示的是 Binary Message,而对于发送普通文本来说,Data 栏位的信息是直接显示发送的文本消息。
以上示例对应的完整代码如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket 发送二进制数据示例</title>
<style>
.block {
flex: 1;
}
</style>
</head>
<body>
<h3>阿宝哥:WebSocket 发送二进制数据示例</h3>
<div style="display: flex;">
<div class="block">
<p>待发送的数据:<button onclick="send()">发送</button></p>
<textarea id="sendMessage" rows="5" cols="15"></textarea>
</div>
<div class="block">
<p>接收的数据:</p>
<textarea id="receivedMessage" rows="5" cols="15"></textarea>
</div>
</div>
<script>
const sendMsgContainer = document.querySelector("#sendMessage");
const receivedMsgContainer = document.querySelector("#receivedMessage");
const socket = new WebSocket("ws://echo.websocket.org");
// 监听连接成功事件
socket.addEventListener("open", function (event) {
console.log("连接成功,可以开始通讯");
});
// 监听消息
socket.addEventListener("message", async function (event) {
console.log("Message from server ", event.data);
const receivedData = event.data;
if (receivedData instanceof Blob) {
receivedMsgContainer.value = await receivedData.text();
} else {
receivedMsgContainer.value = receivedData;
}
});
function send() {
const message = sendMsgContainer.value;
if (socket.readyState !== WebSocket.OPEN) {
console.log("连接未建立,还不能发送消息");
return;
}
const blob = new Blob([message], { type: "text/plain" });
if (message) socket.send(blob);
console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`);
}
</script>
</body>
</html>
可能有一些小伙伴了解完 WebSocket API 之后,觉得还不够过瘾。下面阿宝哥将带大家来实现一个支持发送普通文本的 WebSocket 服务器。
三、手写 WebSocket 服务器
在介绍如何手写 WebSocket 服务器前,我们需要了解一下 WebSocket 连接的生命周期。
从上图可知,在使用 WebSocket 实现全双工通信之前,客户端与服务器之间需要先进行握手(Handshake),在完成握手之后才能开始进行数据的双向通信。
握手是在通信电路创建之后,信息传输开始之前。握手用于达成参数,如信息传输率,字母表,奇偶校验,中断过程,和其他协议特性。 握手有助于不同结构的系统或设备在通信信道中连接,而不需要人为设置参数。
既然握手是 WebSocket 连接生命周期的第一个环节,接下来我们就先来分析 WebSocket 的握手协议。
3.1 握手协议
WebSocket 协议属于应用层协议,它依赖于传输层的 TCP 协议。WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手。为了创建 WebSocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为 “握手”(Handshaking)。
利用 HTTP 完成握手有几个好处。首先,让 WebSocket 与现有 HTTP 基础设施兼容:使得 WebSocket 服务器可以运行在 80 和 443 端口上,这通常是对客户端唯一开放的端口。其次,让我们可以重用并扩展 HTTP 的 Upgrade 流,为其添加自定义的 WebSocket 首部,以完成协商。
下面我们以前面已经演示过的发送普通文本的例子为例,来具体分析一下握手过程。
3.1.1 客户端请求
GET ws://echo.websocket.org/ HTTP/1.1
Host: echo.websocket.org
Origin: file://
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: Zx8rNEkBE4xnwifpuh8DHQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
备注:已忽略部分 HTTP 请求头
字段说明
- Connection 必须设置 Upgrade,表示客户端希望连接升级。
- Upgrade 字段必须设置 websocket,表示希望升级到 WebSocket 协议。
- Sec-WebSocket-Version 表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
- Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 WebSocket 协议。
- Sec-WebSocket-Extensions 用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一个或多个扩展。
- Origin 字段是可选的,通常用来表示在浏览器中发起此 WebSocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。
3.1.2 服务端响应
HTTP/1.1 101 Web Socket Protocol Handshake ①
Connection: Upgrade ②
Upgrade: websocket ③
Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④
备注:已忽略部分 HTTP 响应头
- ① 101 响应码确认升级到 WebSocket 协议。
- ② 设置 Connection 头的值为 “Upgrade” 来指示这是一个升级请求。HTTP 协议提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议。
- ③ Upgrade 头指定一项或多项协议名,按优先级排序,以逗号分隔。这里表示升级为 WebSocket 协议。
- ④ 签名的键值验证协议支持。
介绍完 WebSocket 的握手协议,接下来阿宝哥将使用 Node.js 来开发我们的 WebSocket 服务器。
3.2 实现握手功能
要开发一个 WebSocket 服务器,首先我们需要先实现握手功能,这里阿宝哥使用 Node.js 内置的 http 模块来创建一个 HTTP 服务器,具体代码如下所示:
const http = require("http");
const port = 8888;
const { generateAcceptValue } = require("./util");
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("大家好,我是阿宝哥。感谢你阅读“你不知道的WebSocket”");
});
server.on("upgrade", function (req, socket) {
if (req.headers["upgrade"] !== "websocket") {
socket.end("HTTP/1.1 400 Bad Request");
return;
}
// 读取客户端提供的Sec-WebSocket-Key
const secWsKey = req.headers["sec-websocket-key"];
// 使用SHA-1算法生成Sec-WebSocket-Accept
const hash = generateAcceptValue(secWsKey);
// 设置HTTP响应头
const responseHeaders = [
"HTTP/1.1 101 Web Socket Protocol Handshake",
"Upgrade: WebSocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${hash}`,
];
// 返回握手请求的响应信息
socket.write(responseHeaders.join("\r\n") + "\r\n\r\n");
});
server.listen(port, () =>
console.log(`Server running at http://localhost:${port}`)
);
在以上代码中,我们首先引入了 http 模块,然后通过调用该模块的 createServer()
方法创建一个 HTTP 服务器,接着我们监听 upgrade
事件,每次服务器响应升级请求时就会触发该事件。由于我们的服务器只支持升级到 WebSocket 协议,所以如果客户端请求升级的协议非 WebSocket 协议,我们将会返回 “400 Bad Request”。
当服务器接收到升级为 WebSocket 的握手请求时,会先从请求头中获取 “Sec-WebSocket-Key” 的值,然后把该值加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。
上述的过程看起来好像有点繁琐,其实利用 Node.js 内置的 crypto 模块,几行代码就可以搞定了:
// util.js
const crypto = require("crypto");
const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
function generateAcceptValue(secWsKey) {
return crypto
.createHash("sha1")
.update(secWsKey + MAGIC_KEY, "utf8")
.digest("base64");
}
开发完握手功能之后,我们可以使用前面的示例来测试一下该功能。待服务器启动之后,我们只要对 “发送普通文本” 示例,做简单地调整,即把先前的 URL 地址替换成 ws://localhost:8888
,就可以进行功能验证。
感兴趣的小伙们可以试试看,以下是阿宝哥本地运行后的结果:
从上图可知,我们实现的握手功能已经可以正常工作了。那么握手有没有可能失败呢?答案是肯定的。比如网络问题、服务器异常或 Sec-WebSocket-Accept 的值不正确。
下面阿宝哥修改一下 “Sec-WebSocket-Accept” 生成规则,比如修改 MAGIC_KEY 的值,然后重新验证一下握手功能。此时,浏览器的控制台会输出以下异常信息:
WebSocket connection to 'ws://localhost:8888/' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value
如果你的 WebSocket 服务器要支持子协议的话,你可以参考以下代码进行子协议的处理,阿宝哥就不继续展开介绍了。
// 从请求头中读取子协议
const protocol = req.headers["sec-websocket-protocol"];
// 如果包含子协议,则解析子协议
const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim());
// 简单起见,我们仅判断是否含有JSON子协议
if (protocols.includes("json")) {
responseHeaders.push(`Sec-WebSocket-Protocol: json`);
}
好的,WebSocket 握手协议相关的内容基本已经介绍完了。下一步我们来介绍开发消息通信功能需要了解的一些基础知识。
3.3 消息通信基础
在 WebSocket 协议中,数据是通过一系列数据帧来进行传输的。为了避免由于网络中介(例如一些拦截代理)或者一些安全问题,客户端必须在它发送到服务器的所有帧中添加掩码。服务端收到没有添加掩码的数据帧以后,必须立即关闭连接。
3.3.1 数据帧格式
要实现消息通信,我们就必须了解 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 ... |
+---------------------------------------------------------------+
可能有一些小伙伴看到上面的内容之后,就开始有点 “懵逼” 了。下面我们来结合实际的数据帧来进一步分析一下:
在上图中,阿宝哥简单分析了 “发送普通文本” 示例对应的数据帧格式。这里我们来进一步介绍一下 Payload length,因为在后面开发数据解析功能的时候,需要用到该知识点。
Payload length 表示以字节为单位的 “有效负载数据” 长度。它有以下几种情形:
- 如果值为 0-125,那么就表示负载数据的长度。
- 如果是 126,那么接下来的 2 个字节解释为 16 位的无符号整形作为负载数据的长度。
- 如果是 127,那么接下来的 8 个字节解释为一个 64 位的无符号整形(最高位的 bit 必须为 0)作为负载数据的长度。
多字节长度量以网络字节顺序表示,有效负载长度是指 “扩展数据” + “应用数据” 的长度。“扩展数据” 的长度可能为 0,那么有效负载长度就是 “应用数据” 的长度。
另外,除非协商过扩展,否则 “扩展数据” 长度为 0 字节。在握手协议中,任何扩展都必须指定 “扩展数据” 的长度,这个长度如何进行计算,以及这个扩展如何使用。如果存在扩展,那么这个 “扩展数据” 包含在总的有效负载长度中。
3.3.2 掩码算法
掩码字段是一个由客户端随机选择的 32 位的值。掩码值必须是不可被预测的。因此,掩码必须来自强大的熵源(entropy),并且给定的掩码不能让服务器或者代理能够很容易的预测到后续帧。掩码的不可预测性对于预防恶意应用的作者在网上暴露相关的字节数据至关重要。
掩码不影响数据荷载的长度,对数据进行掩码操作和对数据进行反掩码操作所涉及的步骤是相同的。掩码、反掩码操作都采用如下算法:
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
- original-octet-i:为原始数据的第 i 字节。
- transformed-octet-i:为转换后的数据的第 i 字节。
- masking-key-octet-j:为 mask key 第 j 字节。
为了让小伙伴们能够更好的理解上面掩码的计算过程,我们来对示例中 “我是阿宝哥” 数据进行掩码操作。这里 “我是阿宝哥” 对应的 UTF-8 编码如下所示:
E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5
而对应的 Masking-Key 为 0x08f6efb1
,根据上面的算法,我们可以这样进行掩码运算:
let uint8 = new Uint8Array([0xE6, 0x88, 0x91, 0xE6, 0x98, 0xAF, 0xE9, 0x98,
0xBF, 0xE5, 0xAE, 0x9D, 0xE5, 0x93, 0xA5]);
let maskingKey = new Uint8Array([0x08, 0xf6, 0xef, 0xb1]);
let maskedUint8 = new Uint8Array(uint8.length);
for (let i = 0, j = 0; i < uint8.length; i++, j = i % 4) {
maskedUint8[i] = uint8[i] ^ maskingKey[j];
}
console.log(Array.from(maskedUint8).map(num=>Number(num).toString(16)).join(' '));
以上代码成功运行后,控制台会输出以下结果:
ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a
上述结果与 WireShark 中的 Masked payload 对应的值是一致的,具体如下图所示:
在 WebSocket 协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。那么为什么还要引入数据掩码呢?引入数据掩码是为了防止早期版本的协议中存在的代理缓存污染攻击等问题。
了解完 WebSocket 掩码算法和数据掩码的作用之后,我们再来介绍一下数据分片的概念。
3.3.3 数据分片
WebSocket 的每条消息可能被切分成多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据 FIN 的值来判断,是否已经收到消息的最后一个数据帧。
利用 FIN 和 Opcode,我们就可以跨帧发送消息。操作码告诉了帧应该做什么。如果是 0x1,有效载荷就是文本。如果是 0x2,有效载荷就是二进制数据。但是,如果是 0x0,则该帧是一个延续帧。这意味着服务器应该将帧的有效负载连接到从该客户机接收到的最后一个帧。
为了让大家能够更好地理解上述的内容,我们来看一个来自 MDN 上的示例:
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
在以上示例中,客户端向服务器发送了两条消息。第一个消息在单个帧中发送,而第二个消息跨三个帧发送。
其中第一个消息是一个完整的消息(FIN=1 且 opcode != 0x0),因此服务器可以根据需要进行处理或响应。而第二个消息是文本消息(opcode=0x1)且 FIN=0,表示消息还没发送完成,还有后续的数据帧。该消息的所有剩余部分都用延续帧(opcode=0x0)发送,消息的最终帧用 FIN=1 标记。
好的,简单介绍了数据分片的相关内容。接下来,我们来开始实现消息通信功能。
3.4 实现消息通信功能
阿宝哥把实现消息通信功能,分解为消息解析与消息响应两个子功能,下面我们分别来介绍如何实现这两个子功能。
3.4.1 消息解析
利用消息通信基础环节中介绍的相关知识,阿宝哥实现了一个 parseMessage 函数,用来解析客户端传过来的 WebSocket 数据帧。出于简单考虑,这里只处理文本帧,具体代码如下所示:
function parseMessage(buffer) {
// 第一个字节,包含了FIN位,opcode, 掩码位
const firstByte = buffer.readUInt8(0);
// [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE];
// 右移7位取首位,1位,表示是否是最后一帧数据
const isFinalFrame = Boolean((firstByte >>> 7) & 0x01);
console.log("isFIN: ", isFinalFrame);
// 取出操作码,低四位
/**
* %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;
* %x1:表示这是一个文本帧(text frame);
* %x2:表示这是一个二进制帧(binary frame);
* %x3-7:保留的操作代码,用于后续定义的非控制帧;
* %x8:表示连接断开;
* %x9:表示这是一个心跳请求(ping);
* %xA:表示这是一个心跳响应(pong);
* %xB-F:保留的操作代码,用于后续定义的控制帧。
*/
const opcode = firstByte & 0x0f;
if (opcode === 0x08) {
// 连接关闭
return;
}
if (opcode === 0x02) {
// 二进制帧
return;
}
if (opcode === 0x01) {
// 目前只处理文本帧
#### 总结
=============================================================
从转行到现在,差不多两年的时间,虽不能和大佬相比,但也是学了很多东西。我个人在学习的过程中,习惯简单做做笔记,方便自己复习的时候能够快速理解,现在将自己的笔记分享出来,和大家共同学习。
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)**
个人将这段时间所学的知识,分为三个阶段:
第一阶段:HTML&CSS&JavaScript基础
![](https://img-blog.csdnimg.cn/img_convert/3e0d5b0f6a97b823cc1ef22ff1a18191.png)
第二阶段:移动端开发技术
![](https://img-blog.csdnimg.cn/img_convert/fc21db0a800494796dc6408ce1486031.png)
第三阶段:前端常用框架
![](https://img-blog.csdnimg.cn/img_convert/644efd4ddd0f8d43535f1982ec0da6e4.png)
* 推荐学习方式:针对某个知识点,可以先简单过一下我的笔记,如果理解,那是最好,可以帮助快速解决问题;如果因为我的笔记太过简陋不理解,可以关注我以后我还会继续分享。
* 大厂的面试难在,针对一个基础知识点,比如JS的事件循环机制,不会上来就问概念,而是换个角度,从题目入手,看你是否真正掌握。所以对于概念的理解真的很重要。