Node.js中的单线程服务器

为了解决多线程服务器在高并发的I/O密集型应用中的不足,同时避免早期简单单线程服务器的性能障碍,Node.js采用了基于"事件循环"的非阻塞式单线程模型,实现了如下两个目标:
(1)保证每个请求都可以快速响应;
(2)实现远超过多线程模型的并发连接数。

提示:Node.js在JS层面是单线程的——没有创建新线程的机制。但是在底层的C/C++层面是多线程的,即访问底层操作系统服务时,存在多个并发工作线程的情形——使用了线程池

阻塞执行

阻塞(Block):也称为同步执行(Synchronize),只有前面的操作全部执行完成,才能开始后续操作。(常规的多线程服务器内部都是采用的是阻塞执行)

var conn=mysql.createConnection(...);	//步骤1
var result=conn,query('INSERT...');		//步骤2
conn.end();								//步骤3

非阻塞执行

非阻塞(Non—block):也称为异步执行(Asynchronize),后面的操作不必等待之前操作的执行完毕,可以先执行。

const fs=require('fs');
console.log('读取请求数据');					//操作1
fs.writeFile('app.log','访问日志',()=>{     //操作2console.log('写出操作日志');
})
console.log('输出响应数据');					//操作3
//读取请求数据
//输出响应数据
//写出操作日志

异步回调

Node.js中的业务代码,都是在单一的主线程中执行的;当遇到耗时的阻塞操作时(如文件IO、网络访问、数据库请求等),不会等待其执行完毕,而是注册一个处理函数执行结果的回调函数,继续执行后续的代码。

待耗时的阻塞操作执行完成时,其对应的回调函数会转入回调函数队列,主线程在下次事件循环时会执行这些回调函数。

同步函数调用

const fs=require('fs');
var data=fs.readFileSync('app.log');
console.log('文件内容:',data);
console.log('程序执行完成!');
//文件内容:121212
//程序执行完成!

异步函数调用

const fs=require('fs');
fs.readFile('app.log',function(err,data){console.log('文件内容:',data)
})
console.log('程序执行完成!');
//程序执行完成!
//文件内容:121212

事件驱动

事件驱动编程是一种以事件为基础的编程范式,其中程序的执行是由事件的发生和相应的事件处理程序来驱动的。让我们详细了解事件循环和异步编程,并结合示例说明。

事件循环

事件循环是事件驱动编程的核心机制之一,它负责监听和分发事件,并调用相应的事件处理程序来响应事件。事件循环在一个持续运行的循环中不断地从事件队列中获取事件,并按照事件的顺序依次处理。常见的事件循环模型是基于单线程的,每次只处理一个事件,避免了并发访问的问题。

示例

const eventEmitter = require('events');// 创建事件发射器
const emitter = new eventEmitter();// 监听事件
emitter.on('event', (data) => {console.log('Event received:', data);
});// 触发事件
emitter.emit('event', 'Hello, world!');

在这个示例中,我们创建了一个事件发射器,并使用on方法来监听一个名为"event"的事件。当"event"事件被触发时,事件处理程序会被调用,并将事件数据传递给它。最后,我们使用emit方法触发了"event"事件,并传递了数据"Hello, world!"。这样,事件处理程序会被调用,并输出"Event received: Hello, world!"。

异步编程

异步编程是事件驱动编程中的重要概念,它允许程序在执行耗时操作时不阻塞主线程,而是通过回调函数、Promise、async/await等机制来处理操作的结果。这样可以提高程序的并发性能,避免了长时间的等待。

libuv

利用编译好的libuv库文件,我们可以开始写一个简单又经典的例子: Hello world。

#include "stdio.h"
#include "uv.h"int main() {uv_loop_t *loop = uv_default_loop();printf("hello libuv");uv_run(loop, UV_RUN_DEFAULT);
}

event-loop线程

我们都知道线程是操作系统最基本的调度单元,而进程是操作系统的最基本的资源分配单元,因此可以知道进程其实是不能运行,能运行的是进程中的线程。进程仅仅是一个容器,包含了线程运行中所需要的数据结构等信息。一个进程创建时,操作系统会创建一个线程,这就是主线程,而其他的从线程,都要在主线程的代码来创建,也就是由程序员来创建。因此每一个可执行的运用程序都至少有一个线程

