分类: 技术

  • Apexcharts与echarts的对比

    ApexCharts和ECharts都是目前非常流行的前端图表库,都能帮助你快速创建漂亮的数据可视化。但它们的技术侧重和适用场景有明显区别——ApexCharts主打现代化的交互设计和简洁风格,而ECharts则更偏向于提供海量图表类型和强大的企业级性能。下面我从几个核心维度为你详细拆解:

    📊 核心对比总览

    对比维度ApexChartsECharts
    设计风格现代化、简洁、美观,自带多种仪表板模板功能全面、风格经典,适合严肃的企业级报表
    图表丰富度16 种,包括线形、面积、柱状、饼图、雷达图等数十种内置图表,包含地图、漏斗、桑基图、热力图、关系图等
    渲染引擎SVG,保证视觉清晰度Canvas为主,部分场景支持SVG
    性能与数据量适合轻量级仪表盘,最佳实践建议数据点不超过数千个性能优越,可流畅支持百万级数据渲染
    定制能力支持丰富的开箱即用配置和主题,满足大多数仪表盘需求配置项极其丰富,支持高度个性化定制
    开发体验上手简单,API清晰,文档完善上手容易,中文文档极其完善,社区活跃
    框架支持官方支持 React、Vue、Angular,集成顺畅官方支持 React、Vue、Angular,且有微信小程序支持
    开源协议MIT,完全免费开源Apache 2.0,完全免费开源,由Apache基金会维护
    特殊优势交互细节丰富(如缩放、平移、注解),实时数据更新体验好国产工具,本土化支持强;内置地图和数据集支持

    🧭 深度解析:如何选择?

    了解基本参数后,我们来看看它们在实际项目中的表现差异。

    🎯 ApexCharts:为现代交互式仪表盘而生

    ApexCharts 的核心优势在于其 “现代化”和”美观”。如果你正在开发一个面向运营人员、管理者,需要快速呈现关键指标(KPI)的轻量级仪表盘,它会是一个很棒的选择。

    • 交互体验:它内置了非常流畅的缩放、平移和注解功能,用户与图表的互动会感觉很自然 。
    • 上手速度:它的 API 设计清晰,配合 React、Vue 等框架的官方封装,可以让你在很短时间内就构建出一个功能完备、外观时尚的图表组件 。
    • 需要注意:它基于 SVG 渲染,在处理超大数据集(例如实时更新的金融时序数据或超过几千个点的复杂图表)时,性能可能会成为瓶颈 。

    🏢 ECharts:企业级复杂应用的”瑞士军刀”

    ECharts(尤其是进入Apache基金会后)的定位是成为一款企业级、高性能、全功能的图表库。它就像一把”瑞士军刀”,几乎能满足你对图表的所有想象。

    • 图表广度:当你需要绘制特殊图表,比如地图、桑基图、关系图谱、仪表盘时,ECharts 通常内置支持,而 ApexCharts 可能需要你自行实现或寻找其他方案 。
    • 性能深度:得益于 Canvas 渲染和内置的优化机制,ECharts 在面对百万级数据的业务报表或实时监控大屏时,依然能保持流畅的渲染和交互 。
    • 本土化与生态:ECharts 拥有庞大的中文社区和极其详尽的文档,无论是遇到问题还是寻找示例,都会非常方便。它也与许多国内的大数据平台、报表工具(如FineReport)有良好的集成 。

    💡 选型建议

    • 如果你的项目是…
      • 一个创业公司的数据看板,数据量不大但追求颜值和快速上线:可以优先考虑 ApexCharts。它的设计现代感强,能给你的产品增色不少。
      • 一个大型企业的后台管理系统,需要处理海量数据,图表类型复杂多样ECharts 是更稳妥的选择。它的稳定性和全面的功能集能应对各种挑战。
    • 如果你更看重…
      • 流畅的交互和动画细节:两者都做得很出色,但 ApexCharts 的交互可能感觉更”现代化”一些 。
      • 功能的全面性和性能的极限ECharts 无疑更胜一筹 。
    • 团队的技术背景?
      • 如果团队前端能力较强,追求极致的定制化,甚至可以考虑 D3.js,但它和这两者的复杂度不在一个量级 。
  • websocket详解

    本文由作者“阿宝哥”分享,原题“你不知道的 WebSocket”,有修订和改动。

    1、引言

    本文将从基本概念、技术原理、常见易错常识、动手实践等多个方面入手,万字长文,带你一起全方位探索 WebSocket 技术。

    阅读完本文,你将了解以下内容:

    • 1)了解 WebSocket 的诞生背景、WebSocket 是什么及它的优点;
    • 2)了解 WebSocket 含有哪些 API 及如何使用 WebSocket API 发送普通文本和二进制数据;
    • 3)了解 WebSocket 的握手协议和数据帧格式、掩码算法等相关知识;
    • 4)了解 WebSocket 与http、长轮询、socket等的关系,理清常识性的理解错误;
    • 5)了解如何实现一个支持发送普通文本的 WebSocket 服务器

    2、关于作者

    作者网名:阿宝哥

    个人博客:全栈修仙之路

    作者Githubsemlinker (阿宝哥) · GitHub

    3、什么是 WebSocket

    3.1 WebSocket 诞生背景

    早期,很多网站为了实现推送技术,所用的技术都是轮询(也叫短轮询)。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回最新的数据给客户端。

    常见的轮询方式分为轮询与长轮询,它们的区别如下图所示:

    为了更加直观感受轮询与长轮询之间的区别,我们来看一下具体的代码:

    这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。

    PS:关于短轮询、长轮询技术的前世今身,可以详细读这两篇:《新手入门贴:史上最全Web端即时通讯技术原理详解》、《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE》。

    比较新的轮询技术是 Comet。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在 Comet 中普遍采用的 HTTP 长连接也会消耗服务器资源。

    在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

    Websocket 使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket。

    如:

    ws://echo.websocket.org wss://echo.websocket.org

    WebSocket 与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制。

    默认情况下:

    • 1)WebSocket 协议使用 80 端口;
    • 2)若运行在 TLS 之上时,默认使用 443 端口。

    3.2 WebSocket 简介

    WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。

    WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

    介绍完轮询和 WebSocket 的相关内容之后,接下来用一张图看一下 XHR Polling(短轮询) 与 WebSocket 之间的区别。

    XHR Polling与 WebSocket 之间的区别如下图所示:

    3.3 WebSocket 优点

    普遍认为,WebSocket的优点有如下几点:

    • 1)较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
    • 2)更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
    • 3)保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
    • 4)更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
    • 5)可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

    由于 WebSocket 拥有上述的优点,所以它被广泛地应用在即时通讯/IM、实时音视频、在线教育和游戏等领域。

    对于前端开发者来说,要想使用 WebSocket 提供的强大能力,就必须先掌握 WebSocket API,下面带大家一起来认识一下 WebSocket API。

    PS:如果你想要更浅显的WebSocket入门教程,可以先读这篇《新手快速入门:WebSocket简明教程》后,再回来继续学习。

    4、WebSocket API 学习

    4.1 基本情况

    在介绍 WebSocket API 之前,我们先来了解一下它的兼容性:

    (图片引用自:https://caniuse.com/#search=WebSocket)

    由上图可知:目前主流的 Web 浏览器都支持 WebSocket,所以我们可以在大多数项目中放心地使用它。

    在浏览器中要使用 WebSocket 提供的能力,我们就必须先创建 WebSocket 对象,该对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。

    使用 WebSocket 构造函数,我们就能轻易地构造一个 WebSocket 对象。

    接下来我们将从以下四个方面来介绍 WebSocket API:

    • 1)WebSocket 构造函数;
    • 2)WebSocket 对象的属性;
    • 3)WebSocket 的方法;
    • 4)WebSocket 事件。

    接下来我们从 WebSocket 的构造函数入手开始学习。

    PS:如果你想要更浅显的WebSocket入门教程,可以先读这篇《新手快速入门:WebSocket简明教程》后,再回来继续学习。

    4.2 构造函数

    WebSocket 构造函数的语法为:

    const myWebSocket = newWebSocket(url [, protocols]);

    相关参数说明如下:

    • 1)url:表示连接的 URL,这是 WebSocket 服务器将响应的 URL;
    • 2)protocols(可选):一个协议字符串或者一个包含协议字符串的数组。

    针对第2)点:这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议。

    比如:你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互。如果不指定协议字符串,则假定为空字符串。

    使用WebSocket 构造函数时,当尝试连接的端口被阻止时,会抛出 SECURITY_ERR 异常。

    PS:有关WebSocket构造函数的更详细说明,可以参见官方API文档。

    4.3 属性

    WebSocket 对象包含以下属性:

    每个属性的具体含义如下:

    • 1)binaryType:使用二进制的数据类型连接;
    • 2)bufferedAmount(只读):未发送至服务器的字节数;
    • 3)extensions(只读):服务器选择的扩展;
    • 4)onclose:用于指定连接关闭后的回调函数;
    • 5)onerror:用于指定连接失败后的回调函数;
    • 6)onmessage:用于指定当从服务器接受到信息时的回调函数;
    • 7)onopen:用于指定连接成功后的回调函数;
    • 8)protocol(只读):用于返回服务器端选中的子协议的名字;
    • 9)readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态:
    •     – CONNECTING — 正在连接中,对应的值为 0;
    •     – OPEN — 已经连接并且可以通讯,对应的值为 1;
    •     – CLOSING — 连接正在关闭,对应的值为 2;
    •     – CLOSED — 连接已关闭或者没有连接成功,对应的值为 3
    • 10)url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径。

    4.4 方法

    WebSocket 主要方法有两个:

    • 1)close([code[, reason]]):该方法用于关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作;
    • 2)send(data):该方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值 。若数据无法传输(比如数据需要缓存而缓冲区已满)时,套接字会自行关闭。

    4.5 事件

    使用 addEventListener() 或将一个事件监听器赋值给 WebSocket 对象的 oneventname 属性,来监听下面的事件。

    以下是几个事件:

    • 1)close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置;
    • 2)error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置;
    • 3)message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置;
    • 4)open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置。

    介绍完 WebSocket API,我们来举一个使用 WebSocket 发送普通文本的示例。

    4.6 代码实践:发送普通文本

    在以上示例中:我们在页面上创建了两个 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>     <metacharset=”UTF-8″/>     <metaname=”viewport”content=”width=device-width, initial-scale=1.0″/>     <title>WebSocket 发送普通文本示例</title>     <style>       .block {         flex: 1;       }     </style>   </head>   <body>     <h3>WebSocket 发送普通文本示例</h3>     <divstyle=”display: flex;”>       <divclass=”block”>         <p>即将发送的数据:<button>发送</button></p>         <textareaid=”sendMessage”rows=”5″cols=”15″></textarea>       </div>       <divclass=”block”>         <p>接收的数据:</p>         <textareaid=”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 = newArrayBuffer(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》这篇文章。

    4.7 代码实践:发送二进制数据

    在以上示例中,我们在页面上创建了两个 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 = newBlob([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 instanceofBlob) {     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>发送</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 instanceofBlob) {           receivedMsgContainer.value = await receivedData.text();         } else{           receivedMsgContainer.value = receivedData;         }       });       functionsend() {         const message = sendMsgContainer.value;         if(socket.readyState !== WebSocket.OPEN) {           console.log(“连接未建立,还不能发送消息”);           return;         }         const blob = newBlob([message], { type: “text/plain”});         if(message) socket.send(blob);         console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`);       }     </script>   </body> </html>

    可能有一些小伙伴了解完 WebSocket API 之后,觉得还不够过瘾。下面将带大家来实现一个支持发送普通文本的 WebSocket 服务器。

    5、手写 WebSocket 服务器

    5.1 写在前面

    在介绍如何手写 WebSocket 服务器前,我们需要了解一下 WebSocket 连接的生命周期。

    从上图可知:在使用 WebSocket 实现全双工通信之前,客户端与服务器之间需要先进行握手(Handshake),在完成握手之后才能开始进行数据的双向通信。

    握手是在通信电路创建之后,信息传输开始之前。

    握手用于达成参数,如:

    • 1)信息传输率
    • 2)字母表
    • 3)奇偶校验
    • 4)中断过程;
    • 5)其他协议特性。

    握手有助于不同结构的系统或设备在通信信道中连接,而不需要人为设置参数。

    既然握手是 WebSocket 连接生命周期的第一个环节,接下来我们就先来分析 WebSocket 的握手协议。

    5.2 握手协议

    WebSocket 协议属于应用层协议,它依赖于传输层的 TCP 协议。WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手。为了创建 WebSocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为 “握手”(Handshaking)。

    利用 HTTP 完成握手有几个好处:

    • 1)首先:让 WebSocket 与现有 HTTP 基础设施兼容——使得 WebSocket 服务器可以运行在 80 和 443 端口上,这通常是对客户端唯一开放的端口;
    • 2)其次:让我们可以重用并扩展 HTTP 的 Upgrade 流,为其添加自定义的 WebSocket 首部,以完成协商。

    下面我们以前面已经演示过的发送普通文本的例子为例,来具体分析一下握手过程。

    5.2.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 请求头。

    针对上述请求中的字段说明如下:

    • 1)Connection:必须设置 Upgrade,表示客户端希望连接升级;
    • 2)Upgrade:字段必须设置 websocket,表示希望升级到 WebSocket 协议;
    • 3)Sec-WebSocket-Version:表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用;
    • 4)Sec-WebSocket-Key:是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要;
    • 5)Sec-WebSocket-Extensions:用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一个或多个扩展;
    • 6)Origin:字段是可选的,通常用来表示在浏览器中发起此 WebSocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。

    针对上述第4)点:把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 WebSocket 协议。

    5.2.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 服务器。

    5.3 实现握手功能

    要开发一个 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 握手协议相关的内容基本已经介绍完了。下一步我们来介绍开发消息通信功能需要了解的一些基础知识。

    5.4 消息通信基础

    在 WebSocket 协议中,数据是通过一系列数据帧来进行传输的。

    为了避免由于网络中介(例如一些拦截代理)或者一些安全问题,客户端必须在它发送到服务器的所有帧中添加掩码。服务端收到没有添加掩码的数据帧以后,必须立即关闭连接。

    5.4.1)数据帧格式:

    要实现消息通信,我们就必须了解 WebSocket 数据帧的格式:

    可能有一些小伙伴看到上面的内容之后,就开始有点 “懵逼” 了。

    下面我们来结合实际的数据帧来进一步分析一下:

    在上图中:简单分析了 “发送普通文本” 示例对应的数据帧格式。这里我们来进一步介绍一下 Payload length,因为在后面开发数据解析功能的时候,需要用到该知识点。

    Payload length 表示以字节为单位的 “有效负载数据” 长度。

    它有以下几种情形:

    • 1)如果值为 0-125,那么就表示负载数据的长度;
    • 2)如果是 126,那么接下来的 2 个字节解释为 16 位的无符号整形作为负载数据的长度;
    • 3)如果是 127,那么接下来的 8 个字节解释为一个 64 位的无符号整形(最高位的 bit 必须为 0)作为负载数据的长度。

    备注:多字节长度量以网络字节顺序表示,有效负载长度是指 “扩展数据” + “应用数据” 的长度。“扩展数据” 的长度可能为 0,那么有效负载长度就是 “应用数据” 的长度。

    另外:除非协商过扩展,否则 “扩展数据” 长度为 0 字节。在握手协议中,任何扩展都必须指定 “扩展数据” 的长度,这个长度如何进行计算,以及这个扩展如何使用。如果存在扩展,那么这个 “扩展数据” 包含在总的有效负载长度中。

    PS:关于数据帧格式的详细讲解,可以深入读读以下几篇:

    • 《WebSocket从入门到精通,半小时就够!》
    • 《理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性》

    5.4.2)掩码算法:

    掩码字段是一个由客户端随机选择的 32 位的值。掩码值必须是不可被预测的。因此,掩码必须来自强大的熵源(entropy),并且给定的掩码不能让服务器或者代理能够很容易的预测到后续帧。掩码的不可预测性对于预防恶意应用的作者在网上暴露相关的字节数据至关重要。

    掩码不影响数据荷载的长度,对数据进行掩码操作和对数据进行反掩码操作所涉及的步骤是相同的。

    掩码、反掩码操作都采用如下算法:

    j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j

    解释一下:

    • 1)original-octet-i:为原始数据的第 i 字节;
    • 2)transformed-octet-i:为转换后的数据的第 i 字节;
    • 3)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 掩码算法和数据掩码的作用之后,我们再来介绍一下数据分片的概念。

    5.4.3)数据分片:

    WebSocket 的每条消息可能被切分成多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据 FIN 的值来判断,是否已经收到消息的最后一个数据帧。

    利用 FIN 和 Opcode,我们就可以跨帧发送消息。

    操作码告诉了帧应该做什么:

    • 1)如果是 0x1,有效载荷就是文本;
    • 2)如果是 0x2,有效载荷就是二进制数据;
    • 3)如果是 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, newmessage 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 newyear to you too!

    在以上示例中:客户端向服务器发送了两条消息,第一个消息在单个帧中发送,而第二个消息跨三个帧发送。

    其中:第一个消息是一个完整的消息(FIN=1 且 opcode != 0x0),因此服务器可以根据需要进行处理或响应。而第二个消息是文本消息(opcode=0x1)且 FIN=0,表示消息还没发送完成,还有后续的数据帧。该消息的所有剩余部分都用延续帧(opcode=0x0)发送,消息的最终帧用 FIN=1 标记。

    好的,简单介绍了数据分片的相关内容。接下来,我们来开始实现消息通信功能。

    5.5 实现消息通信功能

    笔者把实现消息通信功能,分解为消息解析与消息响应两个子功能,下面我们分别来介绍如何实现这两个子功能。

    5.5.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) {     // 目前只处理文本帧     let offset = 1;     const secondByte = buffer.readUInt8(offset);     // MASK: 1位,表示是否使用了掩码,在发送给服务端的数据帧里必须使用掩码,而服务端返回时不需要掩码     const useMask = Boolean((secondByte >>> 7) & 0x01);     console.log(“use MASK: “, useMask);     const payloadLen = secondByte & 0x7f; // 低7位表示载荷字节长度     offset += 1;     // 四个字节的掩码     let MASK = [];     // 如果这个值在0-125之间,则后面的4个字节(32位)就应该被直接识别成掩码;     if(payloadLen <= 0x7d) {       // 载荷长度小于125       MASK = buffer.slice(offset, 4 + offset);       offset += 4;       console.log(“payload length: “, payloadLen);     } elseif(payloadLen === 0x7e) {       // 如果这个值是126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小;       console.log(“payload length: “, buffer.readInt16BE(offset));       // 长度是126, 则后面两个字节作为payload length,32位的掩码       MASK = buffer.slice(offset + 2, offset + 2 + 4);       offset += 6;     } else{       // 如果这个值是127,则后面的8个字节(64位)内容应该被识别成一个64位的二进制数表示数据内容大小       MASK = buffer.slice(offset + 8, offset + 8 + 4);       offset += 12;     }     // 开始读取后面的payload,与掩码计算,得到原来的字节内容     const newBuffer = [];     const dataBuffer = buffer.slice(offset);     for(let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) {       const nextBuf = dataBuffer[i ];       newBuffer.push(nextBuf ^ MASK[j]);     }     return Buffer.from(newBuffer).toString();   }   return “”; }

    创建完 parseMessage 函数,我们来更新一下之前创建的 WebSocket 服务器:

    server.on(“upgrade”, function(req, socket) {   socket.on(“data”, (buffer) => {     const message = parseMessage(buffer);     if(message) {       console.log(“Message from client:”+ message);     } elseif(message === null) {       console.log(“WebSocket connection closed by the client.”);     }   });   if(req.headers[“upgrade”] !== “websocket”) {     socket.end(“HTTP/1.1 400 Bad Request”);     return;   }   // 省略已有代码 });

    更新完成之后,我们重新启动服务器,然后继续使用 “发送普通文本” 的示例来测试消息解析功能。

    以下发送 “我是阿宝哥” 文本消息后,WebSocket 服务器输出的信息:

    Server running at http://localhost:8888 isFIN:  true use MASK:  true payload length:  15 Message from client:我是阿宝哥

    通过观察以上的输出信息,我们的 WebSocket 服务器已经可以成功解析客户端发送包含普通文本的数据帧,下一步我们来实现消息响应的功能。

    5.5.2)消息响应:

    要把数据返回给客户端,我们的 WebSocket 服务器也得按照 WebSocket 数据帧的格式来封装数据。

    与前面介绍的 parseMessage 函数一样,我也封装了一个 constructReply 函数用来封装返回的数据。

    该函数的具体代码如下:

    function constructReply(data) {   const json = JSON.stringify(data);   const jsonByteLength = Buffer.byteLength(json);   // 目前只支持小于65535字节的负载   const lengthByteCount = jsonByteLength < 126 ? 0 : 2;   const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;   const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);   // 设置数据帧首字节,设置opcode为1,表示文本帧   buffer.writeUInt8(0b10000001, 0);   buffer.writeUInt8(payloadLength, 1);   // 如果payloadLength为126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小   let payloadOffset = 2;   if(lengthByteCount > 0) {     buffer.writeUInt16BE(jsonByteLength, 2);     payloadOffset += lengthByteCount;   }   // 把JSON数据写入到Buffer缓冲区中   buffer.write(json, payloadOffset);   return buffer; }

    创建完 constructReply 函数,我们再来更新一下之前创建的 WebSocket 服务器:

    server.on(“upgrade”, function(req, socket) {   socket.on(“data”, (buffer) => {     const message = parseMessage(buffer);     if(message) {       console.log(“Message from client:”+ message);       // 新增以下&#128071;代码       socket.write(constructReply({ message }));     } elseif(message === null) {       console.log(“WebSocket connection closed by the client.”);     }   }); });

    到这里,我们的 WebSocket 服务器已经开发完成了,接下来我们来完整验证一下它的功能。

    从上图中可知:以上开发的简易版 WebSocket 服务器已经可以正常处理普通文本消息了。

    最后我们来看一下完整的代码。

    custom-websocket-server.js文件:

    const http = require(“http”); const port = 8888; const { generateAcceptValue, parseMessage, constructReply } = 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) {   socket.on(“data”, (buffer) => {     const message = parseMessage(buffer);     if(message) {       console.log(“Message from client:”+ message);       socket.write(constructReply({ message }));     } else if(message === null) {       console.log(“WebSocket connection closed by the client.”);     }   });   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}`) );

    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”); } 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) {     // 目前只处理文本帧     let offset = 1;     const secondByte = buffer.readUInt8(offset);     // MASK: 1位,表示是否使用了掩码,在发送给服务端的数据帧里必须使用掩码,而服务端返回时不需要掩码     const useMask = Boolean((secondByte >>> 7) & 0x01);     console.log(“use MASK: “, useMask);     const payloadLen = secondByte & 0x7f; // 低7位表示载荷字节长度     offset += 1;     // 四个字节的掩码     let MASK = [];     // 如果这个值在0-125之间,则后面的4个字节(32位)就应该被直接识别成掩码;     if(payloadLen <= 0x7d) {       // 载荷长度小于125       MASK = buffer.slice(offset, 4 + offset);       offset += 4;       console.log(“payload length: “, payloadLen);     } else if(payloadLen === 0x7e) {       // 如果这个值是126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小;       console.log(“payload length: “, buffer.readInt16BE(offset));       // 长度是126, 则后面两个字节作为payload length,32位的掩码       MASK = buffer.slice(offset + 2, offset + 2 + 4);       offset += 6;     } else{       // 如果这个值是127,则后面的8个字节(64位)内容应该被识别成一个64位的二进制数表示数据内容大小       MASK = buffer.slice(offset + 8, offset + 8 + 4);       offset += 12;     }     // 开始读取后面的payload,与掩码计算,得到原来的字节内容     const newBuffer = [];     const dataBuffer = buffer.slice(offset);     for(let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) {       const nextBuf = dataBuffer[i ];       newBuffer.push(nextBuf ^ MASK[j]);     }     return Buffer.from(newBuffer).toString();   }   return “”; } function constructReply(data) {   const json = JSON.stringify(data);   const jsonByteLength = Buffer.byteLength(json);   // 目前只支持小于65535字节的负载   const lengthByteCount = jsonByteLength < 126 ? 0 : 2;   const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;   const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);   // 设置数据帧首字节,设置opcode为1,表示文本帧   buffer.writeUInt8(0b10000001, 0);   buffer.writeUInt8(payloadLength, 1);   // 如果payloadLength为126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小   let payloadOffset = 2;   if(lengthByteCount > 0) {     buffer.writeUInt16BE(jsonByteLength, 2);     payloadOffset += lengthByteCount;   }   // 把JSON数据写入到Buffer缓冲区中   buffer.write(json, payloadOffset);   return buffer; } module.exports = {   generateAcceptValue,   parseMessage,   constructReply, };

    其实服务器向浏览器推送信息,除了使用 WebSocket 技术之外,还可以使用 SSE(Server-Sent Events)。它让服务器可以向客户端流式发送文本消息,比如服务器上生成的实时消息。

    为实现这个目标,SSE 设计了两个组件:浏览器中的 EventSource API 和新的 “事件流” 数据格式(text/event-stream)。其中,EventSource 可以让客户端以 DOM 事件的形式接收到服务器推送的通知,而新数据格式则用于交付每一次数据更新。

    实际上:SSE 提供的是一个高效、跨浏览器的 XHR 流实现,消息交付只使用一个长 HTTP 连接。然而,与我们自己实现 XHR 流不同,浏览器会帮我们管理连接、 解析消息,从而让我们只关注业务逻辑。篇幅有限,关于 SSE 的更多细节,就不展开介绍了,对 SSE 感兴趣的小伙伴可以自行阅读以下几篇:

    1. 《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE》
    2. 《SSE技术详解:一种全新的HTML5服务器推送事件技术》
    3. 《使用WebSocket和SSE技术实现Web端消息推送》
    4. 《详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket》
    5. 《网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket》
    6. 《搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE》

    6、WebSocket学习过程中的易错常识

    6.1 WebSocket 与 HTTP 有什么关系?

    WebSocket 是一种与 HTTP 不同的协议。两者都位于 OSI 模型的应用层,并且都依赖于传输层的 TCP 协议。

    虽然它们不同,但是 RFC 6455 中规定:WebSocket 被设计为在 HTTP 80 和 443 端口上工作,并支持 HTTP 代理和中介,从而使其与 HTTP 协议兼容。 为了实现兼容性,WebSocket 握手使用 HTTP Upgrade 头,从 HTTP 协议更改为 WebSocket 协议。

    既然已经提到了 OSI(Open System Interconnection Model)模型,这里分享一张很生动、很形象描述 OSI 模型的示意图(如下图所示)。

    (图片引用自:https://www.networkingsphere.com/2019/07/what-is-osi-model.html)

    当然,WebSocket与HTTP的关系显然不是这三两句话可以说的清,有兴趣的读者可以详读下面这两篇:

    1. 《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》
    2. 《WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)》

    6.2 WebSocket 与长轮询有什么区别?

    长轮询就是:客户端发起一个请求,服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断请求的数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则等待一定的时间后才返回。

    长轮询的本质还是基于 HTTP 协议,它仍然是一个一问一答(请求 — 响应)的模式。而 WebSocket 在握手成功后,就是全双工的 TCP 通道,数据可以主动从服务端发送到客户端。

    要理解WebSocket 与长轮询的区别,需要深刻理解长轮询的技术原理,以下3篇中有关长轮询的技术介绍建议深入阅读:

    1. 《Comet技术详解:基于HTTP长连接的Web端实时通信技术》
    2. 《新手入门贴:史上最全Web端即时通讯技术原理详解》
    3. 《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE》
    4. 《网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket》

    6.3 什么是 WebSocket 心跳?

    网络中的接收和发送数据都是使用 Socket 进行实现。但是如果此套接字已经断开,那发送数据和接收数据的时候就一定会有问题。

    可是如何判断这个套接字是否还可以使用呢?这个就需要在系统中创建心跳机制。

    所谓 “心跳” 就是定时发送一个自定义的结构体(心跳包或心跳帧),让对方知道自己 “在线”,以确保链接的有效性。

    而所谓的心跳包就是客户端定时发送简单的信息给服务器端告诉它我还在而已。代码就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息,如果服务端几分钟内没有收到客户端信息则视客户端断开。

    在 WebSocket 协议中定义了 心跳 Ping 和 心跳 Pong 的控制帧:

    • 1)心跳 Ping 帧包含的操作码是 0x9:如果收到了一个心跳 Ping 帧,那么终端必须发送一个心跳 Pong 帧作为回应,除非已经收到了一个关闭帧。否则终端应该尽快回复 Pong 帧;
    • 2)心跳 Pong 帧包含的操作码是 0xA:作为回应发送的 Pong 帧必须完整携带 Ping 帧中传递过来的 “应用数据” 字段。

    针对第2)点:如果终端收到一个 Ping 帧但是没有发送 Pong 帧来回应之前的 Ping 帧,那么终端可以选择仅为最近处理的 Ping 帧发送 Pong 帧。此外,可以自动发送一个 Pong 帧,这用作单向心跳。

    PS:这里有篇WebSocket心跳方面的IM实战总结文章,有兴趣可以阅读《Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?》。

    6.4 Socket 是什么?

    网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 Socket(套接字),因此建立网络通信连接至少要一对端口号。

    Socket 本质:是对 TCP/IP 协议栈的封装,它提供了一个针对 TCP 或者 UDP 编程的接口,并不是另一种协议。通过 Socket,你可以使用 TCP/IP 协议。

    百度百科上关于Socket的描述是这样:

    Socket 的英文原义是“孔”或“插座”:作为 BSD UNIX 的进程通信机制,取后一种意思。通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。 在Internet 上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket 正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电, 有的提供 110 伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

    关于 Socket,可以总结以下几点:

    • 1)它可以实现底层通信,几乎所有的应用层都是通过 socket 进行通信的;
    • 2)对 TCP/IP 协议进行封装,便于应用层协议调用,属于二者之间的中间抽象层;
    • 3)TCP/IP 协议族中,传输层存在两种通用协议: TCP、UDP,两种协议不同,因为不同参数的 socket 实现过程也不一样。

    下图说明了面向连接的协议的套接字 API 的客户端/服务器关系:

    PS:要说WebSocket和Socket的关系,这篇《WebSocket详解(六):刨根问底WebSocket与Socket的关系》有专门进行详细分享,建议阅读。本文已同步发布于:http://www.52im.net/thread-3713-1-1.html

    7、参考资料

    [1] 新手快速入门:WebSocket简明教程

    [2] WebSocket从入门到精通,半小时就够!

    [3] 新手入门贴:史上最全Web端即时通讯技术原理详解

    [4] Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE

    [5] SSE技术详解:一种全新的HTML5服务器推送事件技术

    [6] Comet技术详解:基于HTTP长连接的Web端实时通信技术

    [7] WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)

    [8] WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)

    [9] WebSocket详解(六):刨根问底WebSocket与Socket的关系

    [10] Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?

    [11] 理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性

    [12] WebSocket硬核入门:200行代码,教你徒手撸一个WebSocket服务器

    [13] 网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket

    [14] 搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE

  • Chrome无法访问localhost,但Edge可以访问,怎么解决?

     

    问题描述:做了几个项目,用chrome浏览器本地测试的时候发现,无法访问,其他浏览器可以,刚开始以为是缓存很多删除,删除没大会儿继续无法访问。发布到服务器上面chrome可以访问。之后找到了解决方案,故整理一下。

    1. 清除HSTS设置(最可能的原因)

    HSTS(HTTP严格传输安全)是一种安全机制,如果之前通过HTTPS访问过localhost且证书有问题,浏览器可能会将localhost错误地加入强制HTTPS的列表,导致现在无法通过HTTP访问。

    操作步骤:‌

    在Chrome地址栏输入:chrome://net-internals/#hsts
    点击顶部的“Delete domain security policies”标签页。
    在输入框中输入 localhost,然后点击“Delete”按钮。
    操作成功后,关闭所有Chrome窗口,然后重新打开,再尝试访问 http://localhost:端口号。‌
    2. 检查并修改代理设置

    如果您配置过系统代理或浏览器代理,可能会导致Chrome绕过代理访问localhost时出现问题。

    操作步骤:‌

    在Chrome地址栏输入:chrome://settings/system
    点击“打开您的计算机的代理设置”。
    确保“使用代理服务器”选项是‌关闭‌的。如果已开启,请关闭它。‌
    另外,检查“忽略这些主机和域的代理设置”列表中是否包含 localhost 或 127.0.0.1,确保它们被正确排除。‌‌
    3. 检查Chrome扩展程序

    某些扩展程序(如广告拦截器、安全工具或代理插件)可能会错误地拦截对localhost的请求。

    排查方法:‌

    访问 chrome://extensions。
    临时‌禁用‌所有扩展程序。
    然后尝试访问localhost。如果此时可以访问,说明问题出在某个扩展上。您可以重新启用扩展,逐一排查是哪一个导致的问题。‌‌
    4. 检查本地hosts文件(作为排查)

    虽然Edge能访问而Chrome不能,此问题可能性较低,但仍可检查以排除其他干扰。

    操作步骤:‌

    以管理员身份打开记事本,然后打开文件:C:\Windows\System32\drivers\etc\hosts
    确认文件中包含以下一行且未被注释(前面没有#号):
    text
    Copy Code
    127.0.0.1 localhost

    如果没有,请手动添加;如果被注释了,请取消注释。保存文件后重启Chrome。‌‌
    5. 以管理员身份运行Chrome

    有时权限问题也可能导致网络访问异常。

    操作方法:‌

    右键点击Chrome快捷方式,选择“‌以管理员身份运行‌”。‌‌
    总结与建议

    综合来看,‌方法1(清除HSTS设置)是解决此问题的首选‌,因为“Chrome无法访问而Edge可以”是HSTS问题的典型表现。‌ 如果问题依旧,再依次尝试方法2和方法3。


    这个错误通常出现在 Chrome 浏览器中,与安全策略或缓存/代理设置有关。下面我们分析原因并提供解决方法。
    ## 错误含义
    错误信息:
    `Unsafe attempt to load URL http://localhost:3002/ from frame with URL chrome-error://chromewebdata/. Domains, protocols and ports must match.`
    – `chrome-error://chromewebdata/` 是 Chrome 显示**错误页面**时的内部地址。
    – 意思是:在一个已经出错的页面(错误页面)中,尝试加载 `http://localhost:3002/`,但由于**域名、协议或端口不匹配**,被浏览器安全策略阻止。
    这通常是因为 Chrome 之前尝试访问 `localhost:3002` 失败了(比如连接被拒绝、超时、或者被重定向到错误页),然后在那个错误页面里又试图加载一些资源(如自动跳转、iframe、脚本)到 `localhost:3002`,但浏览器的同源策略禁止了这种跨上下文加载。
    简单来说:**Chrome 一开始就没能正常连接到你的本地服务器,然后它自己陷入了一个错误页面的循环,导致了这个报错。**
    ## 为什么 Edge 可以,Chrome 不行?
    Edge 和 Chrome 虽然都基于 Chromium,但两者的沙箱设置、缓存策略、代理配置、安全策略可能不同。常见原因:
    1. **Chrome 的 DNS 缓存或代理配置**
       Chrome 可能保留了错误的 DNS 记录(例如将 `localhost` 解析到了错误 IP),或使用了系统代理但 Edge 没有。
    2. **Chrome 的安全策略更严格**
       Chrome 可能会阻止某些本地端口的访问(例如部分端口被浏览器列入黑名单),但 3002 通常不是黑名单端口。
    3. **Chrome 的扩展程序干扰**
       某些广告拦截、隐私保护或代理扩展可能拦截了对 `localhost` 的请求。
    4. **Chrome 的缓存/Service Worker**
       如果之前访问 `localhost:3002` 时返回了错误响应(比如 404 或 500),Chrome 可能缓存了错误页面,之后反复加载失败。
    5. **IPv6 vs IPv4 问题**
       Chrome 可能优先使用 IPv6 的 `::1`,而服务器只监听 IPv4 的 `127.0.0.1`,导致连接失败。
    ## 解决方法
    ### 1️⃣ 尝试直接使用 IP 地址
    在 Chrome 地址栏输入:
    `http://127.0.0.1:3002`
    如果成功,说明是 `localhost` 解析问题。可以在 `hosts` 文件中确认 `127.0.0.1 localhost` 存在。
    ### 2️⃣ 清除 Chrome 缓存和 DNS 缓存
    – 清除浏览器缓存:设置 → 隐私和安全 → 清除浏览数据(勾选“缓存的图片和文件”)。
    – 清除 Chrome 内置 DNS 缓存:在地址栏输入 `chrome://net-internals/#dns`,点击“Clear host cache”。
    – 重启 Chrome。
    ### 3️⃣ 使用无痕模式
    按 `Ctrl+Shift+N` 打开无痕窗口再访问,避免扩展和缓存干扰。
    ### 4️⃣ 检查 Chrome 的代理设置
    – 设置 → 系统 → 打开代理设置,确保没有为 `localhost` 配置特殊代理。
    – 如果系统启用了代理,尝试关闭代理或在 Chrome 中设置绕过代理:
      `chrome://settings/` → 高级 → 系统 → 打开代理设置 → 高级 → 例外列表中添加 `localhost;127.0.0.1`。
    ### 5️⃣ 禁用所有扩展
    – 在地址栏输入 `chrome://extensions/`,暂时禁用所有扩展,然后重启 Chrome 测试。
    ### 6️⃣ 检查服务器监听地址
    确认你的服务器(`npm run server` 启动的服务)监听的是 `0.0.0.0` 或 `127.0.0.1`。如果只监听了 `::`(IPv6),可能导致某些环境下访问失败。可以在启动时指定 `–host 0.0.0.0` 或修改配置。
    ### 7️⃣ 重置 Chrome 标志(实验性功能)
    – 地址栏输入 `chrome://flags/`,点击右上角的“Reset all”恢复默认,重启 Chrome。
    ### 8️⃣ 防火墙/杀毒软件
    暂时关闭 Windows 防火墙或第三方杀毒软件,测试是否阻止了 Chrome 对 localhost 的访问。
    ### 9️⃣ 使用其他 Chromium 浏览器测试
    如果 Edge 正常,可以尝试安装 Brave、Vivaldi 等基于 Chromium 的浏览器,看是否复现。如果其他都能访问,则问题仅限 Chrome。
    ## 如果以上都无效
    可能是 Chrome 的安装文件损坏,可以尝试:
    – 重装 Chrome。
    – 创建新的 Chrome 用户配置文件(设置 → 用户 → 删除并重新添加)。
    通常情况下,**清除 DNS 缓存和浏览器缓存** + **使用 127.0.0.1 代替 localhost** 就能解决问题。
  • Web应用防火墙

    Web应用防火墙(Web Application Firewall,简称WAF)对网站或者App的业务流量进行恶意特征识别及防护,在对流量进行清洗和过滤后,将正常、安全的流量返回给服务器,避免网站服务器被恶意入侵导致性能异常等问题,从而保障网站的业务安全和数据安全。

    功能介绍

       
    功能类别 功能说明
    业务配置 支持对网站的HTTP、HTTPS流量进行安全防护。
    Web应用安全防护 常见Web应用攻击防护 防御OWASP常见威胁:SQL注入、XSS跨站、WebShell上传、后门攻击、命令注入、非法HTTP协议请求、常见Web服务器漏洞攻击、CSRF、核心文件非授权访问、路径穿越、网站被扫描等。网站隐身:不对攻击者暴露站点地址,避免其绕过Web应用防火墙直接攻击。0day补丁及时更新:及时更新漏洞补丁,防护网站安全。友好的观察模式:针对网站新上线的业务开启观察模式,对于匹配中防护规则的疑似攻击只告警不阻断,方便统计业务误报状况。
    深度精确防护 支持全解析多种常见HTTP协议数据格式:任意头部字段、Form表单、Multipart、JSON、XML。支持解码常见编码类型:URL编码、JavaScript Unicode编码、HEX编码、HTML实体编码、Java序列化编码、PHP序列化编码、Base64编码、UTF-7编码、UTF-8编码、混合嵌套编码。支持预处理机制:空格压缩、注释删减、特殊字符处理,向上层多种检测引擎提供更为精细、准确的数据源。支持复杂格式数据环境下的检测能力;支持合理的检测逻辑复杂度,避免过多检测数据导致的误报,降低误报率;支持多种形式数据编码的自适应解码,避免利用各种编码形式的绕过。
    CC恶意攻击防护 控制单一源IP的访问频率,基于重定向跳转验证、人机识别等。针对海量慢速请求攻击,根据统计响应码及URL请求分布、异常Referer及User-Agent特征识别,结合网站精准防护规则综合防护。充分利用阿里云大数据安全优势,建立威胁情报与可信访问分析模型,快速识别恶意流量。
    精准访问控制 提供友好的配置控制台界面,支持IP、URL、Referer、User-Agent等HTTP常见字段的条件组合,配置强大的精准访问控制策略;支持盗链防护、网站后台保护等防护场景。与Web常见攻击防护、CC防护等安全模块结合,搭建多层综合保护机制;依据需求,轻松识别可信与恶意流量。
    虚拟补丁 在Web应用漏洞补丁发布和修复之前,通过调整Web防护策略实现快速防护。
    攻击事件管理 支持对攻击事件、攻击流量、攻击规模的集中管理统计。
    灵活性、可靠性 支持负载均衡:以集群方式提供服务,多台服务器负载均衡,支持多种负载均衡策略。支持平滑扩容:可根据实际流量情况,缩减或增加集群服务器的数量,实现服务能力弹性扩容。无单点问题:单台服务器宕机或者维修,均不影响正常服务。

    更多产品信息,请参见Web应用防火墙产品页面

    产品优势

       
    产品优势 优势说明
    10年以上网络安全经验 建立在阿里巴巴集团10年以上的网络安全经验上,提供与淘宝、天猫、支付宝等成功应用案例同样的安全体验。由专业的安全团队为您提供服务。抵御已知的OWASP漏洞并不断修复披露漏洞。
    防御CC攻击和爬虫攻击 帮助您抵御和减缓CC攻击。帮助您防御网络爬虫,避免网络资源消耗。检测和阻挡恶意请求,帮助您减少带宽消耗,防止数据库、SMS、API资源亏空,减少响应延时,避免宕机等。针对多样业务场景支持自定义防护规则。
    集成大数据能力 每天约抵御数亿次网络攻击。拥有丰富的IP数据库。拥有广泛的应用案例,对各类常见网络攻击的模式、方法和签名有大量研究。大数据分析不断整合先进的技术。
    简易性、可靠性 5分钟内部署和激活。无需安装任何软硬件或调整路由配置。通过防护集群作用,避免单点故障和冗余。防护流量处理性能高。

    应用场景

    适用于阿里云以及阿里云外所有用户,主要用于金融、电商、O2O、互联网+、游戏、政府、保险等行业各类网站的Web应用安全防护。

    说明

    仅支持通过域名或实例方式接入WAF,不支持使用IP直接接入。

    如何使用WAF

    如何使用WAF

    更多信息,请参见快速使用WAF 3.0

    应用防护RASP和Web应用防火墙的关系

    应用防护RASP(Runtime Application Self-Protection)是一种运行在应用程序内部的安全保护机制,它能够在应用运行时检测攻击并进行自我保护。更多详情,请参见接入应用防护

    RASP和Web应用防火墙并不是相互取代的技术,而是在不同业务和安全防护场景下各有所长。例如,RASP更适合应对未知漏洞(0day漏洞)利用和加密流量等场景,而网络访问控制、区域封禁、CC攻击、爬虫攻击等威胁防护则需要WAF的有效补充。因此,对于应用防护来说,您需要根据业务环境和要求接入RASP以及Web应用防火墙,协同构建应用内生与边界双重防护能力,通过设置多层重叠的安全防护系统来构建多道防线,从而降低应用被入侵、数据泄露和服务不可用等风险。

    合规资质

    WAF已通过ISO 9001、ISO 20000、ISO 22301、ISO 27001、ISO 27017、ISO 27018、ISO 27701、ISO 29151、BS 10012、CSA STAR、等保三级、SOC 1/2/3、C5、HK金融、OSPAR、PCI DSS等多项国际权威认证。

    WAF作为标准的阿里云云产品,在云平台层面具备与阿里云同等水平的安全合规资质。详细内容,请参见阿里云信任中心

  • vue.js设计与实现(1)

    之前工作中做项目用到了VUE,实现了几个项目的完整开发。基本是后台管理系统,有一些涉及到人员管理树tree、table、search等组件的构建和表单页面的校验,审批流程页面组件的建立等,项目的webpack的打包发布等。后来发现一些原理性东西并没有十分的清晰,把一些东西杂糅在了一起。看到霍春阳的这本书评分很好,特意借来拜读和实践,梳理一遍自己对于vue的认知。

    首先把所有内容做了一个脑图,是按照书中的提示做的。

    文字版本

    vue.js设计与实现

    第一篇框架设计概览

    第1章讨论命令式和声明式,虚拟DOM的性能状况。vvue.js 3.0是一个运行时+编译时的框架。

    第2章主要从用户的开发体验、控制框架代码的体积、Tree- Shaking的工作机制、框架产物、特性开关、错误处理、typescript支持等方面出发,讨论了框架设计者再设计框架应该考虑的内容。

    第3章从全局介绍vue.js 3.0的设计思路,以及各个模块之间如何协作。

    第二篇响应系统

    第4章从宏观介绍vue.js3.0的响应系统的实现机制。从副作用函数开始,逐步实现一个完善的响应系统,还讲述了计算属性和watch的实现原理,同时讨论了在实现相应系统的过程中所遇到的问题,以及相应的解决方案。

    第5章从ECMAScript规范入手,从最基本的proxy、reflect以及js对象的工作原理开始,逐步讨论了使用proxy代理js对象的方式。

    第6章主要讨论了ref的概念,并基于ref实现原始值的响应式方案,还讨论了如何使用ref解决响应丢失的问题。

    第三篇渲染器

    第7章主要讨论了渲染器与响应系统的关系,讲述了两者如何配合完成页面更新。渲染器的一些基本名词和概念,以及自定义渲染器的实现与应用。

    第8章渲染器的挂载与更新的实现原理,其中包括子节点的处理、属性的处理和事件的处理。当挂载或更新组件类型的虚拟节点时,还要考虑组件生命周期函数的处理等。

    第9章主要讨论了简单Diff算法的工作原理。

    第10章主要讨论了双端Diff算法的工作原理。

    第11章主要讨论了快速Diff的工作原理。

    第四篇组件化

    第12章主要讨论了组件的实现原理,介绍了组件自身状态的初始化,以及由自身状态变化引起的组件自更新,还介绍了组件的外部状态props、由外部状态变化引起的被动更新,以及组件事件和插槽的实现原理。

    第13章异步组件和函数式组件的工作机制和实现原理。对于异步组件,我们还讨论了超时与错误处理、延迟展示loading组件、加载重试等内容。

    第14章vuejs内建的三个组件的实现原理,即keepalive teleport和transition的组件。

    第五篇编译器

    第15章讨论vuejs模板编译器的工作流程,接着讨论了parser的实现原理与状态机,以及AST的转换与插件化架构,最后讨论了生成渲染函数代码的具体实现。

    第16章主要讨论了如何实现一个符合whatwg组织的HTML解析规范的解析器,内容涵盖解析器的文本模式、文本模式对解析器的影响,以及如何使用递归下降算法构造模板AST。在解析文本内容时,我们还讨论了如何根据规范解码字符引用。

    第17章模板编译优化的相关内容。具体包括block树的更新机制、动态节点的收集、静态提升、预字符串、缓存内联事件处理函数、v-once等优化机制。

    第六篇服务器端

    主要讨论了同构渲染的原理,探讨了CSR、SSR以及同构渲染等方案的各自优缺点,然后探讨了vuejs进行服务端和客户端即获得原理,最后总结了编写同构代码时的注意事项。

    图片版本

  • SFC

    单文件组件 SFC

    在大多数启用了构建工具的VUE项目中,我们可以使用一种类似HTML格式的文件来书写VUE组件,它被称为单文件组件,也被称为*.vue文件。英文single-file components缩写为SFC。顾名思义,vue的单文件组件会将一个组件的逻辑 js模板html 和样式css封装在同一个文件里。

    VUE3中选项式API optionsAPI

    使用选项式API我们可以用包含多个选项的对象来描述组件的逻辑,例如data、methods和mounted。选项所定义的属性都会暴露在函数内部的this上,它会指向当前的组件实例。

     

    组合式API compositionAPI

    通过组合式API,我们可以使用导入的API函数来描述组件逻辑。在单文件组件中,组合式API通常会与<script setup>中的导入和顶层变量、函数都能够在模板中直接使用

  • mac安装nvm

    之前电脑装过各种环境,直接乱了。重新卸载node npm后再安装nvm。亲测可用,简单快速

    1、卸载node、npm

    brew uninstall node

    2、以上卸载完成后,继续清理

    which node

    //可以看到 /usr/local/bin/node

    which npm

    //可以看到 /usr/local/lib/node_modules/

    //全部删除,通过以下两句

    sudo rm -rf /usr/local/bin/node
    sudo rm -rf /usr/local/lib/node_modules/

    //删除全局node模块注册的软链 
    cd /usr/local/bin && ls -l | grep "../lib/node_modules/" | awk '{print $9}'| xargs sudo rm

    3、安装 nvm

    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
    
    4、关闭重启命令窗口,验证nvm版本
    nvm --vesion
  • 语法糖

    1. 什么是语法糖?‌

    语法糖(Syntactic Sugar)是由英国计算机科学家彼得·约翰·兰达提出的术语,指编程语言中添加的某种语法,这种语法对语言的功能没有影响,但更方便程序员使用。它通过简化代码结构,使代码更易读、更符合人类的思维习惯。

    2. 语法糖的作用

    • 提高可读性:简化代码结构,使代码更直观,更容易被其他开发者理解。
    • 减少错误:由于语法糖通常让代码更简洁,因此可以减少因复杂代码而导致的错误。
    • 提升效率:开发人员能够更快地写出正确的代码,从而提高编码效率。

    3. 语法糖的示例

    • C语言:用 a[i] 表示 *(a+i),用 a[i][j] 表示 *(*(a+i)+j)
    • Java:自动装箱/拆箱、增强的 for 循环、泛型以及Lambda表达式。
    • Python:列表推导式、字典推导式、集合推导式、生成器表达式。
    • VUE:v-bind:src=>:src v-on:click => @click

    4. 语法糖的意义

    语法糖不仅是一种便捷的写法,编译器会帮我们做转换,而且可以提高开发编码的效率,在性能上也不会带来损失。它与其他编程思想一样重要,如duck type、人本接口、最小接口、约定优于配置等,广义来讲都是一些思想上的“语法糖”。

    5. 语法糖的争议

    尽管语法糖带来了诸多便利,但也有反对的声音。例如,图灵奖的第一个获得者Alan Perlis曾对此提出质疑。然而,大多数开发者认为,语法糖是一种让编程语言更加用户友好、更具表现力的方式。

    通过以上分析,我们可以看到,语法糖在编程语言中扮演着重要角色,它不仅简化了代码,还提高了开发效率和代码的可读性。

  • 混沌工程(Chaos Engineering)

    最近在看SLA,看到里面一个词叫混沌工程,故整理一下。

    定义:混沌工程是一门对系统进行实验的学科,旨在了解系统对应生产环境的各种混乱状况的能力,建立对系统的信心。所有系统的用户都希望系统具备可靠性,但影响可靠性的因素有很多。混沌工程师能找到证据,指明那些异常但不可回避的状况下系统的应变情况。

    简单来说:混沌工程是在分布式系统上进行实验的学科,目的是建立对该系统能够承受生产环境的动荡的信心。

    我们的用户将 TiDB 运行在国内某云厂商的机器上面,然后跟我们反映,读延迟会不定期的增长,我们看了看监控,发现唯一的异常指标就是 Cached 的 memory 那段时间会突然下降。当时真的就懵逼了,完全不知道是为啥,最终发现,云厂商的运维监控脚本里面有个 bug,会不定期的将磁盘热拔插,并且将现有的 page cache 刷到磁盘,所以那段时间 TiDB 的 read 操作很多是从磁盘重新读取数据的。

    可以看到,分布式系统真的是一个非常复杂的系统,故障无处不在,那么我们如何在这么复杂的分布式系统的世界里面生存下去呢?现在,一个很好的答案就是 – Chaos Engineering,中文里面叫做混沌工程。

    Netflix工程师创建了Chaos Monkey,使用该工具可以在整个系统中在随机位置引发故障。正如GitHub上的工具维护者所说,“Chaos Monkey会随机终止在生产环境中运行的虚拟机实例和容器。”通过Chaos Monkey,工程师可以快速了解他们正在构建的服务是否健壮,是否可以弹性扩容,是否可以处理计划外的故障。
    2012年,Netflix开源了Chaos Monkey。今天,许多公司(包括谷歌,亚马逊,IBM,耐克等),都采用某种形式的混沌工程来提高现代架构的可靠性。 Netflix甚至将其混沌工程工具集扩展到包括整个“Simian Army(中文可以译为猿军)”,用它攻击自己的系统。

    混沌工程

    相比于我们成天担惊受怕系统会出现什么样的问题,还不如提前就模拟线上环境可能出现的各种情况,来看我们的系统是否能做到容错,仍然能继续对外提供服务。当然,我们并不是简单的就在线上环境上面,把机器给断电,或者把网线给拔掉,在混沌工程领域,有一套指导原则,以及标准的实验步骤,具体的可以参考 PRINCIPLES OF CHAOS ENGINEERING 

    简单来说,要做一次混沌实验,我们只需要做到如下的 4 个步骤:

    1. 定义系统的稳态,这个稳态就是系统在正常运行的时候一些指标,譬如当前请求的 QPS,latency 这些。
    2. 将系统分为实验组以及对照组,做出一个假设,譬如我在实验组引入一个故障,这个稳态仍然能在实验组保持。
    3. 执行试验,给实验组引入现实世界中的故障,譬如拔掉网卡。
    4. 验证第 2 步的假设是否成立,如果实验组的稳态跟对照组不一样了,证明我们的系统在第 3 步的故障中不能很好的容错,所以我们需要改进。

    可以看到,上面的步骤非常的简单,但要在实际从很好的做混沌试验,还是有一些困难的,主要在以下几点:

    1. 自动化,我们需要有一套自动化的系统帮我们进行故障注入,进行假设对比等。
    2. 尽可能多的引入不同故障。现实环境中可能会出现非常多的故障,仅仅不是拔网线这么简单,所以引入的故障越多越好。
    3. 业务方无感知。如果我们每次做混沌试验,都要业务系统去配合,譬如在业务里面写一些混沌相关的代码,让混沌试验调用,或者更改系统的部署逻辑,跟混沌试验配合,这种的就属于紧耦合的。

    你好,ChaosMesh!!!

    所以,为了让大家更好的做混沌试验,我们开发了 ChaosMesh,ChaosMesh 是一套基于 Kubernetes 的云原生混沌工程平台。ChaosMesh 的架构如下:

    相比于其他混沌平台,ChaosMesh 有如下优势:

    1. 基于 K8s,只要你的系统能跑在 K8s 上面,那么就可以无缝的集成 ChaosMesh,而且不用修改任何业务代码,真正是被测系统无感知。
    2. 多种多样的故障注入。ChaosMesh 能全方位的帮你对网络,磁盘,文件系统,操作系统等进行故障注入。我们后面也会提供对 K8s,或者云服务自身进行 chaos 的能力。
    3. 易于使用,你无需关注 ChaosMesh 的底层实现细节,只需用 YAML 配置好混沌试验,就可以实施,后面所有的实验是全自动化的。我们也提供了 Dashboard 能让你在网页上就轻松的进行试验。
    4. 可观测性,ChaosMesh 的 Dashboard 能很方便的让你观测系统,知道什么时候进行了什么试验,知道你自己的系统当前的运行情况,当然,这里需要一点配置,你需要告诉 ChaosMesh 如何去获取你系统的稳态指标,譬如你的系统使用 Prometheus,那么就可以告诉 ChaosMesh 如何去 Prometheus 查询相关的监控指标。
    5. 强大的开源社区支持,ChaosMesh 的社区成长的非常迅速,我们非常高兴的看到大部分的功能已经由社区支持,并且也有很多用户。你无需担心遇到问题不知道如何解决,当然,你可能要担心下 ChaosMesh 做实验的时候把你的数据给完全干掉,所以做实验的时候一定要控制好实验半径,这个也是混沌工程的一条原则。

    来一次 Chaos 实验?

    在我们开始一次 Chaos 实验之前,你首先需要满足两个条件:

    1. 你自己的业务是跑在 K8s 上面的
    2. 在 K8s 上面安装了 ChaosMesh

    另外,在开始实验之前,这里我还是要强调一下 Chaos 实验的一些注意事项,可能你觉得我这个大叔很啰嗦,但小心驶得万年船,因为稍微一不注意,你可能就丢了数据了。

    1. 如果你刚准备将你的系统应用 ChaosMesh,一定要保证首先在测试环境中使用。你的系统应该还非常的脆弱,如果在线上进行试验,会非常的危险。
    2. 在生产系统中,一定要控制好试验的爆炸半径,控制好影响范围,譬如我们可以先对某一个街道的用户进行干扰,然后在扩大到某一个区域,或者某一个城市,如果我们一开始的影响半径就很大,一个稍微不留意,你的 boss 就可能让你第二天滚蛋了。
    3. 做混沌实验一定不是随机的瞎做实验,我们是带有目的的,是需要规划好的,与其漫无目的的对系统随机进行故障注入,我们还不如先问自己一个问题『为了对系统在混乱状况下的表现更有信心,在哪里做混沌实验最有价值?』也就是我们要熟悉了解我们的系统,做高杠杆价值的混沌实验。

    好了,现在你已经完全准备好了,现在就可以踏上混沌之旅了,因为 ChaosMesh 的使用是如此简单,你只需要参考 用户指南 就能上手使用,所以我就不过多介绍了,如果你仍然遇到了问题,欢迎给 ChaosMesh 提 issue,相信我,ChaosMesh 社区会很热情的帮你解决问题的。

    总结

    随着 ServiceMesh,Serverless 等理念的兴起,我们的系统真的趋向于越来越分布式,这样虽然简化了我们单个模块的实现,但整体来看,也可能会导致我们的系统因为过于分布式而变得复杂,那么如何在这种复杂的环境下仍然让我们有信心能保证系统能正常稳定运行,混沌工程可以算是一个很不错的选择。

    现在市面上面,支持混沌工程的平台已经有很多了,但我这里仍然推荐 ChaosMesh,毕竟使用它能让你极大提升你对系统的信心。

    该咋做混沌工程?

    在讨论咋做混沌工程之前,让我们先回答下面5个问题。

    什么是混沌工程?

    先说一个能说出点名堂的我个人对混沌工程的理解,**混沌工程,是Netflix公司当面对具有不可预知的“暗债”的复杂系统时,在“目标一致,关系宽松”的企业文化下,“逼上梁山”的结果**。

    什么是“暗债”?这个词来源于2017年发表的一篇名为[STELLA](https://snafucatchers.github.io/)的报告。这份报告由一群韧性工程专家在聚会后所撰写。就在他们聚会的那几天,这群专家赶上了一场极具破坏性的暴风雪。而那场暴风雪的名字,就是 STELLA。

    **暗债**,指当复杂系统内部子系统间相互作用时,所**必然存在**的**不可预知**的漏洞,最终会由此引发系统的意外故障。

    再说说上面那个“除了‘实验’貌似啥也没说”的定义。

    2015年,Netflix将混沌工程正规化,主持起草了[混沌工程原则](https://principlesofchaos.org/?lang=ENcontent),并给混沌工程下了定义。

    混沌工程是在分布式系统上进行实验的学科,目的是建立对该系统能够承受生产环境的动荡的信心。

    “貌似啥也没说”的背后,其实这个定义是大有深意的。

    • 分布式系统就是一种复杂系统,其活动规律是**不可预知和非线性的**。详情参见我之前撰写的[“不可能构建第二个云环境去做测试”](“不可能构建第二个云环境去做测试”——为复杂混沌的微服务生产环境设计韧性系统 v0.11)和[“混沌工程与系统稳定性设计模式”](https://myslide.cn/slides/22341)
    • 生产环境的动荡,来源于复杂系统内部**所固有的“暗债”**。见与不见,暗债就在那里,不增不减
    • 具有“可证伪性”的科学实验,只关注所定义的稳态是否被证伪,而对于**具体采取哪种方法保持稳态,不做要求**,悉听尊便,这就孕育了混沌猴
    • 要建立对系统能够承受生产环境的动荡的信心,需要**针对生产环境“丰富多彩”的暗债,设计同样“丰富多彩”的防范手段**。依据是控制论学者艾什比的[必要多样性法则(Law of Requisite Variety)](艾什比定律_百度百科)——要实现控制,控制系统能够执行的行为的多样性,必须不低于需要应对的环境动荡的多样性
    • 一个不断演化的分布式系统的复杂性,只会增加,而不会减少。依据是著有《人月神话》的那位大神Frederick Brooks的一篇文章中所谈到的两类复杂性(偶然复杂性与本质复杂性),都不会减少,甚至有一类还会增加。

    其中,偶然复杂性,是在资源有限的条件下,对相互冲突的限制条件作出权衡后的必然结果。

    不存在已知的可持续方法,能减少偶然复杂性,因为资源总是有限的,权衡总是要做出的,暗债总是要欠下的。

    而本质复杂性,是满足不断增加的新需求的必然结果。一个不断演化的分布式系统,新需求能少得了吗?

    混沌工程和测试有什么区别?

    两者的运行环境有区别。

    测试一般运行在**测试环境**上,而混沌猴一般运行在**生产环境**上。因为生产环境必然存在不可预知且与测试环境不同的暗债,所以对测试环境所建立的信心,并不能用在生产环境上。

    两者的侧重点也有区别。

    测试一般只关注**已知**的断言是否通过,而混沌工程会更关注发现更多**未知**的暗债。

    混沌工程和故障演练啥区别?

    两者的侧重点有区别。

    故障演练侧重操练**已知**的故障应对过程,而混沌工程侧重通过实验发现**未知**的暗债。

    生产环境是不允许引入任何风险的。如果混沌工程是在生产环境上做实验,那风险多大呀。领导会答应吗?

    前面提到,暗债是生产系统这个复杂系统所固有且不可预知的。那么即使不人为地引入风险,生产环境也是遍布暗债的。不在生产环境进行最小化爆炸半径的实验,来发现暗债,那么**只能任凭暗债在最不希望发生的时刻爆发**。

    该咋做混沌工程?

    借鉴Netflix的实例,可以从“摆正心态、人员主动和试点业务”三方面入手,来启动混沌工程。

    ### 摆正心态

    承认暗债为复杂系统所固有,而不是一味要求工程师[“不能也不该出现失误”](6月27日阿里云故障说明-阿里云开发者社区)。否则在故障面前,大家就只会花大量时间相互甩锅,耽误了发现更多暗债和防范措施。

    ### 人员主动

    前面说过,根据必要多样性法则,要建立对系统能够承受生产环境的动荡的信心,需要针对生产环境“丰富多彩”的暗债,设计同样“丰富多彩”的防范手段。而技术骨干一个人,是发现不了那么多暗债,并找到那么多的防范手段的。所以,就需要发挥各位工程师的主动性。此时,领导者要创造能调动工程师主动性和创造性的企业文化,来促进工程师更安全地发现与修复更多“花样”的暗债。在修复暗债的过程中,就可以使用文章[【分布式系统稳定性设计入门】如果不想总是半夜爬起来抢修生产事故……《发布!》第2版解读](【分布式系统稳定性设计入门】如果不想总是半夜爬起来抢修生产事故……《发布!》第2版解读 v0.2)所介绍的“分布式系统稳定性设计关键清单”。

    ### 试点业务

    • 选择一个出现生产事故频率较高的业务系统,尝试混沌工程。因为事故的反复,出现会让发现与解决暗债的动力更大
    • 基于能反映用户体验的业务稳态行为建立假设,而不是先聚焦于在系统内寻找弱点。因为这样能更利于进行全局优化,让成效更大
    • 为了让暗债浮现出来,设计引入足够多样化的现实世界可能发生的事件,而不是设计那些易于生成但在现实中不大可能出现的事件,以便切中要害。针对每一个所引入的事件,参考上述“分布式系统稳定性设计关键清单”,来进行稳定性设计
    • 可以先从准生产环境入手进行混沌实验,等条件成熟后,再逐渐过渡到生产环境
    • 自动化地持续进行混沌实验,以起到回归实验的效果,持续发现并解决暗债,避免系统随着时间的推移,在韧性方面逐渐“掉队”
    • 设计更安全的实验方式,以最小化爆炸半径,让实验所导致的业务损失降到最低,而不是明知故障难以控制,还要贸然进行实验。如果实验的假设被证伪,那么就遇到了发现新的暗债的好机会。在寻找暗债的过程中,可以参考上述“分布式系统稳定性设计关键清单”,来启发寻找漏洞及修复

    混沌工程的唯一目标就是证明系统存在缺陷。通过开展混沌工程方面的科学实验,你可以测试系统是否存在缺陷,从而了解系统在混乱的类生产环境条件下如何表现。

    混沌工程属于一门新兴的技术学科,行业认知和实践积累比较少,大多数IT团队对它的理解还没有上升到一个领域概念。阿里电商域在2010年左右开始尝试故障注入测试的工作,希望解决微服务架构带来的强弱依赖问题。

    参考来源1:https://zhuanlan.zhihu.com/p/149599011
    参考来源2:https://zhuanlan.zhihu.com/p/149419512

  • 微信小程序分享朋友圈和转发朋友

    onShareAppMessage: function() {
    	wx.showShareMenu({
    	      withShareTicket: true,
    	      menus: ['shareAppMessage', 'shareTimeline']
          })
          return {
            title: '帮我选车',
            path: ''
    	    }
    	},
    	//用户点击右上角分享朋友圈
    	onShareTimeline: function () {
    		return {
    	      title: '帮我选车',
    	      query: {
    	        key: ''
            },
    	      imageUrl: ''
    	    }
    	},

    体验版已支持iOS和android,只是微信官方文档还未做更新