[Nodejs] Node.js快速学习

Node.js学习

  • Node.js简介
  • 技术预研
    • 环境搭建
    • 模块规范
      • CommnJS
    • npm
    • 观察者模式
    • 非阻塞IO
    • 异步编程
    • 事件循环
    • Promise
    • async函数
    • HTTP
    • express
    • koa
    • RPC通信
      • 1.寻址方式
      • 2.通信方式
      • 3.协议
    • Nodejs Buffer编解码
      • buffer创建
    • RPC调用多路复用(Socket编程)
  • 实战项目
    • GraphQL
    • react实现前后端同构
  • 性能优化
    • ab
    • nodejs性能分析工具
      • profile
      • Chrome devtool
      • Clinic.js
    • nodejs优化
    • nodje内存优化
      • 垃圾回收
      • nodejs buffer内存分配
    • 使用C++插件优化nodejs服务计算性能
    • nodejs使用多进程和多线程优化
    • nodejs利用多核网络服务程序:cluster
    • nodejs进程守护与管理
    • 动静分离
      • 对比使用nodejs和nginx来访问静态文件效率
      • 反向代理和缓存

by xiaopi3 from 极客时间

Node.js简介

Nodejs将原本由浏览器进行渲染的操作放到了服务器上,通过服务器直接渲染返回前端,加速网页展示速度,同时nodejs具有文件进程等操作能力,可以用于构建客户端程序

技术预研

分析需求+攻克难点

  • BFF层:backend for frontend:属于浏览器和服务端之间的中间渲染层,负责组装微服务返回的数据成前端所需要的数据给浏览器

该层需要的功能:

  • 对用户侧提供HTTP服务
  • 使用后端RPC服务

环境搭建

  • Chrome
  • VScode
  • Node.js

查看安装:node -v 和 npm -v

模块规范

该方法存在问题:标签书写顺序与加载顺序是相同的,不同脚本调用需要通过全局变量去调用(如jq的$)

  • CommonJS模块规范,用于改进上述缺点

CommnJS

  • require来加载依赖,会在书写处加载然后执行
  • exports用于定义当前文件中的输出
  • require有输出,如果被引用文件没有通过exports定义属性,则输出为空对象,否则为一个包含属性的对象
  • require获得的变量引用和exports的引用指向同一个对象
a.js:
..........
exports.hello='world'

b.js:
var b=require(./a.js)
console.log(b)

// {hello: 'world'}

注意:

  1. 在当前文件中获取未赋值的exports和module.exports,结果都是{}
  2. 在当前文件中,若只给exports赋值,则exports和module.exports指向同一个对象,若给module.exports赋值,则两者指向不通的对象
  3. 通过require获取到的引用是module.exports

一句话解释:可以将exports看作module.exports的快捷方式,当改变module.exports时,exports便不再与module.exports相关联

npm

包管理工具,包就是别人写的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中并未出现的情况

观察者模式

适用场景:

  1. 通知者不需要知道被通知者的存在
  2. 被 通知者不存在,通知者也能继续发送通知

一句话:通知者和被通知者不存在耦合关系

非阻塞IO

阻塞IO:在IO流未完成时,该IO外部的代码被阻塞

非阻塞IO:在IO输入到输出期间,可以接受其他IO输入

查看操作耗时:console.time('a')和console.timeEnd('a')来查看之间的操作耗时

Nodejs所有操作都是非阻塞IO,获取IO结果需要通过回调函数来进行

异步编程

即使用非阻塞IO获取结果,并在回调函数中处理结果

回调函数的格式规范:

  • error-first callback
  • Node-style callback

即:第一个参数为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){
     
        ...
    }
    ...
})

问题:异步流程问题(回调地狱 和 异步并发)

社区解决方案

  • npm:async.js包,目前比较过时了
  • thunk编程方法,目前也比较过时了

事件循环

nodejs会将事件放入一个队列,并且在每次一个很短的事件间隔中检测该队列中的每个事件是否执行完毕(每个事件都可能会包含多个队列),如果执行完毕则执行回调函数,注意:在回调前的部分一般都是c代码,只有回调函数后才是nodejs的调用栈

Promise

第二种异步编程解决方案

承诺:当前未得到结果,但未来会有结果

是一个状态机,包含三种状态:

  1. pending
  2. fulfilled/resolved
  3. rejected
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函数

async函数与return为一个Promise的函数相同,是该函数的一个语法糖而已

若async函数结果是return则为resolve状态的promise,如果是throw则为reject状态的promise

异步函数就是一个返回promise的普通函数

await关键字:

  1. 暂停async函数的执行,知道后面的promise状态改变
  2. 获取后面promise的结果,并赋值到变量中
  3. 可以打破异步无法try catch的界限,捕捉到异常

async/await是一个穿越事件循环存在的function

HTTP

示例:

const http = require('http')
http.createServer((req,res)=>{
     
    res.writeHead(200);
    res.end('hello')
}).listen(3000)

// 浏览器访问:localhost:3000即可

express

一个封装了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

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))

RPC通信

与ajax请求类似:

区别点:

1.寻址方式

ajax请求:
使用DNS寻址

graph LR
浏览器-->|发送域名|DNS
DNS-->|返回IP|浏览器
浏览器-->|使用IP沟通|服务器
服务器-->|返回数据信息|浏览器

RPC调用
使用特定服务器进行寻址,将域名改成了ID,其他的都一样

2.通信方式

