前端多线程web worker详细教程,请保存~

大家好,我是小核桃,最近,在项目中遇到了大量图片下载并缓存的需求,面临着瞬时带宽过大,前端界面过慢,甚至卡死等问题。

所以脑洞大开,想拿Web Worker尝试一下,看看能否有性能上的提升,所以,我花费将近两天的时间整理出了下面的教程,与大家共勉。文章过长,看不完的可以保存起来慢慢看。

简介

Web Worker 是 Web 内容在后台线程中运行脚本的简单方法。工作线程可以在不干扰用户界面的情况下执行任务。
我们都知道,javascript语言是单线程模型,只有等一个任务完成后才能开始下一个任务。
尽管可以使用异步代码,但实际上在执行一个任务的时候其它任务都会被阻塞住,这就会出现一个非常严重的问题,如果一个页面中有复杂的计算或IO密集型的任务长时间运行,那么其他界面就会卡住,甚至出现无影响,此外,如果一个任务需要等待某个事件的发生(比如用户输入、服务器响应等),就会浪费一些宝贵的 CPU 时间,导致资源浪费和效率低下。
为了解决这些问题,诞生了Web Worker,它能够让javascript利用多核CPU的优势,实现真正的并行计算。
其设计思想是在主线程运行的基础上开启一个新的工作线程,并让网页在执行一些复杂或耗时的任务时,将任务交给工作线程执行,在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。
等到 Worker 线程完成任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(一般用来负责UI渲染和DOM相关操作)就会很流畅,不会被阻塞或拖慢。
也就是说,复杂的任务不会影响界面的交互和响应,从而提高网页的性能和流畅度。就像在Android原生端那样,UI是主线程,所有的网络请求必须在子线程中进行。

基本工作线程使用

使用思路:可以将复杂的任务,比如下载多个文件这样的任务抽取出来,放到工作线程中。在主线程中创建工作线程。等工作线程的任务执行完成后,再通过线程通信方式将数据发给主线程,再在界面上进行渲染数据。

主线程(main.js)

worker 是使用构造函数,例如 Worker()创建的对象,该构造函数的参数是个js脚本文件,这个js文件包含了需要在工作线程中运行的代码。主线程使用new Worker() 的方式创建工作线程,如下所示:

// 构造函数
new Worker(aURL)
new Worker(aURL, options)

//示例
const myworker = new Worker("worker.js")

这样就在主线程中就可以使用myworker工作线程了。为了更好的处理错误和向后兼容,一般写法如下:

if (window.Worker) {
//示例
const myworker = new Worker("worker.js")
}

在主线程中,我们可以通过 postMessage() 方法向工作线程中发送消息,并使用 onmessage 监听消息事件从工作线程中接收消息,方向反过来也是如此。

postMessage() 可以传递任意类型的数据,包括二进制数据

  //字符串
myWorker.postMessage("你好,我是主线程");

//数组
myWorker.postMessage([1,2,3,4,5,6]);

//json对象或普通object对象
myWorker.postMessage({"name":"zhangsan"});

console.log("消息发送给了工作线程");
在主线程监听工作线程发来的消息,使用 onmessage 来响应从工作线程发回的消息:
myWorker.onmessage = (e) => {
result.textContent = e.data;
console.log("从工作线程接收到了消息,消息内容为"+ result.textContent);
};

子线程(worker.js)

在worker线程中,需要做2件事:
(1)接收从主线程发来消息,并进行任务处理。
(2)处理完成后,将数据发送给主线程。
这里需要注意的是,工作线程和主线程是运行在不同的全局上下文中,在工作线程中,不能使用window对象,而是要使用self,self 代表子线程自身,即子线程的全局对象。
所以,可以在子线程中使用 self.addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数的参数是一个事件对象,其data属性包含主线程发来的数据。同样的,我们使用self.postMessage()方法来向主线程发送消息。
self.onmessage = (e) => {
console.log("从主线程接收到消息");
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
console.log("发送消息给主线程");
postMessage(workerResult);
};

或者,像下面这样