于是libuv一开始便启动了event-loop线程,再在这个主线程上利用线程池去创建更多的线程。在event-loop线程中是一段while(1)的死循环代码,直到没有活跃的句柄的时候才会退出,这个时候libuv进程才被销毁掉。清楚这点对于后面的学习至关重要。

Handle

中文翻译为句柄,如[译文]libuv设计思想概述一文所属,整个libuv的实现都是基于Handle和Request。所以理解句柄以及libuv提供的所有句柄实例才能够真的掌握libuv。按照原文所述,句柄是:

表示能够在活动时执行某些操作的长生命周期对象。

理解这句话的意思,首先我们抓住两个关键词:长生命周期、对象。Libuv所有的句柄都需要初始化,而初始化都会调用类似这种函数:uv_xxx_initxxx表示句柄的类型,在该函数中,会将传入的形参handle初始化,并赋值返回具体的对象,比如初始化tcp句柄:

... // 随便截取一段初始化代码
handle->tcp.serv.accept_reqs = NULL;
handle->tcp.serv.pending_accepts = NULL;
handle->socket = INVALID_SOCKET;
handle->reqs_pending = 0;
handle->tcp.serv.func_acceptex = NULL;
handle->tcp.conn.func_connectex = NULL;
handle->tcp.serv.processed_accepts = 0;
handle->delayed_error = 0
...

理解了句柄其实就是个对象,那么长生命周期要是怎样的?还是以TCP句柄为例子,你在这个例子tcpserver.c中,可以看到后面tcp服务器的操作:绑定端口、监听端口都是基于tcp句柄,整个句柄存活于整个应用程序,只要tcp服务器没有挂掉就一直在,因此说是长生命周期的对象。

libuv提供的所有句柄如下:

接下去我们简单介绍以下所有的Libuv的句柄

uv_handle_t

首先libuv有一个基本的handle, uv_handle_t,libuv是所有其他handle的基本范式,任何handle都可以强转为该类型,并且和该Handle相关的所有API都可以为其他handle使用。

libuv能否一直运行下去的前提是检查是否有活跃的句柄存在,而检查一个句柄是否活跃(可以使用方法uv_is_active(const uv_handle_t* handle)检查),根据句柄类型不同,其含义也不一样:

  1. uv_async_t句柄总是活跃的并且不能停用,除非使用uv_close关闭掉
  2. uv_pipe_tuv_tcp_t, uv_udp_t等,这些牵扯到I/O的句柄一般也都是活跃
  3. uv_check_t, uv_idle_t, uv_timer_t等,当这些句柄开始调用uv_check_start(), uv_idle_start()的时候也是活跃的。

而检查哪些句柄活跃则可以使用这个方法:uv_print_active_handles(handle->loop, stderr);

以tcpserver.c为例子,我们启动tcp服务器后,启动一个定时器去打印存在的句柄,结果如下:

[-AI] async    0x10f78e9d8
[RA-] tcp      0x10f78e660
[RA-] timer    0x7ffee049d7c0

可以看到tcp的例子中一直存活的句柄是async、tcp、timer。它们前面中括号的标志解释如下:

R 表示该句柄被引用着
A 表示该句柄此时处于活跃状态
I 表示该句柄是内部使用的

uv_timer_t

顾名思义,Libuv的计时器,用来在将来某个时候调用对应设置的回调函数。其调用时机是在整个轮询的最最开始,后面我们会说到轮询的整个步骤。

uv_idle_t

Idle句柄在每次循环迭代中运行一次给定的回调,而且执行顺序是在prepare句柄之前。

prepare句柄的显著区别在于,当存在活动的空闲句柄时,循环将执行零超时轮询,而不是阻塞I/O。

uv_backend_timeout方法中我们可以看到返回的轮询I/O超时时间是0:

if (!QUEUE_EMPTY(&loop->idle_handles))return 0;

idle句柄的回调一般用来执行一些低优先级的任务。

**注意:尽管名称叫做“idle”,空闲句柄在每次循环迭代时都会调用它们的回调函数,
而不是在循环实际上是“空闲”的时候。**

