聊聊 Node.js 的底层原理
liuian 2025-03-13 17:16 33 浏览
作者: theanarkh 来源:编程杂技
之前分享了 Node.js 的底层原理,主要是简单介绍了 Node.js 的一些基础原理和一些核心模块的实现,本文从 Node.js 整体方面介绍 Node.js 的底层原理。
内容主要包括五个部分。第一部分是首先介绍一下 Node.js 的组成和代码架构。然后介绍一下 Node.js 中的 Libuv, 还有 V8 和模块加载器。最后介绍一下 Node.js 的服务器架构。
1 Node.js 的组成和代码架构
下面先来看一下Node.js 的组成。Node.js 主要是由 V8、Libuv 和一些第三方库组成。
1). V8 我们都比较熟悉,它是一个 JS 引擎。但是它不仅实现了 JS 解析和执行,它还是自定义拓展。比如说我们可以通过 V8 提供一些 C++ API 去定义一些全局变量,这样话我们在 JS 里面去使用这个变量了。正是因为 V8 支持这个自定义的拓展,所以才有了 Node.js 等 JS 运行时。
2). Libuv 是一个跨平台的异步 IO 库。它主要的功能是它封装了各个操作系统的一些 API, 提供网络还有文件进程的这些功能。我们知道在 JS 里面是没有网络文件这些功能的,在前端时,是由浏览器提供的,而在 Node.js 里,这些功能是由 Libuv 提供的。
3). 另外 Node.js 里面还引用了很多第三方库,比如 DNS 解析库,还有 HTTP 解析器等等。
接下来看一下 Node.js 代码整体的架构。
Node.js 代码主要是分为三个部分,分别是C、C++ 和 JS。
1. JS 代码就是我们平时在使用的那些 JS 的模块,比方说像 http 和 fs 这些模块。
2. C++ 代码主要分为三个部分,第一部分主要是封装 Libuv 和第三方库的 C++ 代码,比如net 和 fs 这些模块都会对应一个 C++ 模块,它主要是对底层的一些封装。第二部分是不依赖 Libuv 和第三方库的 C++ 代码,比方像 Buffer 模块的实现。第三部分 C++ 代码是 V8 本身的代码。
3. C 语言代码主要是包括 Libuv 和第三方库的代码,它们都是纯 C 语言实现的代码。
了解了 Nodejs 的组成和代码架构之后,再来看一下 Node.js 中各个主要部分的实现。
2 Node.js 中的 Libuv
首先来看一下 Node.js 中的 Libuv,下面从三个方面介绍 Libuv。
1). 介绍 Libuv 的模型和限制
2). 介绍线程池解决的问题和带来的问题
3). 介绍事件循环
2.1 Libuv 的模型和限制
Libuv 本质上是一个生产者消费者的模型。
从上面这个图中,我们可以看到在 Libuv 中有很多种生产任务的方式,比如说在一个回调里,在 Node.js 初始化的时候,或者在线程池完成一些操作的时候,这些方式都可以生产任务。然后 Libuv 会不断的去消费这些任务,从而驱动着整个进程的运行,这就是我们一直说的事件循环。
但是生产者的消费者模型存在一个问题,就是消费者和生产者之间,怎么去同步?比如说在没有任务消费的时候,这个消费者他应该在干嘛?第一种方式是消费者可以睡眠一段时间,睡醒之后,他会去判断有没有任务需要消费,如果有的话就继续消费,如果没有的话他就继续睡眠。很显然这种方式其实是比较低效的。第二种方式是消费者会把自己挂起,也就是说这个消费所在的进程会被挂起,然后等到有任务的时候,操作系统就会唤醒它,相对来说,这种方式是更高效的,Libuv 里也正是使用这种方式。
这个逻辑主要是由事件驱动模块实现的,下面看一下事件驱动的大致的流程。
应用层代码可以通过事件驱动模块订阅 fd 的事件,如果这个事件还没有准备好的话,那么这个进程就会被挂起。然后等到这个 fd 所对应的事件触发了之后,就会通过事件驱动模块回调应用层的代码。
下面以 Linux 的 事件驱动模块 epoll 为例,来看一下使用流程。
1. 首先通过 epoll_create 去创建一个epoll 实例。
2. 然后通过 epoll_ctl 这个函数订阅、修改或者取消订阅一个 fd 的一些事件。
3. 最后通过 epoll_wait 去判断当前订阅的事件有没有发生,如果有事情要发生的话,那么就直接执行上层回调,如果没有事件发生的话,这种时候可以选择不阻塞,定时阻塞或者一直阻塞,直到有事件发生。要不要阻塞或者说阻塞多久,是根据当前系统的情况。比如 Node.js 里面如果有定时器的节点的话,那么 Node.js 就会定时阻塞,这样就可以保证定时器可以按时执行。
接下来再深入一点去看一下 epoll 的大致的实现。
当应用层代码调用事件驱动模块订阅 fd 的事件时,比如说这里是订阅一个可读事件。那么事件驱动模块它就会往这个 fd 的队列里面注册一个回调,如果当前这个事件还没有触发,这个进程它就会被阻塞。等到有一块数据写入了这个 fd 时,也就是说这个 fd 有可读事件了,操作系统就会执行事件驱动模块的回调,事件驱动模块就会相应的执行用层代码的回调。
但是 epoll 存在一些限制。首先第一个是不支持文件操作的,比方说文件读写这些,因为操作系统没有实现。第二个是不适合执行耗时操作,比如大量 CPU 计算、引起进程阻塞的任务,因为 epoll 通常是搭配单线程的,如果在单线程里执行耗时任务,就会导致后面的任务无法执行。
2.2 线程池解决的问题和带来的问题
针对这个问题,Libuv 提供的解决方案就是使用线程池。下面来看一下引入了线程池之后, 线程池和主线程的关系。
从这个图中我们可以看到,当应用层提交任务时,比方说像 CPU 计算还有文件操作,这种时候不是交给主线程去处理的,而是直接交给线程池处理的。线程池处理完之后它会通知主线程。
但是引入了多线程后会带来一个问题,就是怎么去保证上层代码跑在单个线程里面。因为我们知道 JS 它是单线程的,如果线程池处理完一个任务之后,直接执行上层回调,那么上层代码就会完全乱了。这种时候就需要一个异步通知的机制,也就是说当一个线程它处理完任务的时候,它不是直接去执行上程回调的,而是通过异步机制去通知主线程来执行这个回调。
Libuv 中具体通过 fd 的方式去实现的。当线程池完成任务时,它会以原子的方式去修改这个 fd 为可读的,然后在主线程事件循环的 Poll IO 阶段时,它就会执行这个可读事件的回调,从而执行上层的回调。可以看到,Node.js 虽然是跑在多线程上面的,但是所有的 JS 代码都是跑在单个线程里的,这也是我们经常讨论的 Node.js 是单线程还是多线程的,从不同的角度去看就会得到不同的答案。
下面的图就是异步任务处理的一个大致过程。
比如我们想读一个文件的时候,这时候主线程会把这个任务直接提交到线程池里面去处理,然后主线程就可以继续去做自己的事情了。当在线程池里面的线程完成这个任务之后,它就会往这个主线程的队列里面插入一个节点,然后主线程在 Poll IO 阶段时,它就会去执行这个节点里面的回调。
2.3 事件循环
了解 Libuv 的一些核心实现之后,下面我们再看一下 Libuv 中一个著名的事件循环。事件循环主要分为七个阶段,
1. 第一是 timer 阶段,timer 阶段是处理定时器相关的一些任务,比如 Node.js 中的 setTimeout和 setInterval。
2. 第二是 pending 的阶段, pending 阶段主要处理 Poll IO 阶段执行回调时产生的回调。
3. 第三是 check、prepare 和 idle 三个阶段,这三个阶段主要处理一些自定义的任务。setImmediate 属于 check 阶段。
4. 第四是 Poll IO 阶段,Poll IO 阶段主要要处理跟文件描述符相关的一些事件。5. 第五是 close 阶段, 它主要是处理,调用了 uv_close 时传入的回调。比如关闭一个 TCP 连接时传入的回调,它就会在这个阶段被执行。
下面这个图是各个阶段在事件循环的顺序图。
下面我们来看一下每个阶段的实现。
1. 定时器
Libuv 在底层里面维护了一个最小堆,每个定时节点就是堆里面的一个节点(Node.js 只用了 Libuv 的一个定时器节点),越早超时的节点就在越上面。然后等到定时期阶段的时候, Libuv 就会从上往下去遍历这个最小堆判断当前节点有没有超时,如果没有到期的话,那么后面节点也不需要去判断了,因为最早到期的节点都没到期,那么它后面节点也显然不会到期。如果当前节点到期了,那么就会执行它的回调,并且把它移出这个最小堆。但是为了支持类似 setInterval 这种场景。如果这个节点设置了repeat 标记,那么这个节点它会被重新插入到最小堆中,等待下一次的超时。
2. check、idle、prepare 阶段和 pending、close 阶段。
这五个阶段的实现其实类似的,它们都对应自己的一个任务队列。当产生任务的时候,它就会往这个队列里面插入一个节点,等到相应的阶段时,它就会去遍历这个队列里面的每个节点,并且执行它的回调。但是 check idle 还有 prepare 阶段有一个比较特别的地方,就是当这些阶段的节点回调被执行之后,它还会重新插入队列里面,也是说这三个阶段它对应的任务在每一轮的事件循环都会被执行。
3. Poll IO 阶段 Poll IO 本质上是对前面讲的事件驱动模块的封装。下面来看一下整体的流程。
当我们订阅一个 fd 的事件时,Libuv 就会通过 epoll 去注册这个 fd 对应的事件。如果这时候事件没有就绪,那么进程就会阻塞在 epoll_wait 中。等到这事件触发的时候,进程就会被唤醒,唤醒之后,它就遍历 epoll 返回了事件列表,并执行上层回调。
现在有一个底层能力,那么这个底层能力是怎么暴露给上层的 JS 去使用呢?这种时候就需要用到 JS 引擎 V8了。
3. Node.js 中的 V8
下面从三个方面介绍 V8。
1. 介绍 V8 在 Node.js 的作用和 V8 的一些基础概念
2. 介绍如何通过 V8 执行 JS 和拓展 JS
3. 介绍如何通过 V8 实现 JS 和 C++ 通信
3.1 V8 在 Node.js 的作用和基础概念
V8 在 Node.js 里面主要是有两个作用,第一个是负责解析和执行 JS。第二个是支持拓展 JS 能力,作为这个 JS 和 C++ 的桥梁。下面我们先来看一下 V8 里面那些重要的概念。
1. Isolate:首先第一个是 Isolate 它是代表一个 V8 的实例,它相当于这一个容器。通常一个线程里面会有一个这样的实例。比如说在 Node.js主线程里面,它就会有一个 Isolate 实例。
2. Context:Context 是代表我们执行代码的一个上下文,它主要是保存像 Object,Function 这些我们平时经常会用到的内置的类型。如果我们想拓展 JS 功能,就可以通过这个对象实现。
3. ObjectTemplate:ObjectTemplate 是用于定义对象的模板,然后我们就可以基于这个模板去创建对象。
4. FunctionTemplate:FunctionTemplate 和 ObjectTemplate 是类似的,它主要是用于定义一个函数的模板,然后就可以基于这个函数模板去创建一个函数。
5. FunctionCallbackInfo:用于实现 JS 和 C++ 通信的对象。
6. Handle:Handle 是用管理在 V8 堆里面那些对象,因为像我们平时定义的对象和数组,它是存在 V8 堆内存里面的。Handle 就是用于管理这些对象。
7. HandleScope:HandleScope 是一个 Handle 容器,HandleScope 里面可以定义很多 Handle,它主要是利用自己的生命周期管理多个 Handle。
下面我们通过一个代码来看一下 HandleScope 和 Handle 它们之间的关系。
首先第一步新建一个 HandleScope,就会在一个栈里面定义一个 HandleScope 对象。然后第二步新建了一个 Handle 并且把它指向一个堆对象。这时候就会在栈里面分配一个叫 Local 对象,然后在堆里面分配一块 slot 所代表的内存和一个 Object 对象,并且建立关联关系。当执行完这个函数的时候,这个栈就会被清空,相应的这个 slot 代表的内存也会被释放,但是 Object 所代表这个对象,它是不会立马被释放的,它会等待 GC 的回收。
3.2 通过 V8 执行 JS 和拓展 JS
了解了 V8 的基础概念之后,来看一下怎么通过 V8 执行一段 JS 的代码。
首先第一步新建一个 Isolate,它这表示一个隔离的实例。第二步定义一个 HandleScope 对象,因为我们下面需要定义 Handle。第三步定义一个 Context,这是代码执行所在的上下文。第四步定义一些需要被执行的 JS 代码。第五步通过 Script 对象的 Compile 函数编译 JS 代码。编译完之后,我们会得到一个 Script 对象,然后执行这个对象的 Run 函数就可以完成代码的执行。
接下来再看一下怎么去拓展 JS 原有的一些能力。
首先第一步是通过 Context 上下文对象拿到一个全局的对象,类似于在前端里面的 window 对象。第二步通过 ObjectTemplate 新建一个对象的模板,然后接着会给这个对象模板设置一个 test 属性, 值是函数。接着通过这个对象模板新建一个对象,并且把这个对象设置到一个全局变量里面去。这样我们就可以在 JS 层去访问这个全局对象。
下面我们通过使用刚才定义那个全局对象来看一下 JS 和 C++ 是怎么通信的。
3.3 通过 V8 实现 JS 和 C++ 层通信
当在 JS 层调用刚才定义 test 函数时,就会相应的执行 C++ 层的 test 函数。这个函数有一个入参是 FunctionCallbackInfo,在 C++ 中可以通过这个对象拿到 JS 传来一些参数,这样就完成了 JS 层到 C++ 层通信。经过一系列处理之后,还是可以通过这个对象给 JS 层设置需要返回给 JS 的内容,这样可以完成了 C++ 层到 JS 层的通信。
现在有了底层能力,有了这一层的接口,但是我们是怎么去加载后执行 JS 代码呢?这时候就需要模块加载器。
4 Node.js 中的模块加载器
Node.js 中有五种模块加载器。
1. JSON 模块加载器
2. 用户 JS 模块加载器
3. 原生 JS 模块加载器
4. 内置 C++ 模块加载器
5. Addon 模块加载器
现在来看下每种模块加载器。
4.1 JSON 模块加载器
JSON 模块加载器实现比较简单,Node.js 从硬盘里面把 JSON 文件读到内存里面去,然后通过 JSON.parse 函数进行解析,就可以拿到里面的数据。
4.2 用户 JS 模块
用户 JS 模块就是我们平时写的一些 JS 代码。当通过 require 函数加载一个用户 JS 模块时,Node.js 就会从硬盘读取这个模块的内容到内存中,然后通过 V8 提供了一个函数叫 CompileFunctionInContext 把读取的代码封装成一个函数,接着新建立一个 Module 对象。这个对象里面有两个属性叫 exports 和 require 函数,这两个对象就是我们平时在代码里面所使用的变量,接着会把这个对象作为函数的参数,并且执行这个函数,执行完这个函数的时候,就可以通过 module.exports 拿到这个函数(模块)里面导出的内容。这里需要注意的是这里的 require 函数是可以加载原生 JS 模块和用户模块的,所以我们平时在我们代码里面,可以通过require 加载我们自己写的模块,或者 Node.js 本身提供的 JS 模块。
4.3 原生 JS 模块
接下来看下原生 JS 模块加载器。原生JS 模块是 Node.js 本身提供了一些 JS 模块,比如经常使用的 http 和 fs。当通过 require 函数加载 http 这个模块的时候,Node.js 就会从内存里读取这个模块所对应内容。因为原生 JS 模块默认是打包进内存里面的,所以直接从内存里面读就可以了,不需要从硬盘里面去读。然后还是通过 V8 提供的 CompileFunctionInContext 这个函数把读取的代码封装成一个函数,接着新建一个 NativeModule 对象,同样这个对象里面也是有个 exports 属性,接着它会把这个对象传到这个函数里面去执行,执行完这函数之后,就可以通过 module.exports 拿到这个函数里面导出的内容。需要注意是这里传入的 require 函数是一个叫 NativeModuleRequire 函数,这个函数它就只能加载原生 JS 模块。另外这里还传了另外一个 internalBinding 函数,这个函数是用于加载 C++ 模块的,所以在原生 JS 模块里面,是可以加载 C++ 模块的。
4.4 C++ 模块
Node.js 在初始化的时候会注册 C++ 模块,并且形成一个 C++ 模块链表。当加载 C++ 模块时,Node.js 就通过模块名,从这个链表里面找到对应的节点,然后去执行它里面的钩子函数,执行完之后就可以拿到 C++ 模块导出的内容。
4.5 Addon 模块
接着再来看一下 Addon 模块, Addon 模块本质上是一个动态链接库。当通过 require 加载Addon 模块的时候,Node.js 会通过 dlopen 这个函数去加载这个动态链接库。下图是我们定义一个 Addon 模块时的一个标准格式。
它里面有一些 C语言宏,宏展开之后里面内容像下图所示。
里面主要定义了一个结构体和一个函数,这个函数会把这个结构体赋值给 Node.js 的一个全局变量,然后 Nodejs 它就可以通过全局变量拿到这个结构体,并且执行它里面的一个钩子函数,执行完之后就可以拿到它里面要导出的一些内容。
现在有了底层的能力,也有了这一次层的接口,也有了代码加载器。最后我们来看一下 Node.js 作为一个服务器的时候,它的架构是怎么样的?
5 Node.js 的服务器架构
下面从两个方面介绍 Node.js 的服务器架构
1. 介绍服务器处理 TCP 连接的模型
2. 介绍 Node.js 中的实现和存在的问题
5.1 处理 TCP 连接的模型
首先来看一下网络编程中怎么去创建一个 TCP 服务器。
int fd = socket(…); bind(fd, 监听地址); listen(fd); 首先建一个 socket, 然后把需要监听的地址绑定到这个 socket 中,最后通过 listen 函数启动服务器。启动服务器之后,那么怎么去处理 TCP 连接呢?
1). 串行处理(accept 和 handle 都会引起进程阻塞)
第一种处理方式是串行处理,串行方式就是在一个 while 循环里面,通过 accept 函数不断地摘取 TCP 连接,然后处理它。这种方式的缺点就是它每次只能处理一个连接,处理完一个连接之后,才能继续处理下一个连接。
2). 多进程/多线程
第二种方式是多进程或者多线程的方式。这种方式主要是利用多个进程或者线程同时处理多个连接。但这种模式它的缺点就是当流量非常大的时候,进程数或者线程数它会成为这种架构下面的一个瓶颈,因为我们不能无限的创建进程或者线程,像 Apache 还有 PHP 就是这种架构的。
3). 单进程单线程 + 事件驱动( Reactor & Proactor ) 第三种就是单线程 + 事件驱动的模式。这种模式下有两种类型,第一种叫 Reactor, 第二种叫 Proactor。Reactor 模式就是应用程序可以通过事件驱动模块注册 fd 的读写事件,然后事件触发的时候,它就会通过事件驱动模块回调上层的代码。
Proactor 模式就是应用程序可以通过事件驱动模块注册 fd 的读写完成事件,然后这个读写完成事件后就会通过事件驱动模块回调上层代码。
我们看到这两种模式的区别是,数据读写是由内核完成的,还是由应用程序完成的。很显然,通过内核去完成是更高效的,但是因为 Proactor 这种模式它兼容性还不是很好,所以目前用的还不算太多,主要目前主流的一些服务器,它用的都是 Reactor 模式。比方说像 Node.js、Redis 和 Nginx 这些服务器用的都是这种模式。
刚才提到 Node.js 是单进程单线程加事件驱动的架构。那么单线程的架构它怎么去利用多核呢?这种时候就需要用到多进程的这种模式了,每一个进程里面会包含一个Reactor 模式。但是引入多进程之后,它会带来一个问题,就是多进程之间它怎么去监听同一个端口。
5.2 Node.js 的实现和问题
下面来看下针对多进程监听同一个端口的一些解决方式。
1. 主进程监听端口并接收请求,轮询分发(轮询模式)
2. 子进程竞争接收请求(共享模式)
3. 子进程负载均衡处理连接(SO_REUSEPORT 模式)
第一种方式就是主进程去监听这个端口,并且接收连接。它接收连接之后,通过一定的算法(比如轮询)分发给各个子进程。这种模式。它的一个缺点就是当流量非常大的时候,这个主进程就会成为瓶颈,因为它可能都来不及接收或者分发这个连接给子进程去处理。
第二种就是主进程创建监听 socket, 然后子进程通过 fork 的方式继承这个监听的 socket, 当有一个连接到来的时候,操作系统就唤醒所有的子进程,所有子进程会以竞争的方式接收连接。这种模式,它的缺点主要是有两个,第一个就是负载均衡的问题,因为操作系统唤醒了所有的进程,可能会导致某一个进程一直在处理连接,其他其它进程都没机会处理连接。然后另外一个问题就是惊群的问题,因为操作系统唤起了所有的进程,但是只有一个进程它会处理这个连接,然后剩下进程就会被无效地唤醒。这种方式会造成一定的性能的损失。
第三种通过 SO_REUSEPORT 这个标记来解决刚才提到的两个问题。在这种模式下,每个子进程都会有一个独立的监听 socket 和连接队列。当有一个连接到来的时候,操作系统会把这个连接分发给某一个子进程并且唤醒它。这样就可以解决惊群的问题,因为它只会唤醒一个子进程。又因为操作系统分发这个连接的时候,内部是有一个负载均衡的算法。所以这样的话又可以解决负载均衡的问题。
接下来我们看一下 Node.js 中的实现。
1). 轮询模式。在这种模式下,主进程会 fork 多个子进程,然后每个子进程里面都会调用 listen 函数。但是 listen 函数不会监听一个端口,它会请求主进程监听这个端口,当有连接到来的时候,这个主进程就会接收这个连接,然后通过文件描述符的方式传给各个子进程去处理。
2). 共享模式 共享模式下,主进程同样还是会 fork 多个子进程,然后每个子进程里面还是会执行 listen 函数,但同样的这个 listen 函数不会监听一个端口,它会请求主进程创建一个 socket 并绑定到一个需要监听的地址,接着主进程会把这个 socket 通过文件描述符传递的方式传给多个子进程,这样就可以达到多个子进程同时监听同一个端口的效果。
通过刚才介绍,我们可以知道 Node.js 的服务器架构存在的问题。如果我们使用轮询模式,当流量比较大的时候,那么这个主进程就会成为系统瓶颈。如果我们使用共享模式,就会存在惊群和负载均衡的问题。不过在 Libuv 里面,可以通过设置 UV_TCP_SINGLE_ACCEPT 环境变量来一定程度缓解这个问题。当我们设置了这个环境变量。Libuv 在接收完一个连接的时候,它就会休眠一会,让其它进程也有接收连接的机会。
最后来总结一下,本文的内容。Node.js 里面通过 Libuv 解决了操作系统相关的问题。通过 V8 解决了执行 JS 和拓展 JS 功能的问题。通过模块加载器解决了代码加载还有组织的问题。通过多进程的服务器架构,使得 Node.js 可以利用多核,并且解决了多个进程监听同一个端口的问题。
下面是一些资料,有兴趣的同学也可以看一下。
1. 基于 epoll + V8 的JS 运行时 Just:
https://github.com/theanarkh/read-just-0.1.4-code
2. 基于 io_uring+ V8 的 JS 运行时 No.js:
https://github.com/theanarkh/No.js
3. 理解 Node.js 原理:
https://github.com/theanarkh/understand-nodejs
- 上一篇:关于NodeJS工作原理的五个误解
- 下一篇:node 中间层怎样做的请求合并转发
相关推荐
- 改了user的用户名后桌面没了
-
1、C:\用户\当前用户名\AppData\Local文件夹,然后将IconCache.db文件删除,然后重启电脑。这没什么好担心的,这个文件,电脑重启后会重新创建,这种做法被称作---重建图标缓存2...
- ibm(ibm体重指数)
-
是国际商业机器有限公司,简称IBM(IntenationalBusinessMachinesCopoation)。总公司在纽约州阿蒙克市。该公司创立时的主要业务为商用打字机,及后转为文字处理机,然后到...
- 电脑如何设置防火墙(电脑如何设置防火墙其它软件禁止联网)
-
电脑防火墙设置方法如下1、首先,我们打开我们的电脑,然后我们双击电脑桌面上的控制面板;2、进入控制面板之后,我们点击WindowsDefender防火墙;3、弹出的界面,我们点击启用或关闭Windo...
-
- through(through和by的区别)
-
区别by表示方法,手段。through表示以、通过、经由。在表示手段时,by,through有时也可换用by1、表示方法,手段。即“用...通过...相当于bymeansof如:Allworkhadtobedone...
-
2026-01-13 16:55 liuian
- bizhub15打印机驱动下载(bizhub打印机驱动安装)
-
1、请用USB数据线连接复印机和电脑。 2、打开电脑,然后到复印机的官网下载当前系统的驱动程序,然后点击安装。 3、安装完成后,点击打开打印机和传真,就可以到看扫描仪的图标。 4、找个要扫描的内...
- win7电脑截屏(windows7电脑截屏)
-
在Win7系统中,自带的截图快捷键是“PrtScn”键,即PrintScreen键。按下这个键后,系统会将当前屏幕的内容复制到剪贴板中,然后用户可以将其粘贴到其他应用程序中进行编辑或保存。此外,Wi...
- win10电脑所有软件都打不开(win10任何软件都打不开)
-
具体步骤如下:萊垍頭條1、如果遇到这类情况,你先看下快捷键alt+tab键能否查看,并把鼠标放在任务栏的图标上,或者查看一下窗口的缩略图。萊垍頭條2、我们将鼠标放在任务栏上,选中打不开的软件,然后al...
- 如何创建电子邮件账号(如何创建电子邮件账号在outlook中)
-
用QQ号的一键激活邮箱几乎是最快,最简单的注册邮箱手段了,且QQ邮箱功能强大,安全方便,推荐你使用,具体注册方法如下:1、你可以点击QQ面板邮箱快捷按钮,直接激活邮箱。2、如果你没有QQ,直接申请QQ...
- 戴尔音频驱动下载(戴尔电脑声卡驱动下载)
-
1、如果是笔记本没有音频设备的话,并不是没有输出设备,而是我们没有启用或者没有安装音频驱动导致的。先打开控制面板。2、打开控制面板之后下面依次找到音频清晰管理器,并且打开。3、打开之后我们这里把主音量...
- toshiba硬盘(TOSHIBA硬盘tlc)
-
东芝移动硬盘a3好,性价比很高,传输速率高,稳定耐用,安全高效外壳是磨砂质感!USB3.0,即插即用采用NTFS格式,兼容Windwos10、Windwos8.1、Windwos7,格式化后可兼容M...
- 完整版xp系统下载(xp系统最新版本安装包)
-
2012年前的可以无压力安装XP系统,搜索:itellyou.cn这里有WINDOWS几乎所有的系统。windowsXP系统升级的具体操作步骤如下:1、首先我们将老毛桃装机工具下载到U盘,将老毛桃...
- ps下载电脑版官方下载(ps电脑版下载地址)
-
目前在电脑上免费下载PS是不太可能的。主要有以下几个原因。1.AdobePhotoshop(简称PS)是一款商业软件,它需要用户购买和激活许可证才能合法使用。从正规渠道下载并且获得合法授权需要付费...
- 迅猛兔加速器(迅猛兔加速器官网)
-
要下载迅猛兔加速器,首先需要在官网或其他可信的下载平台上搜索并找到该软件。一般情况下,官网提供的下载链接是最稳定和安全的选择。在下载之前,确保您的电脑或手机系统能够支持使用此软件,并检查下载链接的文件...
- 台式电脑怎么重做系统(台式电脑怎么重装系统)
-
你好,电脑系统重装的步骤如下:1.备份数据:在重装系统之前,需要备份电脑中的重要数据,以免数据丢失。2.准备安装介质:需要准备一个安装介质,可以是光盘、U盘或者硬盘分区镜像等。3.设置启动顺序:将电脑...
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
