什么是node.js?
node.js使用了一个事件驱动、非阻塞式I/O的模型,使其轻量又高效。
node.js是一个基于Chrome V8引擎的JavaScript运行环境。 → 在node.js中运行JavaScript跟在Chrome里运行JavaScript有什么不同? → 理论上来说,在node.js里写JavaScript和在Chrome里写JavaScript几乎没有不同 → 几乎没有不同的地方,也就是有不同的地方,哪里不一样呢? → 1、node.js没有浏览器API,即document,window等。2、增加了node.jsAPI。→ 那么对于开发者来说,在Chrome里写JavaScript控制浏览器,node.js让我们可以用类似的方式控制整个计算机。
node.js非阻塞I/O
I/O即Input/Output,一个系统的输入和输出。阻塞I/O和非阻塞I/O的区别就在于系统接收输入到输出期间能不能接收其他输入。
理解非阻塞I/O的要点在于:1、确定一个进行Input/Output的系统,2、思考这个系统在Input/Output过程中,能不能进行其他Input/Output。
const glob = require('glob');
//阻塞I/O
var result = null;
console.time('glob');
result = glob.sync(__dirname + '/**/*');//同步的读取文件里的内容
console.timeEnd('glob');//打印文件读取的时间
console.log(result);
//输出结果
glob: 12.77ms
'/users/......'
//非阻塞I/O
var result = null;
console.time('glob');
glob(__dirname + '/**/*', function(err,res){
result = res;
//console.log(result);
console.log('got result');
})
console.timeEnd('glob');
cnosole.log(1 + 1)
//输出结果
glob: 2.684ms
2
got result
复制代码
下面是nodejs异步编程的原理结构图,其中我们可以把左边当做一个node.js线程,node.js的所有I/O操作都是非阻塞的,它会把大量的计算能力分发到其他的C++线程(图中右边部分),等到其他的C++线程计算完毕之后再把结果返回到node.js线程中,node.js 再把计算结果返回给应用程序。在其他的C++线程计算过程中,node.js线程也在同时处理其他请求或事件,而不是一直在等其他的C++线程的计算结果。这就是node.js的非阻塞I/O。
node.js事件循环
关于node.js的介绍的第一句就是:node.js使用了一个事件驱动、非阻塞式I/O的模型,使其轻量又高效。
node.js用事件驱动和非阻塞式I/O的方式实现了一个单线程、高并发的JavaScript运行环境。单线程意味着同一时间只能处理一件事,一般来说高并发的实现是提供多线程模型,服务器为每个客户端请求分配一个线程,使用同步I/O,也就是阻塞式I/O,系统通过线程切换来弥补同步I/O调用的时间开销。node.js是单线程模型,采用一个主线程来处理所有的请求,然后对 I/O 操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。
Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。
当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();
// 创建事件处理程序
var connectHandler = function connected() {
console.log('连接成功。');
// 触发 data_received 事件
eventEmitter.emit('data_received');
}
// 绑定 connection 事件处理程序
eventEmitter.on('connection', connectHandler);
// 使用匿名函数绑定 data_received 事件
eventEmitter.on('data_received', function(){
console.log('数据接收成功。');
});
// 触发 connection 事件
eventEmitter.emit('connection');
console.log("程序执行完毕。");
//打印结果
连接成功。
数据接收成功。
程序执行完毕。
复制代码
node.js异步编程
node.js回调函数格式规范:error-first callback/node-style callback,即第一个参数是error,后面的参数才是结果。
callback会造成的流程控制问题:1、异步回调的层层调用很容易造成回调地狱,让我们的代码维护性很低。2、异步事件并发,需要获取到两个异步事件的结果再进行下一步,这时代码的逻辑会比较繁琐
node.js官网给出解决方案:async.js(有点过时)提供异步并发控制,async.forEachOf方法可以分别执行每一个异步操作,等异步操作都结束后再执行后面的逻辑。
var async = require("async");
var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"};
var configs = {};
async.forEachOf(obj, (value, key, callback) => {
fs.readFile(__dirname + value, "utf8", (err, data) => {
if (err) return callback(err);
try {
configs[key] = JSON.parse(data);
} catch (e) {
return callback(e);
}
callback();//每个callback都执行之后,才会执行后面的逻辑
});
}, err => {
if (err) console.error(err.message);
// configs is now a map of JSON data
doSomethingWith(configs);
});
复制代码
node.js中也可以使用promise来进行异步编程,promise让我们能够以同步的逻辑编写异步代码。promise执行then和catch会返回一个新的promise,该promise最终状态会根据then和catch的回调函数的执行结果决定。同时可以通过调用promise.all,promise.race等API能够对并发多个异步操作的结果进行逻辑控制。
异步编程的终极方案:以同步的方式写异步,async / await 。async函数是promise的语法糖封装,async / await的特点:
- await关键字可以“暂停”async函数的执行
- await关键字可以以同步的写法获取promise的执行结果
- try-catch可以获取await所得到的结果
node.js提供HTTP服务
一个网页请求,包含两次HTTP包交换:1、浏览器想HTTP服务器发送请求HTTP包。2、HTTP服务器向浏览器返回HTTP包
HTTP服务器提供HTTP服务,HTTP服务的功能主要是解析进来的HTTP请求报文以及返回对应的HTTP返回报文。
var http = require("http");
http.createServer(function(req,res){
res.writeHead(200);
res.end('hello');
}).listen(3000)
复制代码
node.js引入调用内置的HTTP模块,该模块提供的createServer方法就能够创建一个能够进行HTTP服务的实例,调用该实例的listen方法就能够监听本机的3000端口。
node.js提供的Express框架-让我们能够更加简洁、更加方便地写出HTTP服务,大大减轻开发负担。
var fs = require("fs");
var express = require("express");
const app = express();
app.use(function(req, res){
});
//get表示该HTTP请求的方法是get
app.get('/favicon.ico',funtion(req,res){
res.status(200);
//res.writeHead(200);
//res.end('hello');
});
//返回本地文件
app.get('/',funciton(req,res){
res.send(fs.readFileSync(__dirname + '/index.heml','utf-8'));
})
//请求方法是post
app.post('/favicon.ico',funtion(req,res){
//....
});
app.listen(3000);
复制代码
Express提供中间件next方法,可以实现洋葱模型,但是在异步的操作中,洋葱模型会失效。由于Express对于异步操作的支持不是很完善,node.js社区推出Koa框架。Koa特点:
- 使用async函数实现的中间件,有“暂停执行”的功能,在异步的情况下也符合洋葱模型
- context对象上挂载HTTP请求的request和response,ctx.request,ctx.response。返回内容:ctx.response.body = fs.createReadStream(‘really_large.xml’);
- 相对于Express,Koa将路由功能砍掉了,Koa采用极简的思路,没有绑定任何的中间件,我们可以将路由放到中间件里实现(例如koa-mount中间件),但是Koa里没有提供路由的中间件
var fs = require("fs");
var koa = require("koa");
var mount = require("koa-mount");
const app = koa();
app.use(
mount('/favicon.ico',function(ctx){
ctx.status = 200;
})
);
app.use(
mount('/',function(ctx){
ctx.body = fs.readFileSync(__dirname + '/index.heml','utf-8');
})
);
app.listen(3000);
复制代码
Express VS Koa
- Express门槛更低,Koa更强大优雅
- Express封装更多东西,开发更便捷,Koa可定制性更高
RPC(Remote Procedure call)调用
RPC调用和前端的ajax相似,ajax指浏览器和服务器之间的通信,RPC调用一般指服务器和另外一个服务器之间的通信,RPC和ajax的相同点是:1、都是两个计算机之间的网络通信,2、都需要双方约定一个数据格式
RPC和ajax的不同点是:
1、ajax使用DNS作为选址服务,但是RPC不一定使用DNS作为寻址服务(RPC通信一般是在内网进行请求,使用DNS作为选址服务有点浪费)。
2、RPC应用层协议一般不使用HTTP,RPC通信的时候一般使用二进制协议(更小的数据包体积,更快的编解码速率,性能上有优势)。
3、RPC通信基于TCP或UDP协议
Buffer
JavaScript语言自身只有字符串数据类型,没有二进制数据类型。但在处理像TCP流或文件流时,必须使用到二进制数据。Buffer是node.js提供的用来创建一个专门存放二进制数据的缓存区。在 Node.js 中,Buffer 类是随 Node 内核一起发布的核心库。Buffer 库为 Node.js 带来了一种存储原始数据的方法,可以让 Node.js 处理二进制数据,每当需要在 Node.js 中处理I/O操作中移动的数据时,就有可能使用 Buffer 库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。
//Buffer.from方法表示从一个现有的数据结构创建缓存
var buffer1 = Buffer.from('Ashin');
var buffer2 = Buffer.from([1,2,3,4]);
//Buffer.alloc方法表示创建一个固定长度的缓存
var buffer3 = Buffer.alloc(20);
//在buffer中写入数据
buffer2.writeInt8(12,1);//表示在buffer2的第二位上写入有符号8位整数,值为12,
buffer2.writeInt16BE(512,2)//表示在buffer2的第三位上写入有符号16位整数,值为512,其中BE表示高位在前面,地位在后面;同理LE表示地位在前面,高位在后面
复制代码
net建立RPC通道
net在node.js中提供和HTTP类似的服务,我们首先利用net创建客户端和服务端的单工通信:只能客户端向服务端发送数据,或者只能服务端向客户端发送数据。
//client.js代码
const net = require('net');
const socket = new net.Socket({});
//连接本地4000端口
socket.connect({
host: '127.0.0.1',
port: 4000
});
socket.wirte("good morning");//写入数据
复制代码
net调用createServer方法创建一个服务器,和HTTP不同的是,net里接收socket参数,socket在网络通信中代表着这个网络通路的写入和读取的代理对象。socket.on监听data,如果数据发生变化,立即执行回调函数,回调函数里的buffer就是上面讲的二进制流,通过toString()可以返回字符串。
//server.js代码
const net = require('net');
const server = net.createServer((socket) => {
socket.on('data', function (buffer) {
console.log(buffer,buffer.toString());
})
});
server.listen(4000);
复制代码
在此基础上,来实现半双工通信:客服端发送请求,服务端接受请求后返回数据,即同一时间只有一方在发送数据。比如说客户端发送一个课程ID到服务端,服务端通过课程ID将其对应的课程名称返回给客户端.
//client.js代码
const net = require('net');
const socket = new net.Socket({});
//连接本地4000端口
socket.connect({
host: '127.0.0.1',
port: 4000
});
const LESSON_IDS = [
"136797",
"136798",
"136799",
"136800"
];
const buffer = Buffer.alloc(4);
buffer.wirteInt32BE(
LESSON_IDS[ Math.floor( Matn.random() * LESSON_IDS.lenth )]
)
socket.wirte(buffer);//写入数据
socket.on('data', function (buffer) {
console.log(buffer.toString());
})
//server.js代码
const net = require('net');
const server = net.createServer((socket) => {
socket.on('data', function (buffer) {
const lessonId = buffer.readInt32BE();
setTimeout(()=>{
socket.write(Buffer.from(LESSON_DATA[lessonId]))
},500)
console.log(buffer,buffer.toString());
})
});
server.listen(4000);
// 假数据
const LESSON_DATA = {
136797: "01 | 课程介绍",
136798: "02 | 内容综述",
136799: "03 | Node.js是什么?",
136800: "04 | Node.js可以用来做什么?"
}
复制代码
半双工通信过程中,如果客户端接连发送多个请求到服务器,服务器经过计算后再返回两个返回报文,但却不能保证返回报文和请求报文的顺序是一一对应的。所以在半双工通信的基础上,给每个请求添加序号seq,同时返回的报文中也添加对应的seq序号,那么在客户端接收到多个请求之后可以根据请求序号来区分每个返回报文对应的请求,进而实现全双工通信。
//client.js代码
const net = require('net');
const socket = new net.Socket({});
socket.connect({
host: '127.0.0.1',
port: 4000
});
const LESSON_IDS = [
"136797",
"136798",
"136799",
"136800"
];
const buffer = Buffer.alloc(4);
const id = Math.floor( Matn.random() * LESSON_IDS.lenth );
buffer.wirteInt32BE(
LESSON_IDS[ id ]
)
socket.wirte(buffer);//写入数据
socket.on('data', function (buffer) {
console.log(buffer.toString());
})
let seq = 0;
function encode(index) {
const seqBuffer = buffer.slice(0,2);
cosnt titleBuffer = buffer.slice(2);
console.log(seqBuffer.readreadInt32BE(), titleBuffer.toString())
const buffer = Buffer.alloc(6);
buffer.writeInt16BE(seq++)
buffer.writeInt32BE(LESSON_IDS[index], 2);
return buffer;
}
socket.wirte(encode(id));//写入数据
setInternal(()=>{
id = Math.floor( Matn.random() * LESSON_IDS.lenth );
socket.wirte(encode(id));
},50)//每50ms发送一次请求
//server.js代码
const net = require('net');
const server = net.createServer((socket) => {
socket.on('data', function (buffer) {
const seqBuffer = buffer.slice(0,2);
const lessonId = buffer.readInt32BE(2);
setTimeout(()=>{
const buffer = Buffer.concat([seqBuffer,Buffer.from(LESSON_DATA[lessonId])]);
socket.write(buffer)
}, 10 * Matn.random() );//随机在1s内返回数据
console.log(buffer,buffer.toString());
})
});
server.listen(4000);
// 假数据
const LESSON_DATA = {
136797: "01 | 课程介绍",
136798: "02 | 内容综述",
136799: "03 | Node.js是什么?",
136800: "04 | Node.js可以用来做什么?"
}
复制代码