uv_prepare_t 

prepare句柄将在每次循环迭代中运行一次给定的回调,而且是选择在I/O轮询之前。

问题是:libuv为什么要创造这么一种句柄?其实从名称来猜测,libuv应该是想提供一种方式让你可以在轮询I/O之前做些事情,然后在轮询I/O之后使用check句柄进行一些结果的校验。

uv_check_t

check句柄将在每次循环迭代中运行一次给定的回调,而且是选择在I/O轮询之后。其目的在上面已经提过

uv_async_t

Async句柄允许用户“唤醒”事件循环,并在主线程(原文翻译为another thread,其实不对)调用一开始注册的回调。这里说的唤醒其实就是发送消息给主线程(event-loop线程),让其可以执行一开始注册的回调了。

**注意:libuv会对`uv_async_send()`做一个聚合处理。
也就是说它并不会调用一次就执行一次回调。**

我们使用thread.c为例子,使用uv_queue_workuv_async_send来实践,得到的结果打印如下:

// 打印出主进程ID号和event-loop线程ID
I am the master process, processId => 90714
I am event loop thread => 0x7fff8c2d9380// 这个是uv_queue_work执行的回调,
从线程ID可以看到回调函数是在线程池中的某个线程中执行
I am work callback, calling in some thread in thread pool, pid=>90714
work_cb thread id 0x700001266000// 这个是uv_queue_work执行完回调后结束的回调,
从线程ID可以看到这个回调已经回到了主线程中执行
I am after work callback, calling from event loop thread, pid=>90714
after_work_cb thread id 0x7fff8c2d9380// 这个是uv_async_init的回调,其触发是因为在work callback中执行了uv_async_send,
可以从0x700001266000得到验证,该回调也是在主线程中执行
I am async callback, calling from event loop thread, pid=>90714
async_cb thread id 0x7fff8c2d9380
I am receiving msg: This msg from another thread: 0x700001266000

uv_poll_t

Poll句柄用于监视文件描述符的可读性、可写性和断开连接,类似于poll(2)的目的。

Poll句柄的目的是支持集成外部库,这些库依赖于事件循环来通知套接字状态的更改,比如c-areslibssh2。不建议将uv_poll_t用于任何其他目的;因为像uv_tcp_tuv_udp_t等提供了一个比uv_poll_t更快、更可伸缩的实现,尤其是在Windows上。

可能轮询处理偶尔会发出信号,表明文件描述符是可读或可写的,即使它不是。因此,当用户试图从fd读取或写入时,应该总是准备再次处理EAGAIN错误或类似的EAGAIN错误。

同一个套接字不能有多个活跃的Poll句柄,因为这可能会导致libuv出现busyloop或其他故障。

当活跃的Poll句柄轮询文件描述符时,用户不应关闭该文件描述符。否则可能导致句柄报告错误,但也可能开始轮询另一个套接字。但是,可以在调用uv_poll_stop()uv_close()之后立即安全地关闭fd。

下面罗列的是轮询的事件类型:

enum uv_poll_event {UV_READABLE = 1,UV_WRITABLE = 2,UV_DISCONNECT = 4,UV_PRIORITIZED = 8
};

uv_signal_t

Signal句柄在每个事件循环的基础上实现Unix风格的信号处理。在udpserver.c中展示了Signal句柄的使用方式:

uv_signal_t signal_handle;
r = uv_signal_init(loop, &signal_handle);
CHECK(r, "uv_signal_init");r = uv_signal_start(&signal_handle, signal_cb, SIGINT);void signal_cb(uv_signal_t *handle, int signum) {printf("signal_cb: recvd CTRL+C shutting down\n");uv_stop(uv_default_loop()); //stops the event loop
}

关于Signal句柄有几个点要知悉:

  1. 以编程方式调用raise()abort()触发的信号不会被libuv检测到;所以这些信号不会对应的回调函数。
  2. SIGKILL和SIGSTOP是不可能被捕捉到的
  3. 通过libuv处理SIGBUS、SIGFPE、SIGILL或SIGSEGV会导致未定义的行为

uv_process_t