self.addEventListener('message', function (e) {
console.log("从主线程接收到消息");
let data = e.data;
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);break;
case 'stop':
self.postMessage('WORKER STOPPED: ' + data.msg);
self.close(); // 在worker线程内部关闭工作线程.
break;
default:
self.postMessage('Unknown command: ' + data.msg);};}, false);

注意:在主线程中,要使用 Worker 对象的 onmessage和 postMessage() 方法,来监听和发送消息,而在 Worker 线程中,可以直接使用 onmessage 和 postMessage() 方法,不需要加上 Worker 对象的前缀,因为 Worker 线程中的 this 或 self 就是指向 Worker 对象的。也就是说在work线程中可以省略掉 self 或 this 关键字。

终止工作线程

//在主线程中终止工作线程可以使用
myWorker.terminate();

//在子线程中终止
self.close();
终止正在运行的工作线程后,工作线程立即被终止。

处理错误

当工作线程中发生运行时错误时,将调用其 onerror 事件处理程序。它接收名为 error 的事件,该事件实现了ErrorEvent 接口。可以在主线程监听主线程的错误信息
// 为 Web Worker 对象添加一个 onerror 事件处理函数,用于捕获并显示错误的信息
myworker.onerror = function(event) {
// 阻止默认的错误处理行为
event.preventDefault();
// 在控制台中打印错误的信息console.log("Error in Web Worker:");
console.log("错误信息: " + event.message);
console.log("文件脚本: " + event.filename);
console.log("行号: " + event.lineno);
};

//控制台运行
Error in Web Worker:
错误信息: Something went wrong!
文件脚本: worker.js
行号: 2
在子线程中可以主动抛出,或自动抛出运行时异常
// worker.js
// 在 Web Worker 中故意抛出一个错误
throw new Error("Something went wrong!");

使用web worker的规则及限制

(1)一个 工作线程可以创建和使用其他的工作线程 ,也就是说,可以在后台线程中再开启更多的后台线程,实现多层次的并行计算。
(2)为了保证网页的安全性,子 工作线程必须和创建它的父 工作线程 在同一个域名下,不能跨域访问其他网站的 工作线程的脚本文件,要满足同源策略。
(3)除此之外,为了方便的管理依赖文件,子 Worker 的文件路径是相对于父 Worker 的文件路径,而不是相对于网页的文件路径,举个例子,如果父 Worker 的文件路径是 https://example.com/worker.js ,那么子 Worker 的文件路径应该是 https://example.com/subworker.js ,而不是 https://example.com/page/subworker.js 。这样做可以让Worker工作线程更容易地找到它们所需要的文件,而不用担心文件路径的问题。

在主线程中导入脚本和库

在工作线程中,如果想导入一些配置文件、公共的模块、服务等脚本时,可使用 importScripts() 函数。
importScripts() 函数是一种在 Web Worker 中导入其他脚本的方法,它用在在工作线程中使用其他脚本中定义的变量、函数、对象等。
importScripts() 函数可以接受零个或多个 URI 作为参数,表示要导入的脚本的文件路径。这些文件路径可以是绝对的或相对的,如果是相对的,那么是相对于工作线程脚本的文件路径,而不是网页的文件路径,例如,以下都是有效的 importScripts() 函数的调用:
// 导入一个脚本
importScripts("foo.js");
// 导入多个脚本
importScripts("foo.js", "bar.js");
// 导入绝对路径的脚本
importScripts("https://example.com/foo.js");
// 导入相对路径的脚本
importScripts("../foo.js");
浏览器会按照参数的顺序,依次加载和执行每个脚本,然后我们 就可以在工作线程中使用每个脚本中的全局对象。比如,如果 foo.js 中定义了一个变量 a 和一个函数 b ,那么 在工作线程中 就可以使用 a 和 b 。
需要注意的是,这些全局对象是在 工作线程的全局作用域中,而不是在主线程的全局作用域中,也就是说,主线程不能直接访问它们。
importScripts() 函数是同步的,也就是说,它会阻塞 Web Worker 的执行,直到所有的脚本都被加载和执行完毕。如果有任何一个脚本无法加载,那么会抛出一个 NETWORK_ERROR 的错误,而且后面的代码都不会执行。
不过,之前执行的代码(包括使用 setTimeout() 延迟的代码)仍然有效,因为它们已经被加载到工作线程的内存中了。另外,importScripts() 函数之后的函数声明也会被保留,因为函数声明总是在代码的其余部分之前被解析。
下面给大家列举一些importScripts() 函数的应用场景:
  • 当我们在worker工作线程使用一些公共的或复用的功能时,可以导入一些通用的或模块化的脚本,比如一些工具函数、库函数、算法函数等。
  • 在工作线程中使用一些动态的或条件的功能时,导入一些根据不同情况选择的脚本,比如一些配置文件、语言文件、主题文件等。
  • 在工作线程使用一些异步的或延迟的功能时,可以通过 importScripts() 函数导入一些在特定时间或事件触发的脚本,比如一些更新文件、通知文件、回调文件等。
