Nodejs将原本由浏览器进行渲染的操作放到了服务器上,通过服务器直接渲染返回前端,加速网页展示速度,同时nodejs具有文件进程等操作能力,可以用于构建客户端程序
分析需求+攻克难点
该层需要的功能:
查看安装:node -v 和 npm -v
该方法存在问题:标签书写顺序与加载顺序是相同的,不同脚本调用需要通过全局变量去调用(如jq的$)
require
来加载依赖,会在书写处加载然后执行exports
用于定义当前文件中的输出a.js:
..........
exports.hello='world'
b.js:
var b=require(./a.js)
console.log(b)
// {hello: 'world'}
注意:
一句话解释:可以将exports看作module.exports的快捷方式,当改变module.exports时,exports便不再与module.exports相关联
包管理工具,包就是别人写的nodejs模块
npm包可以安装其他npm包,在写nodejs项目时,第一步要npm init
将自己的工程声明为一个npm包
安装:
npm install 包名
或者
npm i 包名
卸载:
npm uninstall 包名
注意:
-g:全局安装
–save:写入package.json的dependencies
-S:同上
-D:写入package.json的Devdependencies
如果没有加参数,可能会出现只在工程中安装了模块,但是package.json中并未出现的情况
适用场景:
一句话:通知者和被通知者不存在耦合关系
阻塞IO:在IO流未完成时,该IO外部的代码被阻塞
非阻塞IO:在IO输入到输出期间,可以接受其他IO输入
查看操作耗时:
console.time('a')和console.timeEnd('a')
来查看之间的操作耗时
Nodejs所有操作都是非阻塞IO,获取IO结果需要通过回调函数来进行
即使用非阻塞IO获取结果,并在回调函数中处理结果
回调函数的格式规范:
即:第一个参数为error,后面的参数才是结果
调用栈:程序运行时,函数调用函数,形成一个调用的嵌套栈,位于栈顶的元素是最里层的调用。当使用try catch时,位于try包裹函数里的调用栈如果抛出异常会被捕获
事件循环:nodejs的一个概念,每次事件循环会生成一个新的调用栈(setTimeout函数是在新的事件循环中调用的)
注意:在一个异步任务中抛出的错误,无法被任务外的try catch包裹到,如果一定要使用,可以使用传递函数的方法,如下:
fucntion a(callback){
setTimeout(()=>{
if(Math.random()<0.5){
callback(null,'success')
}else{
callback(new Error('fail'))
}
},500)
}
a(function(res){
if(res instanceof Error){
...
}
...
})
问题:异步流程问题(回调地狱 和 异步并发)
社区解决方案:
nodejs会将事件放入一个队列,并且在每次一个很短的事件间隔中检测该队列中的每个事件是否执行完毕(每个事件都可能会包含多个队列),如果执行完毕则执行回调函数,注意:在回调前的部分一般都是c代码,只有回调函数后才是nodejs的调用栈
第二种异步编程解决方案
承诺:当前未得到结果,但未来会有结果
是一个状态机,包含三种状态:
graph LR
pending-->|value|fulfilled
pending-->|error|rejected
// 一个promise的构成
new Promise(function(resolve,reject){
// 此处调用resolve或者reject会改变状态机的运行方向
resolve();
// reject();
})
.then() // 当promise进入resolve状态时:该方法需要一个函数,函数入参为resolve的结果(resolve传入的值,只能接收一个参数)
.catch() // 当promise进入reject状态时:该方法需要一个函数,函数入参为reject的结果。注意:在catch中可以捕获promise的错误,不会抛到全局
Promise的then或者catch方法返回的依然是promise,其返回promise的状态与then或者catch方法返回值有关(若then/catch中语句正常return则为resolve,如果时throw则为reject)。注意:若return new Promise则结果与该Promise挂钩
并行异步
并行异步使用Promise.all([])
Promise.all([xxx,xxx,...])
.then(()=>{
})
// 注意:此处catch到的是all中第一个reject的promise
.catch(err=>{
})
async函数与return为一个Promise的函数相同,是该函数的一个语法糖而已
若async函数结果是return则为resolve状态的promise,如果是throw则为reject状态的promise
异步函数就是一个返回promise的普通函数
await关键字:
async/await是一个穿越事件循环存在的function
示例:
const http = require('http')
http.createServer((req,res)=>{
res.writeHead(200);
res.end('hello')
}).listen(3000)
// 浏览器访问:localhost:3000即可
一个封装了http请求的第三方包
示例:
const express = require('express')
const app = express();
app.listen(3000);
app.get('/favicon.ico',function(req,res){
res.writeHead(200);
res.end();
return;
})
app.get('/',function(req,res){
res.send(fs.readFiileSync(__dirname+'/index.html')) //发送文件,express会自动判断,不加encoding,发送buffer
res.send(fs.readFiileSync(__dirname+'/index.html','utf-8')) //加上encoding,则发送字符串,也可以通过res.set方法来手动设置返回内容格式
})
app.get('/game',function(req,res){
// res.writeHead(200);
// res.end();
const query = req.query; // query.字段 即可获取url参数里的对应key的value
res.status(200); // 上面两个可以简化成
// res.send('hello world') //可以用于在网页上显示
return;
})
req.send()方法:
The body parameter can be a Buffer object, a String, an object, or an Array. For example:
res.send(Buffer.from('whoop'))
res.send({
some: 'json' })
res.send('some html
')
res.status(404).send('Sorry, we cannot find that!')
res.status(500).send({
error: 'something blew up' })
req.set()方法:
该方法可以设置返回内容的格式
res.set('Content-Type', 'text/plain')
res.set({
'Content-Type': 'text/plain',
'Content-Length': '123',
'ETag': '12345'
})
fs.readFileSync()方法:
fs.readFileSync(path[, options]) 读取文件,返回文件名或者描述,如果指定了encoding,则返回字符串,否则返回buffer
express还提供中间件功能:next,在调用了next的地方会跳到下一个回调函数中去,执行完所有的又会回到原处继续往下执行
app.get('/favicon.ico',
function(req,res){
...
next()
...
},
function(req,res){
...
next()
...
},
function(req,res){
...
next()
...
},
)
注意:当回调函数中有异步操作时,如:setTimeout()等,会直接跳过,继续执行,因为异步操作所在事件循环和当前不是一个,所以express中无法获取异步操作中的值。(催生出koa)
koa解决了express中间件无法使用异步函数的缺陷
引入ctx:context上下文变量,该变量包括request和response,同时可以用来传递数据
在处理req和res时使用直接赋值方式
koa不绑定任何中间件,路由都是放在函数中自己实现,也可以使用路由中间件来实现,如:koa-mount
注意:koa需要使用new来实例化对象
代码示例:
const koa=require('koa')
const app=new koa();
app.use(async(ctx,next)=>{
...
await next();
...
});
app.use(async(ctx,next)=>{
...
return next().then((=>{
...
}))
});
app.use(async(ctx,next)=>{
await next();
ctx.response.type='xml';
ctx.response.body=fs.createReadStream('xxx.xml');
})
使用koa-mount来使用路由
// 使用路由,设置状态
const mount = require('koa-mount')
app.use(mount('/favicon.ico',function(ctx){
ctx.status=200;
}))
// 使用路由,且输出body
app.use(mount('/',function(ctx){
ctx.body=fs.readFiileSync(__dirname+'/index.html','utf-8')
}))
注意:mount函数只能接收一个路由和一个函数,不能写多个函数,如果要执行多个函数,可以采用新实例化一个koa,依次挂载多个函数,然后将实例当作函数挂载
const game=new koa();
game.use(
async function (ctx,next){
...
await next()
...
}
)
game.use(
function (ctx,next){
...
next()
...
}
)
game.use(
async function (ctx,next){
...
await next()
...
}
)
// 将实例挂载在这里
const app=new koa();
app.use(mount('/game',game))
与ajax请求类似:
区别点:
ajax请求:
使用DNS寻址
graph LR
浏览器-->|发送域名|DNS
DNS-->|返回IP|浏览器
浏览器-->|使用IP沟通|服务器
服务器-->|返回数据信息|浏览器
RPC调用
使用特定服务器进行寻址,将域名改成了ID,其他的都一样
RPC:TCP通信方式
ajax:http通信协议
ajax使用http协议:包括html和json
比如,json使用需要定义key,增加了包的体积
RPC使用二进制协议:更小的体积、更快的解码速率**
直接采用二进制数表示,每个字段不定多少个位,灵活传输,减少体积
一般是服务端之间的通信,所以速度要求快
该包用来处理TCP流
注意:int8和int16表示的含义是:二进制长度为8和二进制长度为16位长的数值范围。其中,每个长度表示一个二进制数,3个长度表示一个8进制数,4个长度表示一个16进制数。
在buffer中,每一个数字为一个16进制数,即4个二进制位数,所以一个int8占用2个数字,int16占用4个数字
在buffer中,两个数字称作一位
大端 or 小端
表示的含义是高位BE和低位LE的排布:
protocol buffer:google研究出来的二进制编码序列化结构化的库,与语言无关;该库包括js的编解码,没有专门针对nodejs的
protocol-buffers:npm包,针对nodejs编解码
使用方式:
// 先定义一个demo.proto文件,需要配上字段序号
message Test {
required float num = 1;
required string payload = 2;
required int32 id = 3;
}
// 使用方法
var protobuf = require('protocol-buffers')
// 传递一个proto的schema对象或者proto文件buffer/string
var schema = protobuf(fs.readFileSync('test.proto'))
// 查看schema包含的结构
console.log(schema)
// 进行编码
var buf = schema.Test.encode({
num: 42,
payload: 'hello world',
id: 1123,
})
// 查看buffer
console.log(buf)
// 解码,解出来是一个js对象
console.log(schema.Test.decode(buf));
RPC使用TCP通信方式,TCP使用socket通信
类似于http模块,不过http用的req和res,net用的是socket
搭建最简单的单工通信:使用net包
// server端
const net = require('net')
const server = net.createServer((socket)=>{
socket.on('data',function(buffer){
console.log(buffer,buffer.toString())
})
})
server.listen(4000)
// client端
const net = require('net')
const socket = new net.Socket({
})
socket.connect({
host:'127.0.0.1',port:4000})
socket.write('good morning')
socket.on():持续监听事件,如果事件发生则进入回调函数执行,接收两个参数:事件名、回调函数
注意:半双工通信直接转换成全双工通信会存在时序问题,当连续多个请求发送到服务端时,服务端处理各请求速度不一样,有可能后发送请求结果先于前请求结果返回——引入时序
注意:如果通过循环连续发送请求包,TCP底层会将其拼成一各TCP包,一次性发送到服务端——称为【沾包】,全双工通信问题
沾包:可以通过切分来解决
const server = new RPC({
decodeResquest(buffer){
},
isCompleteRequest(buffer){
},
// 切分数据,在每个数据的前面附加8位长度的字段,前4位存序列,后四位存包数据长度
encodeResponse(data,seq){
...
const body = schemas.ColumnResponse.encode(data)
const head = aBuffer.alloc(8)
head.writeUint32BE(seq)
head.writeUnit32BE(body.length,4)
return Buffer.concat([head,body])
}
})
netty:javaRPC通信框架
nodejs作用:向前端提供http服务,向后端进行RPC通信
nodejs两大特性:V8引擎+非阻塞IO&事件循环,达到高性能服务标准
RESTful:
GraphQL:
facebook实现的api服务库,让前端能【自定义查询】数据
先安装react react-dom @babel/register @babel/preset-react @babel/core
next.js:服务端渲染react
项目做好后需要进行性能测试才能最后上线
常用的性能测试工具:apache bench 和 webbench
ab -c200 -n1600 http://127.0.0.1:3000/download/
-c指定并发数
-n指定总共要执行多少次请求
-t指定压测总时间
压测的同时服务器执行top
,用来显示cpu和内存使用量;执行iostat
,用来显示硬盘的状态
node --prof xxx.js
node --prof-process xxxx.log > profile.txt
由于使用prof性能分析工具最后是问本行式,如果想要更直观的显示,额可以使用chrome自带的性能分析工具
node --inspect-brk xxxx.js
chrome://inspect
,在target中可以看到监听的地址注意:–inspect-brk表示运行js时,程序暂停在最开始处,直到手动继续
提供比chrome更多可视化的图表分析
采用上述几种方式观察可优化的性能点,从最高的几个点开始优化
优化方案:
nodejs http优化准则:提前计算,将服务过程转移到启动过程
v8分为新生代和老生代。。。
新生代:容量小,垃圾回收快
老生代:正好相反
优化原则:
可以用chorom对其进行分析,选择内存页面,保存快照,然后进行对比,判断内存占用点在哪里
buffer相当于c++的char[]数组
小于8kb buffer:直接new一个8kb空间,之后申请的buffer如果够装,则会在这个空间中进行分配,当小于8kb且剩余空间不够装时,重新分配8kb进行分配,但是原来的剩余空间依然保留,给能装下的buffer分配。如果有对象被销毁,则占用的空间变成可分配空间继续使用。【大大节省内存分配消耗,类似池】
节省内存最好的方式就是使用【池】
安装node-gyp,参考官网
编写c++插件:
.node
文件即可由于非napi插件(即底层v8接口直接编写)与平台相关过大,接口经常变化,所以推荐使用NAPI来编写(相当于封装了一层)
将计算转移到C++:
所以,c++插件不一定比js快,要综合考虑
进程:操作系统挂载程序,运行程序单元,拥有独立的资源
线程:操作系统运算调度的单元,统一进程内共享内存资源
进程类似“公司”,线程类似“职员”
Nodejs事件循环:
nodejs可以通过创建子进程模块来充分利用cpu:
主进程中:
const cp = require('child_process');
const c = cp.fork(__dirname + '/child.js'); //创建并使用node子进程来运行js
c.send("haha") //主进程向子进程发送消息
c.on('message',(str)=>{
//主进程监听消息
console.log(str)
})
子进程:
process.on('message',(str)=>{
//子进程监听消息,process是全局变量
console.log(str)
process.send("abc") //发送消息给主进程
})
注意:如果确实是需要使用大量的子进程,可以使用worker_threads做任务分发,这个功能主要是用于对cpu敏感的任务中,使用这个包并不会对io有什么提升(因为nodejs异步事件循环已经足够好了)
实现的原理是:浏览器给node发送http请求时,node不直接处理,而是fork几个子进程,将请求转发给其中一个子进程,子进程处理完成返回处理结果给node主进程,最后返回给浏览器。
cluster模块完成了上述过程
示例代码:
const cluster = require('cluster')
if(cluster.isMaster){
cluster.fork()
cluster.fork()
cluster.fork()
}else{
require('./app')
}
这种方式充分利用了电脑的多核能力,提升了计算效率
这里可以使用os
包提供的api来获取电脑cpu核数:
...
const os = require('os')
if(...){
for(let i=0;i<os.cpus().length;i++){
...
}
}else{
...
}
注意:并非每个cpu核都需要fork一个进程,由于node本身的事件循环和其他功能都会占用cpu核,所以并非越多越好,而且每次fork都会重新复制一份代码,成倍的占用内存空间,所以一般而言,都是采取:os.cpus().length/2
这种方式
注意:一般而言,app.js中对3000端口的监听只能监听一次,重复监听会报“端口已经被占用”的错误。这里正常监听多次是因为cluster模块覆写了listen方法。
使用情形:当进程出现错误时导致退出:
可以使用uncaughtException
来监听这种未捕获异常,监听该事件会导致原本结束码为1的进程退出变为不退出。这是非常危险的事情,因为报错进程无法在服务,但是却不退出。
使用方法:
if(...){
for(let i=0;i<os.cpus().length;i++){
...
}
// 由于子进程监听到未捕获错误,然后退出了,主进程可以在监听到进程退出事件时,再fork一个出来,保证进程的个数
cluster.on('exit',()=>{
setTimeout(()=>{
cluster.fork()
},5000)
})
}else{
...
process.on('uncaughtException',(err)=>{
console.error(err)
process.exit(1) //由于不会退出了,所以必须手动退出
})
// 同时监控内存情况,防止内存泄漏,这里如果当前线程的内存占用大于700M时,就退出,然后让主线程重新启动
setInterval(()=>{
if(process.memoryUsage().rss>734003200{
console.log('oom');
process.exit(1);
})
},5000)
}
增加内存监控:
if(...){
for(let i=0;i<os.cpus().length;i++){
...
}
}else{
...
// 同时监控内存情况,防止内存泄漏,这里如果当前线程的内存占用大于700M时,就退出,然后让主线程重新启动
setInterval(()=>{
if(process.memoryUsage().rss>734003200{
console.log('oom');
process.exit(1);
})
},5000)
}
增加心跳检测:解决僵尸进程
if(...){
for(let i=0;i<os.cpus().length;i++){
const worker = cluster.fork()
// 主进程每个3秒发送一个心跳包
let missedPing = 0;
let inter = setInterval(()=>{
worker.send('ping')
missedPing++
if(missedPing>=3){
clearInterval(inter)
process.kill(worker.process.pid)
}
},3000);
worker.on('message',(msg)=>{
if(msg =='pong'){
missedPing--;
}
})
}
}else{
...
// 子进程里监听message信息
process.on('message',(str)=>{
if(str =='ping'){
process.send('pong')
}
}
含义:
静:不会因为请求参数不同而变动
动:由参数变动而内容不通,且变动数量不可枚举
分开部署:
静态:CDN分发,HTTP缓存
动态:用大量源站机器承载,结合反向代理和负载均衡
示例:模拟浏览器发送请求,访问nginx静态页面
注意:nginx.conf配置文件中,user nginx
表示使用用户为nginx的来访问文件,如果文件是root用户创建,则可能会没有权限访问,所以可以改成user root
。
安装压测工具,再linux上,ab工具叫做:httpd-tools
启动nodejs,使用ab命令压测:
ab -c 400 -n1600 http://127.0.0.1:3000/
此时,qps为3100多
启动nginx,使用ab命令压测:
ab -c 400 -n1600 http://127.0.0.1:80/
此时,qps为6000多
结论:使用nginx来作为静态页面能极大提升性能,可以在location中定义动态转发
配置nginx.conf:做反代
location ~ /node/(\d*){
proxy_pass http://127.0.0.1:3000/detail?columnid=$1;
}
配置nginx.conf:做负载均衡,开启两个nodejs进程,分别监听3000和3001,当反代时会根据规则选择upstream中的一个server
upstream abc.com{
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
...
location ~ /node/(\d*){
proxy_pass http://abc.com/detail?columnid=$1;
}
配置nginx.conf:做缓存,再proxy_pass下一行输入:proxy_cache
即可