process句柄将会新建一个新的进程并且能够允许用户控制该进程并使用流去建立通信通道。对应的demo可以查看:process.c,值得注意的是,args中提供的结构体的第一个参数path指的是可执行程序的路径,比如在demo中:

const char* exepath = exepath_for_process();
char *args[3] = { (char*) exepath, NULL, NULL };

实例中的exepath是:FsHandle的执行路径。

另外一个注意点就是父子进程的std的配置,demo中提供了一些参考,如果使用管道的话还可以参考另外一个demo:pipe

 uv_stream_t

流句柄提供了双工通信通道的抽象。uv_stream_t是一种抽象类型,libuv以uv_tcp_tuv_pipe_tuv_tty_t的形式提供了3种流实现。这个没有具体实例。但是libuv有好几个方法的入参都是uv_stream_t,说明这些方法都是可以被tcp/pipe/tty使用,具体有:

int uv_shutdown(uv_shutdown_t* req, uv_stream_t* handle, uv_shutdown_cb cb)
int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb)
int uv_accept(uv_stream_t* server, uv_stream_t* client)
int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb)
int uv_read_stop(uv_stream_t*)
int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb)
int uv_write2(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbu

uv_tcp_t

tcp句柄可以用来表示TCP流和服务器。上小节说到的uv_stream_tuv_tcp_t的”父类“,这里使用结构体继承的方式实现,uv_handle_tuv_stream_tuv_tcp_t三者的结构关系如下图:

使用libuv创建tcp服务器的步骤可以归纳为:

1、初始化uv_tcp_t: uv_tcp_init(loop, &tcp_server)
2、绑定地址:uv_tcp_bind
3、监听连接:uv_listen
4、每当有一个连接进来之后,调用uv_listen的回调,回调里要做如下事情:4.1、初始化客户端的tcp句柄:uv_tcp_init()4.2、接收该客户端的连接:uv_accept()4.3、开始读取客户端请求的数据:uv_read_start()4.4、读取结束之后做对应操作,如果需要响应客户端数据,调用uv_write,回写数据即可。

uv_pipe_t

 

Pipe句柄在Unix上提供了对本地域套接字的抽象,在Windows上提供了命名管道。它是uv_stream_t的“子类”。管道的用途很多,可以用来读写文件,还可以用来做线程间的通信。我们在实例中用来实现主线程与多个子线程的互相通信。实现的模型是这样的:

uv_udp_t

UDP句柄为客户端和服务器封装UDP通信。使用libuv创建udp服务器的步骤可以概括为:

1、初始化接收端的uv_udp_t: uv_udp_init(loop, &receive_socket_handle)
2、绑定地址:uv_udp_bind
3、开始接收消息:uv_udp_recv_start
4、uv_udp_recv_start里执行回调,可以使用下面方法回写数据发送给客户端4.1、uv_udp_init初始化send_socket_handle4.2、uv_udp_bind绑定发送者的地址,地址可以从recv获取4.3、uv_udp_send发送指定消息

uv_fs_event_t

FS事件句柄允许用户监视一个给定的路径的更新事件,例如,如果文件被重命名或其中有一个通用更改。这个句柄使用每个平台上最佳的解决方案。

 

uv_fs_poll_t

FS轮询句柄允许用户监视给定的更改路径。与uv_fs_event_t不同,fs poll句柄使用stat检测文件何时发生了更改,这样它们就可以在不支持fs事件句柄的文件系统上工作。

Request

那么接下去就说到Request这个短生命周期的概念,中文翻译为”请求“,类似于nodejs中的req,它也是一个结构体。还是以上述的tcp服务器为例子,有这么一段代码:

  if (r < 0) {// 如果接受连接失败,需要清理一些东西uv_shutdown_t *shutdown_req = malloc(sizeof(uv_shutdown_t));r = uv_shutdown(shutdown_req, (uv_stream_t *)tcp_client_handle, shutdown_cb);CHECK(r, "uv_shutdown");}

当客户端连接失败,需要关闭掉这个连接,于是我们就会初始化一个request,然后传递给我们需要请求的操作,这里是关闭请求shutdown

 uv_request_t是基本的request,其他任何request都是基于该结构进行扩展,它定义的所有api其他request都可以使用。和uv_handle_t一样的功效。

libuv运行的三种模式

接着说说Libuv提供的三种运行模式:

  • UV_RUN_DEFAULT 默认轮询模式,此模式会一直运行事件循环直到没有活跃句柄、引用句柄、和请求句柄
  • UV_RUN_ONCE 一次轮询模式,此模式如果pending_queue中有回调,则会执行回调而直接跨过uv__io_poll。如果没有,则此方式只会执行一次I/O轮询(uv__io_poll)。如果在执行过后有回调压入到了pending_queue中,则uv_run会返回非0,你需要在未来的某个时间再次触发一次uv_run来清空pending_queue。
  • UV_RUN_NOWAIT 一次轮询(无视pending_queue)模式,此模式类似UV_RUN_ONCE但是不会判断pending_queue是否存在回调,直接进行一次I/O轮询。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/173259.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通过el-tree 懒加载树,创建国家地区四级树

全国四级行政地区树数据库sql下载路径&#xff1a;【免费】全国四级地区(省市县)数据表sql资源-CSDN文库https://download.csdn.net/download/weixin_51722520/88469807?spm1001.2014.3001.5503 我在后台获取地区信息添加了限制&#xff0c;只获取parentid为当前的地…

[AUTOSAR][诊断管理][ECU][$34] 下载请求

文章目录 一、简介二、服务请求报文定义肯定响应支持的NRC三、示例代码34_req_dowload.c一、简介 RequestDownload(0x34)—— 下载请求 这个服务主要是用来给ECU下载数据的,最常见的应用就是在bootloader中,程序下载工具会发起下载请求,以完成ECU程序的升级。 二、服务…

计算线阵相机 到 拍摄产品之间 摆放距离?(隐含条件:保证图像不变形)

一物体被放置在传送带上&#xff0c;转轴的直径为100mm。已知线阵相机4K7u&#xff08;一行共4096个像素单元&#xff0c;像素单元大小7um&#xff09;&#xff0c;镜头35mm&#xff0c;编码器2000脉冲/圈。保证图像不变形的条件下&#xff0c;计算相机到产品之间 摆放距离&…

国际阿里云CDN加速OSS资源教程!

当您需要加速OSS上的静态资源时&#xff0c;可以通过阿里云CDN加速OSS域名&#xff0c;实现静态资源的访问加速。本文详细介绍了通过CDN控制台实现OSS加速的操作流程和应用场景。 客户价值 阿里云OSS可提供低成本的存储&#xff0c;CDN可以实现静态资源加速分发。使用OSS作为C…

Linux学习第24天:Linux 阻塞和非阻塞 IO 实验(一): 挂起

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 在正式开始今天的笔记之前谈一下工作中遇见的一个问题。 本篇笔记主要学习Linux 阻塞和非阻塞 IO 实验&#xff0c;主要包括阻塞和非阻塞简介、等待队列、轮询、…

05.大模型大数据量

文章目录 大模型顿悟时刻&#xff1a;Emergent Ability&#xff08;涌动现象&#xff09;Calibration Inverse Scaling PrizeSwitch Transformers 大数据量数据预处理去重 模型大小与训练数据的选择Instruction-tuningHuman TeachingKNN LM 部分截图来自原课程视频《2023李宏毅…

NewStarCTF2023week4-Nmap

题目要我们找出Nmap扫描得到所有的开放端口 Nmap通常用于直接扫描目标主机&#xff0c;而不是直接扫描pcap文件。 那么这里我们还是使用wireshark来分析&#xff0c;使用过滤器&#xff1a; tcp.flags.syn 1 and tcp.flags.ack 1 这个过滤条件可以筛选出TCP端口开放的数据…

Games104现代游戏引擎笔记 网络游戏架构基础

挑战1:网络同步 挑战2:是网络的可靠性&#xff0c;包括应对网络的延迟&#xff0c;丢包和掉线 挑战3: 反作弊和安全系统&#xff0c;因为网络游戏的本质是经济系统 挑战4:多样性(不同设备&#xff0c;不同服务器)&#xff0c;在不停服的情况下热更新 挑战5:大量人数时对高并发…

MySQL(1):开始

概述 DB&#xff1a;数据库&#xff08;Database&#xff09; 即存储数据的“仓库”&#xff0c;其本质是一个文件系统。它保存了一系列有组织的数据。 DBMS&#xff1a;数据库管理系统&#xff08;Database Management System&#xff09; 是一种操纵和管理数据库的大型软件…

目标检测算法改进系列之嵌入Deformable ConvNets v2 (DCNv2)

Deformable ConvNets v2 简介&#xff1a;由于构造卷积神经网络所用的模块中几何结构是固定的&#xff0c;其几何变换建模的能力本质上是有限的。在DCN v1中引入了两种新的模块来提高卷积神经网络对变换的建模能力&#xff0c;即可变形卷积 (deformable convolution) 和可变形…

【微信小程序开发】学习小程序的网络请求和数据处理

前言 网络请求是微信小程序中获取数据和与服务器交互的重要方式。微信小程序提供了自己的API来处理网络请求&#xff0c;使得开发者可以轻松地在微信小程序中实现数据的获取和提交。本文将介绍微信小程序中的网络请求&#xff0c;包括使用wx.request发起GET和POST请求&#xf…

Kotlin协程核心理解

一、协程是什么&#xff1f; 1.1 基本概念的理解 我们知道JVM中的线程的实现是依赖其运行的操作系统决定的&#xff0c;JVM只是在上层进行了API的封装&#xff0c;包含常见的有线程的启动方法&#xff0c;状态的管理&#xff0c;比如&#xff1a;Java中抽象出了6种状态&#x…

数据结构与算法之矩阵: Leetcode 134. 螺旋矩阵 (Typescript版)

螺旋矩阵 https://leetcode.cn/problems/spiral-matrix/ 描述 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示…

人工智能(AI)在医疗领域的应用

人工智能&#xff08;AI&#xff09;在医疗领域的应用 人工智能&#xff08;AI&#xff09;在医疗领域的应用近年来得到了广泛的关注。其中&#xff0c;AI辅助治疗疾病的技术成为了研究热点。本文将介绍AI辅助治疗疾病的技术&#xff0c;包括其定义、应用场景、案例分析和发展…

3.线性神经网络

#pic_center R 1 R_1 R1​ R 2 R^2 R2 目录 知识框架No.1 线性回归基础优化算法一、线性回归1、买房案例2、买房模型简化3、线性模型4、神经网络5、损失函数6、训练数据7、参数学习8、显示解9、总结 二、 基础优化算法1、梯度下降2、学习率3、小批量随机梯度下降4、批量大小5、…

1024 云上见 · 上云挑战(ChatGPT搭建)

【玩转1024】使用函数计算X通义千问搭建AI助手&#xff0c;参与1024小说创作大赛 【使用函数计算X通义千问搭建AI助手&#xff0c;参与小说创作大赛】&#xff1a;本活动基于函数计算X 通义千问快速部署 AI 个人助手应用&#xff0c;用户可以根据需要选择不同角色的AI助手开启…

基于引力搜索算法的无人机航迹规划-附代码

基于引力搜索算法的无人机航迹规划 文章目录 基于引力搜索算法的无人机航迹规划1.引力搜索搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用引力搜索算法来优化无人机航迹规划。 …

Android12之#pragma clang diagnostic ignored总结(一百六十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

【网络安全 --- 文件上传靶场练习】文件上传靶场安装以及1-5关闯关思路及技巧,源码分析

一&#xff0c;前期准备环境和工具 1&#xff0c;vmware 16.0安装 若已安装&#xff0c;请忽略 【网络安全 --- 工具安装】VMware 16.0 详细安装过程&#xff08;提供资源&#xff09;-CSDN博客文章浏览阅读186次&#xff0c;点赞9次&#xff0c;收藏2次。【网络安全 --- 工…

生活日用品经营小程序商城的作用是什么

生活日用品如牙膏牙刷、护手霜等小产品是人们生活所需&#xff0c;传统客户会通过线下超市商场购买&#xff0c;但现在的消费者往往会选择线上渠道购买&#xff0c;省时省力坐在家里等货上门即可。 因此对商家来说&#xff0c;需要打通线上渠道才能获得更多生意&#xff0c;提…