下面来看看这个函数如何使用,为了方便我就将案例写到同一个脚本中
/** main.js */

// 创建一个 Web Worker 对象,指定 worker.js 作为执行内容
var worker = new Worker("worker.js");
// 为 Web Worker 对象添加一个 onmessage 事件处理函数,用于接收并显示 Web Worker 发送的消息
worker.onmessage = function(e) {
// 在控制台中打印消息
console.log(e.data);
};

/** worker.js */

// 使用 importScripts() 函数导入两个脚本
importScripts("first.js", "second.js");
// 使用 foo.js 中定义的变量 a
var b = a + 1;
// 使用 bar.js 中定义的函数 c
var d = c(b);
// 使用 postMessage() 函数,向主线程发送一个消息
postMessage(d);

/** first.js*/
// 定义一个变量 a
var a = 5;

/** second.js*/
// 定义一个函数 c
function c(x) {
return x * x;
}
在主线程中输出36,这是因为工作线程 导入了 first.js second.js ,然后使用了它们中定义的变量 a 和函数 c ,计算出了结果 36 ,并发送给了主线程。这样,就可以通过 importScripts() 函数,在 工作线程 中使用其他脚本中的功能了。

共享工作线程

前面我们介绍了基本工作线程的使用方法,现在来聊聊共享工作线程,二者的区别在于,基本的工作线程只作用于生成它的主线程,它是由一个或一个脚本创建的,只能和创建它的网页或脚本进行通信。
而共享的工作线程是由多个网页或者脚本共同创建的,它可以和所有创建它的网页或者脚本进行通信,也就是说,它可以在不同的网页或者脚本之间共享数据和消息。
共享的工作线程适合处理一些与多个网页或者脚本共享的后台任务,比如缓存、数据库、网络请求等。
使用方式和基本工作线程类似,一个很大的区别是,对于共享的工作线程,必须通过 port 对象进行通信, 并打开一个显式端口,脚本可以使用该端口与工作线程进行通信(在基本的工作线程中是隐式完成的)。
在发布任何消息之前,需要使用 onmessage 事件处理程序隐式启动端口连接,或者使用 start() 方法显式启动端口连接。仅当 message 事件通过 addEventListener() 方法连接时才需要调用 start() 。
需要注意的是:使用 start() 方法打开端口连接时,如果需要双向通信,则需要父线程和工作线程都调用该方法。

基本用法(差异点)

使用new SharedWorker 创建共享的工作线程:

const myWorker = new SharedWorker("worker.js");

主线程向共享线程发送消息,必须过端口对象调用 postMessage() 方法

 //主线程向共享线程发送消息
myWorker.port.postMessage([12, 'hello']);

在共享线程中

onconnect = (e) => {
const port = e.ports[0];

port.onmessage = (e) => {
const workerResult = `Result: 你好,主线程`;
port.postMessage(workerResult);
};
};
首先,我们使用 onconnect 与主线程通过端口建立连接(即,当父线程中的 onmessage 事件处理程序设置时,或者当 start() 方法在父线程中显式调用时),此时,主线程要建立消息监听。
onconnect 的参数是一个事件对象,可以使用此事件对象的 ports 属性来获取端口并将其存储在变量中。
接下来,我们在端口上添加一个 onmessage 方法来进行任务处理并将结果返回到主线程去。在工作线程中设置 onmessage时, 还会隐式打开返回到父线程的端口连接,因此实际上不需要调用 port.start()
最后,在主线程中监听共享线程发来的消息
myWorker.port.onmessage = (e) => {
result2.textContent = e.data;
console.log("Message received from worker");
};

