事件驱动
-
NodeJs
是单线程、非阻塞I/O的事件驱动;- 不同于
Java/PHP/.net
等服务器语言,NodeJs
不会为每个Client
连接创建新线程; - 当有新的
Client
请求连接时会触发内部事件,通过非阻塞I/O
、事件驱动机制,让Node
应用程序在宏观上是并行的; - 使用
Node.js
,一个8G
的服务器可以同时处理超过4w
的Client
连接。
- 不同于
- 处理异步的两种常用方式:回调函数、事件的订阅/发布
- 事件是整个
Node
的核心,Node
中大部分模块都使用/继承events
模块,类似WebAPI
的EventTarget
-
node
开发的监控工具nodemon
,热启动npm i nodemon -g // 启动程序 nodemon ./index.js
- 并发处理的发展及其代表
- 多进程:
LinuxC Apache
- 多线程:
Java
- 异步IO:
JavaScript
- 协程:
lua openresty go Deno TS
- 多进程:
events模块
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
- 通过
EventEmitter
实例来绑定和监听事件,以广播(订阅/发布)的形式处理异步; - 订阅事件的方式
emitter.on('toparent', (data) => { // 方式一:订阅事件'toparent' console.log(data) // 处理广播数据data }) emitter.addListener('toparent', (data) => { // 方式二 console.log(data); })
emitter.once(event, callback)
表示只注册一次 - 发布事件:
emitter.emit('toparent', '广播数据')
-
emitter.setMaxListeners(2);
设置最大的订阅个数
Process
-
process
对象是一个全局变量,提供了当前Node
程序与系统的有关信息,还可以控制当前的Node
进程 -
process.argv
获取运行node
程序的命令,是一个数组node app.js -i --> ['node命令的路径', 'app.js的路径', '-i']
-
process.evn
环境变量相关,比如通过配置环境变量控制开发模式与生产模式的切换- 在当前系统上新建一个环境变量:
mode='dev'
- 判断当前是否处于开发模式:
process.env.mode == 'dev'
- 在当前系统上新建一个环境变量:
-
process.exit()
结束当前进程 -
process.stdout/stdin
标准输入输出流- 将数据输出到终端:
process.stdout.write('hello'); --> console.log 的底层原理
- 监听用户的输入:
process.stdin.on('data', e=>{ # 可用于实现交互式的命令行,比如Vue.js的脚手架 process.stdout.write('用户输入: ', e.toString()); })
- 在命令行输入内容后,还要点击
Enter
键作为输入完成的信号,process.stdin
才能监听到,所以接收到的数据其实是以回车符号结尾的; - 去除
Windows
系统的回车符:e.toString().replace('\r\n', '');
- 将数据输出到终端:
- 移动光标的位置:
process.stdout.cursorTo(x, y);
os
os
操作系统相关的模块
-
os.endianness():
CPU
的字节序,可能的是BE
或LE
; -
os.hostname():
操作系统的主机名; -
os.type():
操作系统名; -
os.platform():
编译时的操作系统名; -
os.arch():
操作系统CPU
架构,x64、arm、ia32
; -
os.release():
操作系统的发行版本; -
os.uptime():
操作系统运行的时间,以秒为单位; -
os.totalmem():
系统内存总量,单位为字节; -
os.freemem():
操作系统空闲内存量,单位是字节; -
os.networkInterfaces():
获取网络接口列表; -
os.cpus():
返回一个对象数组,包含所安装的每个 CPU/内核的信息。
stream
- 流是一种在
Node
中处理流式数据的抽象接口,fs、net、http、https
等模块都提供了流的实现; -
stream
约定了一些基本特性(但并没有实现),所有实现流操作的对象都具备这些共同的特性; - 流的基本类型
-
Writable:
可写入数据的流,如fs.createWriteStream()
-
Readable:
可读取数据流,如fs.createReadStream()
-
Duplex:
可读又可写的流,又称为双工数据流 -
Transform:
可修改的双工数据流,在读写过程中可修改/转换
-
Writable:write()、end()、setDefaultEncoding()
Readable:setEncoding()、read()、pipe()、pause()、resume()
Buffer
-
Buffer:
缓冲区,类似于数组,长度固定,专门用于操作二进制数据
v8
是JS
引擎,内存有限,32
位操作系统约0.7G
,64
位约为1.4G
,Node
使用的也是v8
引擎。虽然v8
有内存限制,但Buffer
实际上是对底层内存的直接操作,它的大小不计入v8
的内存开销;
-
let buf = new Buffer(10); --> 10
个字节的Buffer
,但Buffer
的所有构造函数已废弃。- 虽然
Buffer
存储的是二进制数据,但显示时都是以十六进制的形式(二进制太长了); -
Buffer
操作的是底层内存,大小一旦确定,就会分配一段连续的内存空间,不允许修改大小;buf[0] = 88 // 十进制转为十六进制:58,也可以直接赋值十六进制 buf[11] = 255 // 角标越界不会报错,但不会有变化
-
Buffer
中每个元素的范围:00-ff
,即十进制的0-255
,二进制的一个字节;buf[2] = 556 // 超过一个字节的最大数值255,会舍弃高位,只存储低8位:2c
- 控制台默认打印的数字都是十进制,
buf[0].toString(16)
表示以十六进制输出; -
buf.tostring():
将Buffer
中的二进制数据转为字符串。
- 虽然
-
Buffer.alloc(size[, fill, encoding]):
创建size
个字节的Buffer
对象-
fill
和encoding
都是可选参数,encoding
默认为utf8
- 不指定
fill
时,新创建的Buffer
默认用0
填充。
-
-
Buffer.allocUnsafe(size):
不安全的Buffer
,Buffer
中可能含有敏感数据;-
Buffer.alloc()
的默认值都是00
,也即分配内存时会清空该内存中的原数据; -
Buffer.allocUnsafe()
在分配内存时不会清空内存中原数据,所以默认值可能不是00
;
-
-
Buffer.from(str|array|buffer):
将字符串/数组/buffer
数据转化为一个新的Buffer`-
var str="Hello"; --> str.length
字符串长度 -
var buf = Buffer.from(str); --> buf.length
占用内存的字节大小 - 英文字符串的每个字符占一个字节,所以
str.length==buf.length
- 查看字符串的字节长度:
Buffer.byteLength('你好');
-
-
Buffer.concat(array):
合并array
中的Buffer
,再转为一个新的Buffer
- 比较两个
Buffer
的值是否完全相等:buf1.equals(buf2);
- ·Buffer·对象支持的编码格式非常有限,
ascii、utf8、utf16le、ucs2、base64、binary、hex
,不支持GBK
。-
fs
模块读取一个GBK
编码的文件时,会出现报错信息fs.readFile(path, 'gbk', (err, data) => { // err: Unknown encoding: gbk // 不过不指定编码,默认是utf8编码,data.toString()得到的是乱码 })
-
iconv-lite
用来帮助解决编码转换问题;用于在node
中处理各种操作系统中出现的各种奇特编码,它不提供读写文件的操作,只提供文件编码转换的功能。const iconv = require('iconv-lite') // 坚持当前系统是否支持某种编码 iconv.encodingExists("us-ascii") fs.readFile(path, (err, data) => { // 以GBK编码的形式解码成一个默认为utf8编码格式的字符串 const gbkData = iconv.decode(data, 'gbk') gbkData.toString() // 得到字符串内容 })
-
fs
-
fs:
文件系统,node
的核心模块,var fs = require("fs");
-
fs
模块中的所有操作都可以选择同步/异步,同步会阻塞程序的执行,而异步则不会; - 在
fs
中,带有Sync
的都是同步方法,不带的都是异步方法,且异步方法带有回调函数; - 异步的错误在回调函数中,而同步的错误只能用
try-catch
捕获。
-
-
open()/openSync():
打开文件,返回一个文件的描述符,通过该描述符进行文件操作;fs.openSync(path, flags, mode); fs.open(path, flags, mode, callback);
-
path:
文件路径; flags:操作类型r/w; mode:设置文件的操作权限,一般不传; -
callback
回调2
个参数arguments:function(err, fd){ ... }
-
err:
错误对象,没有错误则为null
;fd:
文件描述符; -
Node
的设计就是错误优先,所以回调函数的第一个参数是错误对象。
-
write()/writeSync():
向文件中写入内容;fs.writeSync(fd, string, position, encoding); fs.write(fd, string, position, encoding, callback);
-
fd:
文件描述符;string:
写入的内容,如果不是字符串,则被强制转为字符串; -
position:
写入指针的位置,默认为0
;encoding:
写入的编码,默认为utf-8
; -
callback
回调3
个参数:function(err, written, string){ ... }
,其中written
是传入的字符串被写入的字节数。
-
-
close(fd, callback)/closeSync(fd):
关闭文件操作;-
fd:
文件描述符,表示要关闭的文件操作; -
callback
只回调一个参数:function(err){ ... }
-
-
fs
的写入过程:数据-->Buffer-->stream
,为了提高效率,数据先写入缓冲区,再一次性写入文件.
简单读写
- 简单文件写入:
fs.writeFile()/fs.writeFileSync()
,原理仍是write()/writeSync()
writeFile(file, data, options, callback)
-
file:
操作的文件路径。如果是绝对路径,下面两种方式是等效的。fs.writeFile("C:/workplace/test.txt", ...); fs.writeFile("C:\\workplace\\test.txt", ...);
-
data:
待写入的数据,可以是字符串或Buffer
,如果是buffer
,options
中的encoding
是无效的; -
options:
可选对象,设置写入动作,包括encoding(默认utf-8),mode(默认0o66),flag(默认w)
,{ flag: 'a' }
表示向文件中追加写入; -
callback
回调函数,不用手动关闭操作流。
-
- 流式文件写入:用于写入大文件,可以分多次写入文件。同步/异步/简单文件写入都不适合大文件的写入,性能较差,容易导致内存溢出;
事件注册var ws = fs.createWriteStream(path, options); // 创建一个写入流; ws.write(str); // 写入数据 ws.end(); // 等待写入完成再关闭流,不能用ws.close(),会造成流中的数据丢失;
-
ws.on("事件名", function)
绑定一个长期有效的事件; -
ws.once("事件名", function)
绑定一个一次性的事件,触发一次之后自动失效;
ws.once("open", function(){ // 监听写入流的打开事件 console.log("流打开了...") }); ws.once("close", function(){ // 监听写入流的关闭事件 console.log("流关闭了...") });
-
- 简单文件读取:
fs.readFile()/fs.readFileSync()
fs.readFile(path, options, callback)
-
path
表示读取文件的路径; -
callback:function(err, data)
回调的data
是一个Buffer
对象,因为读取到的内容可能是二进制文件,如图片、音频。data.toString()
只适用于字符串,对二进制数据会乱码。
fs.readFile("an.jpg", function(err, data){ if(!err) { fs.writeFile("pn.jpg", data, function(err){ ... }); } });
-
- 流式文件读取:用于读取大文件,可以分多次读取文件
读取一个可读流中的数据,必须为可读流绑定一个var rs = fs.createReadStream(path, options); // 创建一个读取流;
data
事件,绑定后会自动开始读取;rs.on("data", function(data){ // 读取过程不是一次性事件 // data也是一个Buffer对象 });
- 管道:把读取流的内容 通过管道 传递给写入流,并写入本地。
这种管道的方式在读/写大文件的过程中,会非常有效率!var rs = fs.createReadStream('./avatar.png'); var ws = fs.createWriteStream('./avatar-bak.png'); rs.pipe(ws);
常用操作
-
fs.exists(path, callback)/existsSync(path):
文件/目录是否存在。已被fs.stat()/fs.access()
替代。 - ·fs.stat(path, callback)/statSync(path):·获取文件信息,类似文件/目录的属性信息
-
callback:function(err, stats) --> stats
对象中保存了文件的状态信息 -
stats.size:
获取文件的大小; -
stats.isFile()/isDirectory():
是否是一个文件/文件夹。
-
-
fs.unlink(path, callback)/unlinkSync(path):
删除文件; -
fs.readdir(path, options, callback)/readdirSync(path, options)
- 读取一个目录的结构,获取指定目录下的所有文件/文件夹;
-
callback:function(err, files) --> files
是一个数组,文件/文件夹的名称。
-
fs.truncate(path, len, callback)/truncateSync(path, len)
截断文件,将文件修改为指定的大小,len表示字节大小,将文件大小设置为len
-
fs.mkdir(path, mode, callback)/mkdirSync(path, mode):
创建目录,但不会递归创建 -
fs.rmdir(path, callback)/rmdirSync(path):
删除目录,但不能删除非空目录 -
fs.rename(old, new, callback):
重命名/剪切 -
fs.watchFile(filename, options, listener):
监视文件的修改- 内部原理是一个定时机制,定时检查文件中的内容;
-
options:{ persistent: true, interval: 5007 },
默认5s
检查一次;
fs.watchFile("test.txt", {interval:1000}, function(curr, prev) { //curr是修改后的状态,prev是修改前的状态,它们都是stats对象 });
-
fs.watch():
可以监听目录的状态变化。
fs Promise
在 node10.0
之后,fs
模块中引入了Promise
,文件操作不再区分异步和同步,而是返回Promise
对象
const fsPromises = require('fs').promises;
UDP之dgram
-
dgram
模块:提供UDP
数据包socket
的实现,const dgram = require('dgram');
- 服务端
- 创建
socket
对象const server = dgram.createSocket(); // 静态方法创建socket对象 const server = new dgram.Socket(type[, callback]);
-
type:udp4 =>IPV4、udp6 =>IPV6; callback:
接收到数据的回调; - 绑定
IP
和端口号server.bind(3000, '127.0.0.1');
- 监听事件:
close(关闭)、error(发生错误)、listening(启动监听)、message(收到消息)
server.on('listening', () => { console.log('启动成功...'); }); server.on('message', (data) => { //网络传输的数据是二进制的,在node上是一个buffer对象 });
- 关闭
socket
server.close(callback);
- 创建
- 客户端
let client = dgram.createSocket('udp4'); // 发送数据: client.send('Hello Server', 3000, '127.0.0.1');
TCP之net
-
net
模块:提供了创建基于流的TCP/IPC
服务器和客户端的异步网络API
const net = require('net');
- 服务端
let server = new net.Server(); / net.createServer(); // 启动监听 server.listen(3000, '127.0.0.1'); // IP默认为0.0.0.0 server.listen(3000, '0.0.0.0') // 监听当前设备上的所有IP收到的3000端口消息
- 一台电脑可能有多个网卡(对应多个
IP
),但端口号是唯一的,0.0.0.0
类似于通配符*
- 连接事件、发送数据
server.on('connect', (socket) => { // 回调的socket表示当前客户端的socket对象 console.log('有客户端连接...'); socket.write('hello client'); // 向客户端写(发送)数据 socket.on('data', (data) => { // 监听客户端发来的数据 // ... }); });
- 获取客户端的
IP
和端口号:socket.remoteAddress,socket.remotePort
- 一台电脑可能有多个网卡(对应多个
- 客户端
// 创建客户端:new net.Socket(); / net.createConnection();
let client = net.createConnection(3000, '127.0.0.1');
client.on('data', data => { // 监听服务器发来的数据
// ...
});
client.write('hello server'); // 向服务器端发送数据
-
socket
数据的分包- 当一个数据包很大时,并不会一次性传输,而是分多次,也即
data
事件会被触发多次; - 服务端告诉客户端数据传输完成:
socket.end();
server.on('connect', (socket) => { let data = fs.readFileSync('./a.mp4'); // 读取一个大文件 socket.write(data); // 开始传输,socket内部会分包传输 socket.end(); // 通知客户端传输完成 })
-
socket.end();
一旦执行,本次连接也会终止,客户端断开连接; - 客户端收到数据传输完成的事件:
client.on('end', () => { ... });
- 客户端需要把每次收到的数据(
buffer
),拼接成一个buffer
对象,才是一个完整的数据包。let list = []; client.on('data', chunk => { list.push(chunk); }); client.on('end', () => { // 数据传输完成,连接已断开 let data = Buffer.concat(list); // 把多个buffer对象合并为一个buffer fs.writeFile('./b.mp4', data, err => {}); });
- 当一个数据包很大时,并不会一次性传输,而是分多次,也即
网友评论