RPC:TCP通信方式

  • 单工:只有一方能发送消息,且固定
  • 半双工:双方可以轮流发送消息,但同一时间只有一方工作
  • 全双工通信

ajax:http通信协议

3.协议

ajax使用http协议:包括html和json

比如,json使用需要定义key,增加了包的体积

RPC使用二进制协议:更小的体积、更快的解码速率**

直接采用二进制数表示,每个字段不定多少个位,灵活传输,减少体积

一般是服务端之间的通信,所以速度要求快

Nodejs Buffer编解码

该包用来处理TCP流

buffer创建

  • from() :从数据结构中创建buffer
  • alloc() :创建指定长度的buffer

注意:int8和int16表示的含义是:二进制长度为8和二进制长度为16位长的数值范围。其中,每个长度表示一个二进制数,3个长度表示一个8进制数,4个长度表示一个16进制数。

在buffer中,每一个数字为一个16进制数,即4个二进制位数,所以一个int8占用2个数字,int16占用4个数字

在buffer中,两个数字称作一位

大端 or 小端

表示的含义是高位BE和低位LE的排布:

  • 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调用多路复用(Socket编程)

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:

  • 专注数据聚合,可以返回数据内容中的某个字段,也能聚合好资源后再返回给前端

GraphQL

facebook实现的api服务库,让前端能【自定义查询】数据

react实现前后端同构

先安装react react-dom @babel/register @babel/preset-react @babel/core

next.js:服务端渲染react

性能优化

项目做好后需要进行性能测试才能最后上线

常用的性能测试工具:apache bench 和 webbench

ab

ab -c200 -n1600 http://127.0.0.1:3000/download/

-c指定并发数
-n指定总共要执行多少次请求
-t指定压测总时间

压测的同时服务器执行top,用来显示cpu和内存使用量;执行iostat,用来显示硬盘的状态

nodejs性能分析工具

profile

  1. 启动js时,启用性能分析:node --prof xxx.js
  2. 程序运行时会在当前路径下会新增一个文件xxxx.log,比如:进行一段ab压测
  3. 将文件输出到一个文本文件中查看:node --prof-process xxxx.log > profile.txt

Chrome devtool

由于使用prof性能分析工具最后是问本行式,如果想要更直观的显示,额可以使用chrome自带的性能分析工具

  1. 启动js,启动chrome调试工具:node --inspect-brk xxxx.js
  2. 此时输出一个监听地址
  3. 启动chrome,地址栏进入:chrome://inspect,在target中可以看到监听的地址
  4. 点击监听下的inspect进入性能调试工具
  5. 点击profiler选项,点击录制,控制台进行ab压测,完成后停止录制,观察性能记录情况

注意:–inspect-brk表示运行js时,程序暂停在最开始处,直到手动继续

Clinic.js

提供比chrome更多可视化的图表分析

nodejs优化

采用上述几种方式观察可优化的性能点,从最高的几个点开始优化

优化方案:

  1. 减少不必要的计算,即将重复计算移出中间件,将计算结果直接引入,如文件读取
  2. 空间换时间

nodejs http优化准则:提前计算,将服务过程转移到启动过程

nodje内存优化

垃圾回收

v8分为新生代和老生代。。。
新生代:容量小,垃圾回收快
老生代:正好相反

优化原则:

  • 减少内存使用,提高服务性能(内存用的越少,清理速度越快,垃圾回收次数减少)
  • 防止内存泄漏(内存泄漏会导致大量老生代对象出现,每次回收都会遍历一遍)绝对禁止

可以用chorom对其进行分析,选择内存页面,保存快照,然后进行对比,判断内存占用点在哪里

nodejs buffer内存分配

buffer相当于c++的char[]数组

小于8kb buffer:直接new一个8kb空间,之后申请的buffer如果够装,则会在这个空间中进行分配,当小于8kb且剩余空间不够装时,重新分配8kb进行分配,但是原来的剩余空间依然保留,给能装下的buffer分配。如果有对象被销毁,则占用的空间变成可分配空间继续使用。【大大节省内存分配消耗,类似池】

节省内存最好的方式就是使用【池】

使用C++插件优化nodejs服务计算性能

安装node-gyp,参考官网

编写c++插件:

  1. 编写源码文件:xxx.cc
  2. 编写binding文件:xxx.gyp (类似webpack.json)
  3. 编译:node-gyp rebuild
  4. 编写js文件,直接require编译文件中的.node文件即可

由于非napi插件(即底层v8接口直接编写)与平台相关过大,接口经常变化,所以推荐使用NAPI来编写(相当于封装了一层)

将计算转移到C++:

  • 收益:C++计算快
  • 成本:C++和V8变量转换的消耗

所以,c++插件不一定比js快,要综合考虑

nodejs使用多进程和多线程优化

进程:操作系统挂载程序,运行程序单元,拥有独立的资源
线程:操作系统运算调度的单元,统一进程内共享内存资源

进程类似“公司”,线程类似“职员”

Nodejs事件循环:

  • 主线程运行V8和js
  • 多个子线程通过事件循环调度

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异步事件循环已经足够好了)

nodejs利用多核网络服务程序:cluster

实现的原理是:浏览器给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方法。

nodejs进程守护与管理

使用情形:当进程出现错误时导致退出:

可以使用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

对比使用nodejs和nginx来访问静态文件效率

安装压测工具,再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即可

你可能感兴趣的:(Nodejs,nodejs)