数据通信

在前面,我们只是简单描述了基本使用方式,下面来看看具体的通信机制是什么。
主线程与工作线程之间的通信可以是文本、对象、字符串等,但这不是线程之间的数据共享,而单纯的是数据的拷贝,也就是是值传递,而不是地址传递。对象在传递给工作线程时被序列化,随后在另一端反序列化。二者之间线程不共享同一个实例,因此最终结果是在每一端都会创建一个副本,即,worker线程的数据修改,不会影响主线程的数据。

对此,大多数浏览器都是基于结构化克隆算法实现的。所谓的结构化克隆算法是一种复制复杂的 JavaScript 对象的方法,它可以让你在不同的地方使用同样的对象,比如在不同的线程之间传递数据,或者在数据库中存储数据,或者给其他的功能使用数据。它会把你要复制的对象分解成很多小的部分,然后一一复制这些部分,最后再组合成一个新的对象,这个新的对象和原来的对象一模一样,但是是一个独立的副本,不会影响原来的对象。

传递复杂类型

平时,我们可以使用json做序列化与反序列化,但是JSON不能用来处理循环引用的情况,比如下面是一个循环引用(指一个对象的属性引用了这个对象本身,或者引用了另一个对象的属性,而另一个对象的属性又引用了这个对象,形成了一个循环。)的例子:

// 创建一个对象 a
var a = {};
// 创建一个对象 b
var b = {};
// 让 a 的属性 x 引用 b
a.x = b;
// 让 b 的属性 y 引用 a
b.y = a;

如果你用 JSON 来复制这个循环引用的对象,会出错,因为 JSON 不能处理循环引用,它会认为这是一个无限循环,无法转换成字符串。如果你这样写会报错

// 使用 JSON 复制循环引用的对象
let c = JSON.parse(JSON.stringify(a)); // 报错

但是如果你用结构化克隆算法来复制这个循环引用的对象,就不会出错,因为结构化克隆算法可以处理循环引用,它会记录每个对象的引用,避免无限循环,正确地复制每个对象。

// 用结构化克隆算法复制循环引用的对象
let c = structuredClone(a); // 不报错

对于传递复杂类型:比如对象、数组、函数等。这些类型需要经过结构化克隆算法(Structured Clone Algorithm)进行序列化和反序列化,复杂类型的传递都会有一定的性能影响。

传递可转移对象

除了复杂的类型,主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。默认情况下,如果我们通过postMessage(ArrayBuffer.toString())直接将数据发送出去,那么就又会出现二进制的拷贝,因为前面我们说了,大多数浏览器都是基于结构化克隆算法实现的数据拷贝,如果文件比较大,那么无疑会造成非常大的性能问题。
为了解决这个问题,现代浏览器包含一种额外的方法来将某些类型的对象传递给工作线程或从工作线程传出某些类型的对象。可传输对象通过零复制操作从一个上下文传输到另一个上下文,例如,主线程把二进制数据直接转移给工作线程,主线程就无法再使用这些二进制数据了,即,原始 ArrayBuffer 将被清除并且不再可用。这样做的目的就是为了防止出现多个线程同时修改数据。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker。此方式在发送大型数据集时会带来巨大的性能改进。
使用方式
//使用方式 Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例如
const uInt8Array = new Uint8Array(1024 * 1024 * 32).map((v, i) => i);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

可传输对象支持跨代理传输。传输是有效地重新创建对象,同时共享对底层数据的引用,然后分离正在传输的对象。

注意:转账是不可逆且非幂等的操作。一旦对象被转移,它就不能被再次转移,甚至不能被再次使用。

注意:像 Int32Array 和 Uint8Array 这样的类型化数组是可序列化的,但不可传输。然而,它们的底层缓冲区是一个 ArrayBuffer ,它是一个可传输的对象。我们可以在数据参数中发送 uInt8Array.buffer ,但不在传输数组中发送 uInt8Array 。

当使用 structuredClone() 创建对象的深层副本时,也可以使用传输。克隆操作之后,传输的资源将被移动而不是复制到克隆对象。
const original = new Uint8Array(1024);
const clone = structuredClone(original);
console.log(original.byteLength); // 1024
console.log(clone.byteLength); // 1024

