怎么看待nodejs可支持高并发
1、nodejs的单线程架构模型
nodejs并不是真正的单线程架构,其中还有I/O线程(网络I/O、磁盘I/O),这些线程是由更底层的libuv处理,js运行在v8上是单线程的。
单线程的好坏
优势:
- 省去了线程切换的开销
- 线程的同步、冲突问题不需要担心
劣势:
- 无法充分利用CPU资源
- 单线程崩溃程序就无了
- 只能用一个cpu,一旦其被某个计算占用,后续请求就会被一直挂起
2、Node.js中的事件循环机制
事件循环允许Node.js执行非阻塞I/O操作.尽管JavaScript是单线程的.通过尽可能将操作卸载到系统内核。由于大多数现代内核都是多线程的,因此它们可以处理在后台执行的多个操作。当其中一个操作完成时,内核会告诉Node.js,以便可以将相应的回调添加到轮询队列中以最终执行
EventLoop 中的 6个步骤都是相对于宏任务(Macro)的讲述的。NodeJs执行完成一个宏任务后会立即清空当前队列中产生的所有微任务。
timers
执行setTimeout(),setInterval()回调函数
pending callbacks
执行I/O回调
idle,prepare
尽在NodeJs内部调用,无法操作
poll
获取新的I/O事件
-
如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。
-
如果 轮询 队列 是空的 ,还有两件事发生:
- 如果脚本被
setImmediate()
调度,则事件循环将结束 poll(轮询) 阶段,并继续 check(检查) 阶段以执行那些被调度的脚本。 - 如果脚本 未被
setImmediate()
调度,则事件循环将等待回调被添加到队列中,然后立即执行。
- 如果脚本被
poll可以提前结束使事件循环,提升效率
check
执行setImeediate回调
close callbacks
执行socket的close事件回调
执行一些列关闭的回调函数,socket.on( 'close' , . . )
-
什么是同步异步
- 同步
- 等待被调用方执行完毕才能继续执行
- 会阻塞后面代码的执行
- 异步
- 不需要一直等待被调用方响应,调用方的主动轮询和被调用方的主动通知
- 不会阻塞后面代码的执行
- 区别:调用过程中是主动等待还是被动通知,是否阻塞
- 同步
-
什么是阻塞非阻塞
- 区别:调用状态,调用方在获取结果的过程中是干等还是互不耽误
- 异步非阻塞是节约调用方时间的(nodejs 一大特点)
典型
setTimeout(function timeout () { console.log('timeout'); },0);
setImmediate(function immediate () { console.log('immediate'); });
这里打印输出出来的结果,并没有什么固定的先后顺序,偏向于随机。(event loop启动需要时间)
答:首先进入的是timers
阶段,如果我们的机器性能一般,那么进入timers
阶段,1ms
已经过去了 ==(setTimeout(fn, 0)等价于setTimeout(fn, 1))==,那么setTimeout
的回调会首先执行。
如果没有到1ms
,那么在timers
阶段的时候,下限时间没到,setTimeout
回调不执行,事件循环来到了poll
阶段,这个时候队列为空,于是往下继续,先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout
的回调函数。
问题总结:而我们在==执行启动代码==的时候,进入timers
的时间延迟其实是==随机的==,并不是确定的,所以会出现两个函数执行顺序随机的情况。
3、nodejs 是异步非阻塞的
比如有个客户端请求A进来,需要读取文件,读取文件后将内容整合,最后数据返回给客户端。但在读取文件的时候另一个请求进来了,那处理的流程是怎么样的?
- 请求A进入服务器,线程开始处理该请求
- A 请求需要读取文件,ok,交给文件 IO 处理,但是处理得比较慢,需要花 3 秒,这时候 A 请求就挂起(这个词可能不太恰当),等待通知,而等待的实现就是由事件循环机制实现的,
- 在A请求等待的时候,cpu 是已经被释放的,这时候B请求进来了, cpu 就去处理B请求
- 两个请求间,并不存在互相竞争的状在 js 引擎上执行的,执行栈一直卡态。那什么时候会出现请求阻塞呢?涉及到大量计算的时候,因为计算是着,别的函数就没法执行,举个栗子,构建一个层级非常深的大对象,反复对这个这个对象
JSON.parse(JSON.stringify(bigObj))
nodejs怎么创建进程
cluster 模块可以创建共享服务器端口的子进程
// 1、引入cluster
const { default: cluster } = require('cluster')
const http=require('http')
// 2、获取核数
const cpuNums=require('os').cpus().length
// 3、用fork开启进程
if(cluster.isPrimary){
for(let i=0;i<cpuNums;i++){
cluster.fork()
cluster.on('exit',(worker,code,signal)=>{
console.log(`worker ${worker.process.pid} died`)
})
}
}else{
// 子线程操作
http.createServer((req,res)=>{
res.end('hello')
}).listen(8001,()=>{
console.log('running at 8001')
})
}
进程间通信
线程
在一个进程的前提下开启多个线程
const {
Worker, isMainThread, parentPort, workerData
} = require('worker_threads');
const worker = new Worker(__filename, {
workerData: script
});
- 线程间如何传输数据:
parentPort
postMessage
on
发送监听消息 - 共享内存:
SharedArrayBuffer
通过这个共享内存
可开辟线程执行大量计算,返回结果
nodejs特点
- 事件驱动
- 主线程通过event loop事件循环触发的方式来运行程序
- 非阻塞异步I/O模型
-
node 具有异步 I/O 特性,每当有 I/O 请求发生时,node 会提供给该请求一个 I/O 线程。然后 node 就不管这个 I/O 的操作过程了,而是继续执行主线程上的事件,只需要在该请求返回回调时再处理即可。也就是 node 省去了许多等待请求的时间
-
- 轻量高效
exports与modeul.exports
node中每一个模块都有一个自己的module对象
对外到处成员只要将其挂载到module.exports中
exports是module.exports的优化写法,exoprts=module.exports
-
当一个模块需要导出单个成员的时候,直接给
exports
赋值是不管用的。exports
赋值会断开和module.exports
之间的引用。同理,给module.exports
重新赋值也会断开
模块加载规则
- 优先从缓存中加载
- 如果已经
require
过,不会重复执行加载,直接可以拿到里面的接口对象 - 目的是避免重复加载,提高模块加载效率
- 如果已经
- 判断模块标识符
require('模块标识符')
- 核心模块
- 自定义模块(路径形式的模块标识)
- ./,../或/xxx,d:/a/foo.js
- 首位的/是绝对路径,代表当前文件模块所属磁盘根目录c:/
- 第三方模块
package-lock.json作用(存放包的下载信息,地址,版本等)
- 记住依赖信息,提升加载速度
- 为了系统的稳定性考虑,锁定版本号,防止自动升级
package.json文件(包的信息,依赖包的下载索引)
作用是:
- 保存第三方包的依赖信息,比 node_modules 清晰。package.json 文件相当于给他人使用时,提供了一份安装所有依赖包的自动下载索引
- dependencies:在生产环境中需要用到的依赖
- devDependencies:在开发、测试环境中用到的依赖
NodeJs运行机制
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户。
js-》v8-》node-》v8-》js
. 为什么JavaScript是单线程?
- 防止DOM渲染冲突的问题;
- Html5中的Web Worker可以实现多线程
什么是任务队列
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
path.join和path.resolve
path.join()
:所有给定的 path 片段连接到一起,然后规范化生成的路径path.resolve()
:方法会将路径或路径片段的序列解析为绝对路径,解析为相对于当前目录的绝对路径,相当于cd命令
- join是把各个path片段连接在一起, resolve把
/
当成根目录
path.join('/a', '/b') // '/a/b'
path.resolve('/a', '/b') //'/b'
复制代码
- join是直接拼接字段,resolve是解析路径并返回
path.join("a","b") // "a/b"
path.resolve("a", "b") // "/Users/tree/Documents/infrastructure/KSDK/src/a/b"
注册路由时 app.get、app.use、app.all 的区别是什么?
app.use
是express用来调用中间件的方法。中间件通常不处理请求和响应,一般只处理输入数据,并将其交给队列中的下一个处理程序,比如下面这个例子app.use('/user')
,那么只要路径以 /user
开始即可匹配,如 /user/tree
就可以匹配
app.all
是路由中指代所有的请求方式,用作路由处理,匹配完整路径,在app.use之后 可以理解为包含了app.get、app.post等的定义,比如app.all('/user/tree')
,能同时覆盖:get('/user/tree') 、 post('/user/tree')、 put('/user/tree')
,不过相对于app.use()的前缀匹配,它则是匹配具体的路由
两个node程序之间怎样交互?
通过fork,原理是子程序用process.on来监听父程序的消息,用 process.send给子程序发消息,父程序里用child.on,child.send进行交互,来实现父进程和子进程互相发送消息
父亲调用child.send,子调用process.on接受
子调用process.send,父调用child.on接受