original[0] = 1;
console.log(clone[0]); // 0

// Transferring the Uint8Array 会报异常,因为 它不是一个可传输(转移)对象
const transferred = structuredClone(original, {transfer: [original]});

// We can transfer Uint8Array.buffer.
const transferred = structuredClone(original, { transfer: [original.buffer] });
console.log(transferred.byteLength); // 1024
console.log(transferred[0]); // 1

//转移后 Uint8Array.buffer 不可再用
console.log(original.byteLength); // 0
下面来一个案例来加深一下印象:主线程获取图片,传递到工作线程中,在工作线程中将二进制流转换为ImageBitmap,再传给主线程,主线程使用画布画出来。
主线程代码:
// 主线程代码

// 获取图片文件
const file = document.getElementById("file").files[0];
// 创建 FileReader 对象
const reader = new FileReader();
// 创建 Worker 对象
const worker = new Worker("worker.js");
// 为 FileReader 对象添加一个 onload 事件监听器,用于在文件读取完成后执行
reader.onload = (e) => {
// 获取 ArrayBuffer 类型的数据
const buffer = e.target.result;
// 将 ArrayBuffer 类型的数据作为可转移对象传递给 Web Worker
worker.postMessage(buffer, [buffer]);
};
// 为 Worker 对象添加一个 onmessage 事件监听器,用于接收 Web Worker 传递的数据
worker.onmessage = (e) => {
// 获取 ImageBitmap 类型的数据
const bitmap = e.data;
// 获取 canvas 元素
const canvas = document.getElementById("canvas");
// 获取 canvas 的绘图上下文
const ctx = canvas.getContext("2d");
// 将 ImageBitmap 类型的数据绘制到 canvas 上
ctx.drawImage(bitmap, 0, 0);
};
// 以 ArrayBuffer 类型的方式读取图片文件
reader.readAsArrayBuffer(file);

子线程代码:

// worker.js
// 为 Worker 添加一个 onmessage 事件处理函数,用于接收主线程传递的数据
onmessage = (e) => {
// 获取 ArrayBuffer 类型的数据
const buffer = e.data;
// 将 ArrayBuffer 类型的数据转换为 ImageBitmap 类型的数据
createImageBitmap(buffer).then((bitmap) => {
// 将 ImageBitmap 类型的数据作为可转移对象传递回主线程
postMessage(bitmap, [bitmap]);
});
};

有哪些可转移对象

  • ArrayBuffer

  • MessagePort

  • ReadableStream

  • WritableStream

  • TransformStream

  • WebTransportReceiveStream

  • WebTransportSendStream

  • AudioData

  • ImageBitmap

  • VideoFrame

  • OffscreenCanvas

  • RTCDataChannel

嵌入式工作线程

通常我们都会将工作线程的代码单独放到一个脚本中。而嵌入式的工作线程,就是在主线程的代码中 通过一定的方式,将工作线程的脚本嵌入进来。这样就保证了主线程和工作线程在一个页面中。
在主线程中通过 <script type="text/js-worker"> 的方式来嵌入工作线程的代码块,这段代码块自定义了 type属性,这个属性浏览器不认识,所以它不能被js引擎解析,这样就可以了。
<script type="text/js-worker">
function handleWorkerTask(){
// 处理工作线程的任务
return "任务处理完成";
}
onmessage = (event) => {
let result = handleWorkerTask();
postMessage(result);
};

</script>
在主线程中创建工作线程,这样的 type 属性默认为text/javascript,所以可以被JS引擎解析。
 <script>

//将上面的代码块脚本生成二进制blob对象
const blob = new Blob(
Array.prototype.map.call(
document.querySelectorAll("script[type='text/js-worker']"),
(script) => script.textContent,
),
{ type: "text/javascript" },
);

// 为blob二进制对象生成URL,并创建工作线程
document.worker = new Worker(window.URL.createObjectURL(blob));

document.worker.onmessage = (event) => {
console.log("从子线程中接收到消息");
};
// 启动工作线程.
window.onload = () => {
document.worker.postMessage("");
};

</script>

如上代码,其工作机制是先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。

工作线程的调试

大多数浏览器都允许您在 JavaScript 调试器中调试 Web Worker,其方式与调试主线程完全相同!例如,Firefox 和 Chrome 都会列出主线程和活动工作线程的 JavaScript 源文件,并且可以打开所有这些文件来设置断点和日志点。

Worker 线程嵌套

指的是在一个工作线程中,可以继续创建新的工作线程。这个使用方式是一样的。

结语

最后,整理一下工作线程可以调用的API和不能调用的API,并结合当前vue框架中的网络请求如何使用工作线程做个简单的说明:

在工作线程中,可以运行任何标准的 JavaScript 代码,不过有一些例外情况。给大家整理了一份工作线程中支持和不支持的 API 表格,供参考:

支持的API 不支持的API
基本的 JavaScript 语法和内置对象,如 Array, Object, Math, JSON, Promise 等 DOM API,如 document, window, Element, Event 等
Web Worker API,如 Worker, MessagePort, MessageChannel, BroadcastChannel 等 BOM API,如 navigator, location, history, screen 等
Web Storage API,如localStorage, sessionStorage 等 HTML API,如 Audio, Video, Image, Canvas, FormData 等
IndexedDB API,如IDBFactory, IDBRequest, IDBTransaction 等 Web Audio API,如AudioContext, AudioNode, AudioParam 等
Fetch API,如fetch, Request, Response, Headers 等 WebRTC API,如 RTCPeerConnection, RTCDataChannel, MediaStream 等
WebSocket API,如WebSocket, CloseEvent, MessageEvent 等 Service Worker API,如ServiceWorker, ServiceWorkerRegistration, ServiceWorkerGlobalScope 等
File API,如 Blob, File, FileReader, URL 等 Notification API,如Notification, NotificationEvent 等
Stream API,如ReadableStream, WritableStream, TransformStream 等 Geolocation API,如Geolocation, Position, Coordinates 等
Crypto API,如crypto, CryptoKey, SubtleCrypto 等 Sensor API,如Sensor, Accelerometer, Gyroscope 等
Performance API,如 performance,PerformanceEntry, PerformanceObserver 等 Alert , Confirm 等
Console API,如console, Console 等
Timer API,如setTimeout, setInterval, clearTimeout, clearInterval 等
Navigator

在vue项目中,我们通常使用 axios 作为网络调用组件,它是一个基于 Promise 的 HTTP 库,可以用来发送请求和接收响应,由于它使用了 Fetch API 和 XMLHttpRequest API,所以它在工作线程中是支持的,但是需要注意以下几点:

  • 在工作线程中使用 axios 时,需要先使用 importScripts() 函数导入 axios 的脚本文件,例如:

importScripts("https://unpkg.com/axios/dist/axios.min.js");
  • 在工作线程中使用 axios 时,不能使用拦截器、取消请求、进度条等功能,因为它们依赖于 window 对象或 DOM API,而这些在工作线程中是不可用的。

  • 在工作线程中使用 axios 时,需要注意跨域请求的问题,因为工作线程的 origin 是不同于主线程的,所以可能会遇到 CORS 的错误。为了解决这个问题,可以在请求头中设置 Origin 为主线程的 origin,或者在服务器端配置允许工作线程的 origin 访问。

如果你想判断一个 API 是否在工作线程中支持,你可以使用 typeof 操作符来检查它是否存在,比如:if (typeof document === "undefined") 或 if (typeof fetch === "function")



往期文章推荐

计算机中的取模和补码运算到底是什么?

Mysql 索引加强版图解教程!为什么使用B+Tree的存储结构,看完这篇不懂都难。

面试官:来说说mysql索引的基本原理

太强了!nginx日志分析-生产环境实战!代码亲测有效

一款强大的nginx日志分析工具-goaccess分析

springboot+redisson分布式锁+定时job实现高性能、稳定的竞拍系统

nginx如何正确的配置才能获取到客户端的真实IP?多种场景说明清楚。

SpringBoot集成Guava RateLimiter 实现限流+源码解析

原文始发于微信公众号(小核桃编程):前端多线程web worker详细教程,请保存~

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/216072.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!