内置模块
我来讲一下内置模块,但我就讲一些常用的哈,不然难度很大,而且后期大多数都用的是第三方模块
http模块
我先讲这个模块,这是创建 web 服务器的模块,通过该模块提供的 http.createServer()
方法,就能很方便的把一台普通的电脑,摇身一变,成一台 Web 服务器,从而对外提供 Web 资源服务
没错,用这个模块,就可以,创建一个简单的服务器了,很简单吧
服务器和普通电脑的区别在于,服务器上安装了 web 服务器软件
该模块可以基于 node.js 提供的 http 模块,通过几行简单的代码,就能轻轻松松的手写一个服务器软件,从而对外提供 web 服务
我都说到这了,那我就简单的科普一下,服务器相关的概念吧,>0<,别跑
IP 地址
IP 地址就是互联网上每台计算机的唯一地址,所以 IP 地址 是具有唯一性的
IP 地址 的格式:通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d 都是 0~255 之间的十进制整数
实在不懂的话,就理解成 ip 地址是用来定位计算机的位置的,就行了,关于IP地址的演变等,这里就不细说了
域名和域名服务器
尽管 IP 地址 能够唯一的标记网络上的计算机,但 IP地址 是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名地址(Domain Name)
IP地址 和 域名 是一一对应的关系,这份对应关系存放在一种叫做域名服务器 (DNS,Domain name server) 的电脑中。使用者只需通过好记的域名访问对应的服务器即可使用,对应的转换工作由域名服务器实现。所以,域名服务器就是提供 IP 地址 和 域名 之间的转换服务的服务器而已
实在不懂的话,就可以理解成 域名服务器 是媒婆 ,她给 ip地址 和 域名 搭线架桥,结婚在社会层面的达成道德和法律的契约 ,相互绑定 ,这就好理解了吧 ,写这个教程好累 呼呼
端口号
在一台电脑中,可以运行成百上千个 web 服务,每个web 服务 都对应一个唯一的端口号,客户端发送过来的网络请求,通过端口号,可以被准确地交给对应的 web 服务 进行处理
端口号用来定位具体的应用程序,而端口号的范围是 0~65536 之间
在计算机中有一些常用默认的端口号,http 服务的 80 端口 ,https 服务的 443 端口等等
在实际开发过程中我么也只会用到一些常见的端口,例如:8080,3000,8088…
关键是这些端口号在日常开发中已经够用了
如果这都理解不了话,那还是别学了哈哈哈哈,劝退
如何简单创建一个 Web 服务器
关于监听事件我在本章下面讲的很清楚,所以不理解也很正常
1 | // 导入 http 模块 |
我们可以将写入服务器要处理的事情,分为以下几个步骤
- 提供服务:对数据的服务
- 客户端发来请求
- 服务端接收请求
- 服务端处理请求
- 给个反馈(发送响应)
- 注册 require 的事件,当客户端请求过来时,自动执行这个回调函数
- 回调函数接收两个参数:request,response
- request:请求对象,可以获取请求过来的路径信息
- response:响应对象,可以给客户发送响应数据
响应式数据的content-type
向请求的客户端发送响应头, 该函数在一个请求内最多只能调用一次,如果不调用,则会自动生成一个默认的响应头, 因为在实际开发中,我们需要返回对应的数据以及对应的的文本格式 所以我们需要设置对应的响应头,响应头决定了对应的返回数据的格式以及编码格式,如果在使用该模块时,不写的话,会导致中文内容出现乱码
原因是在服务端默认发送的数据是 utf-8 格式的数据,但是在浏览器中不知道发送过来的数据是utf8的数据,所以会按照当操作系统的默认编码区解析,最终导致造成乱码,在中文操作系统中默认解析方式是 gbk,解决方案是告诉浏览器,发送的是什么类型的数据,是 utf-8 的数据,通过设置发送数据头来告诉浏览器,发送的数据格式是什么格式的数据
声明文本格式的数据 res.Header("Content-Type","text/plain; charset=utf-8")
声明 html 格式的数据 res.setHeader("Content-Type", "text/html; charset=utf-8")
该格式识别图片 res.Header(‘Content-Type’:‘image/jpg;charset=UTF8’)
该格式识别css res.Header(‘Content-Type’:‘text/css;charset=UTF8’)
该格式json res.setHeader('content-type', 'application/json;charset=utf-8')
该格式js res.setHeader('content-type', 'application/javascript;charset=utf8')
res.writeHead( )
的使用方式和上面的类似,但我推荐这种的使用方式
1 | response.writeHead(statusCode, [reasonPhrase], [headers]) |
接收参数:
- 第一个是HTTP状态码,如200(请求成功),404(未找到)等
- 第二个是告诉浏览器发送的是什么数据类型
- 第三个就是具体发送的是什么数据
1 | // 该格式可以识别HTML结构,编码格式是UTF-8 |
我们举个例子
1 | const http = require("http") |
既然都学到这了,那我就来点难的吧
1 | //index.js |
这才刚刚开始而已,赶快进行下一个吧
1 | let http = require("http") |
url模块
这个模块主要是用来对网址进行处理和解析的实用工具
该模块提供了两种处理网址的API:一种是基于 node.js 特定的旧版的 API ,另一个是基于 WHATWG 网址标准 的新版的 API,也就是新老API之间的自家竞争
在讲模块方法之前,我们先来比较一下,这两者之间的区别
新老版本之间的解析网址字符串
WHATWG 的 API 比 传统 的根据安全性,所以我更推荐前者
使用 WHATWG API
1 | const Url = new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash") |
使用旧版 API
1 | const Url = require("url") |
WHATWG 网址 API
URL 类
new URL(input[, base])
这个是用于实例化 URL 对象的,也就是将传入的 URL 字符串解析成 URL 对象,所以连导入都不要导入,方便
参数
- input:表示要解析的绝对或相对的 URL。如果 input 是相对路径,则必填 base。 如果 input 是绝对路径,则可以忽略 base
- base:如果 input 不是绝对路径,则为要解析的基本 URL
1 | const Url = new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash") |
解析成功之后返回的URL对象,新旧方式返回的属性,略有不同
1 | // URL { |
URL类中的属性
属性 | 作用 |
---|---|
hash | 获取及设置 URL 的片段部分。即 # 符号之后的内容 |
host | 获取及设置 URL 的主机部分 |
hostname | 获取及设置 URL 的主机名部分,不包含端口 |
href | 获取及设置序列化的 URL,整个URL 字符串 |
origin | 获取只读的序列化的 URL 的 origin |
username | 获取及设置 URL 的用户名部分 |
password | 获取及设置 URL 的密码部分 |
pathname | 获取及设置 URL 的路径部分 |
port | 获取及设置 URL 的端口部分 |
protocol | 获取及设置 URL 的协议部分 |
search | 获取及设置 URL 的序列化查询部分 |
searchParams | 获取表示 URL 查询参数的 URLSearchParams 对象。该属性属于只读属性,但是可以通过 search 属性去修改 |
URL类中的方法
方法 | 作用 |
---|---|
url.toString() | 返回序列化的 URL,返回值与 url.href 和 url.toJSON() 的相同 |
url.toJSON() | 返回序列化的 URL。返回值与 url.href 和 url.toString() 的相同。使用JSON.stringify() 序列化时将自动调用该方法 |
1 | const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash") |
URLSearchParams类
URLSearchParams 类提供对 URL 查询部分的读写权限。这个类与 querystring 模块有相似的目的。这个类是专门为 URL 查询字符串而设计的
到坚持到这了,嘻嘻,加油
构造函数
这个类有四个构造函数,用于不同的情况
构造函数 | 作用 |
---|---|
new URLSearchParams() | 实例化一个新的空的 URLSearchParams 对象 |
new URLSearchParams(string) | 将 string 解析成一个查询字符串, 并且使用它来实例化一个新的 URLSearchParams 对象。 如果以 ‘?’ 开头,则忽略 |
new URLSearchParams(obj) | 通过使用查询 哈希映射 实例化一个新的 URLSearchParams 对象。 obj 的每一个属性的键和值都将被强制转换为字符串 |
new URLSearchParams(iterable) | 以一种类似于 Map 的构造函数的迭代映射方式实例化一个新的 URLSearchParams 对象 |
直接通过 URL 类的 searchParams 属性实例化 URLSearchParams 类
1 | const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash") |
直接通过 URLSearchParams 类的构造函数实例化
1 | const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash") |
方法
方法 | 作用 |
---|---|
append(name, value) | 在查询字符串中附加一个新的键值对 |
delete(name) | 删除所有键为name的键值对 |
entries() | 在查询中的每个键值对上返回一个迭代器 |
forEach(fn) | 在查询字符串中迭代每个键值对,并调用给定的函数 |
get(name) | 返回键是name的第一个键值对的值。如果没有对应的键值对,则返回null |
getAll(name) | 返回键是name的所有键值对的值,如果没有满足条件的键值对,则返回一个空的数组 |
has(name) | 如果存在至少一对键是 name 的键值对则返回 true |
keys() | 返回每一个键值对上的键 |
set(name, value) | 与 name 相对应的值设置为 value。如果已经存在键为 name 的键值对,则将第一对的值设为 value 并且删除其他对。 如果不存在,则将此键值对附加在查询字符串后 |
sort() | 按现有名称就地排列所有的名称-值对 |
toString() | 返回查询参数序列化后的字符串 |
values() | 返回每一个键值对上返回一个值 |
urlSearchParams[Symbol.iterator]() | 返回一个键值对形式迭代器 |
我们挑一些方法来用,>0<
1 | const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash") |
http模块补充
补充主要讲的是跨域
GET
我们抓取b站热门的数据
思路很简单,就是前端向node发送ajax请求,node客户端向b站抓取数据,然后返回node客户端 再转发到前端,很简单吧,我怕讲复杂了,你们不懂
话不多说,直接上代码
1 | //index.html |
POST
我们来抓取 小米有品 的数据,试试 POST
整体思路和上面的一模一样
话不多说,直接上代码
如果 https.request() 又不懂的话 ,可看 https 安全超文本传输协议
1 | //index.html |
events模块
这是典型的异步发布模式,由于 node.js 中不存在浏览器中冒泡,捕获这些行为,但 node.js 中实现了 events 模块,而且几乎所有常用的node模块都继承了events模块
而 events 只对外暴露一个对象,那就是 EventEmitter,而它只有两个作用,一个是事件的发射,一个是事件的监听
监听器函数(listeners) 可以添加给对象,对象发出事件时,对应的函数就会被执行。在监听器函数中,this引用的是它(监听器函数),但不要用箭头函数,这是人话吗
所有发出事件的对象都是 events.EventEmitter 的实例,万恶之源了属于是,我们可以通过 require(‘events’).EventEmitter 直接得到 EventEmitter 类,也可以先创建 require(‘events’) 的变量,再用这个变量创建 EventEmitter 实例 ,当 EventEmitter 对象遇到错误时,通常会触发 error 事件
什么是事件驱动
简单来说就是通过有效的方法来监听事件状态的变化,并在发生变化时做出相应的动作
添加监听器
为事件绑定事件处理程序,可以用 emitter.addListener(event,listener)
和 emitter.on(event,listener)
,它们作用完全一样,我推荐第二种方法
传入参数是事件(event)和处理函数(listener)
我们举个例子
1 | const http = require("http") |
众所周知,既然有监听器,那我就多讲它一点吧
只执行一次的监听器
使用 **emitter.once(event,listener)**
绑定的事件监听器只执行一次,然后就会被删除掉
这是一次性的,保护环境,节约资源,满分,没错我说的是反话
我们举个例子
1 | const http = require("http") |
现在轮到清除监听器了,说好听点叫移除,:( bushi
移除监听器
移除监听器使用 emitter.removeListener(event,listener)
该函数要放在要移除监听器的后面,不然清不掉
1 | const http = require("http") |
然而 错误的移除事件 不能移除匿名的事件处理程序,必须像上面一样移除具名函数
1 | const http = require("http") |
移除一个不过瘾,是不是,要移就将所有的监听器全部移除,玩的就是一个清净,人间清醒
移除所有监听器
移除所有监听器使用 emitter.removeAllListener([event])
需要传入某种类型的事件参数,不传的话会把所有类型的事件监听都移除掉,玩的就是西海岸哈哈哈哈
1 | const http = require("http") |
设置监听器最大绑定数
EventEmitter 支持多个事件监听器,默认最大值是10。也就是说可以在某个事件添加10个监听函数,默认情况下,超过10个就会发出警告提示
你想的没错有反转,是可以自定义的,嘿嘿嘿
可以通过 emitter.setMaxLisstener(n)
来设置同一事件的监听器最大绑定数,如果设置为0时,也就是无限制
自定义事件
从此开启自定义时代,人人皆是开发者
我们可以使用 emitter.emit(event,[arg1],[arg2],[...])
触发自定义的事件,放心没有忌口
1 | const http = require("http") |
查看事件绑定的监听器的数量
如果我们想查看事件绑定的监听器的数量呢
没错我们有现成的方法,哈哈哈
可以使用 EventEmitter.listenerCount(emitter,event)
查看事件监听器的数量,也可以用 emitter.listeners('event').length
来查看哦
推荐第二个,第一个用点老了,不好用,旧的不去,新的不来,(bushi
1 | const http = require("http") |
关于排错
我们用一个很简单的例子来理解吧
1 | server.on("request", (req, res) => { |
第一个在 res.write() 后调用了 res.end() 结束了写入流,所以再次调用 res.write(‘2222’) 就会报错,可以注释掉第一个 res.end() 调用,就可以正常使用了
我们一不做而不息,我们来看看这个模块的设置吧
查询已注册监听器的事件名称
如果我们想知道已注册监听器的事件的名称,以数组的形式返回
我们可以使用 emitter.eventNames()
方法来调用它
1 | const event = require("events") |
关于这个模块已经讲的差不多了,我不想再讲了
在讲 fs 模块之前,我们先讲一下 Buffer 缓冲区这个概念,有助于理解 fs 模块
Buffer缓冲区
什么是 Buffer 缓冲区
在 node.js 应用中,需要处理网络协议、操作数据库、处理图片、接收上传文件等,在网络流和文件的操作中,要处理大量的二进制数据,而 Buffer 就是在内存中开辟一片区域(初次初始化为8KB),用来存放二进制数据,可以理解成就是一个临时的容器
如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,如果数据到达的速度比进程消耗的数据慢,那么早先到达的数据需要等待一定量的数据到达之后才能被处理
这里的等待区也就指的是缓冲区,通常位于计算机的 RAM 中
简单来讲,node.js 不能控制数据传输的速度和到达时间,但可以决定何时发送数据,如果还没到发送时间,则将数据放在 Buffer 中,也就是在RAM中,直至将它们发送完毕
特征
- Buffer 的结构与数组类似,操作方法也与数组类似
- 数组不能存储二进制文件,而 Buffer 是专门存储二进制数据的存在
- Buffer 存储的是二进制数据,显示是以 16 进制的形式呈现
- Buffer 每一个元素范围是 00
ff,即 0255、00000000~11111111 - 每一个元素占用一个字节内存
- Buffer 是对底层内存的直接操作,因此大小一旦确定就不能修改
使用方式
Buffer 类在全局作用域中,无须 require 导入哦
创建 Buffer 的方法有很多种,我们讲讲下面的几种常见的形式:
方法 | 作用 |
---|---|
Buffer.from(str[, encoding]) | 将一个字符串转换为 Buffer |
Buffer.alloc(size) | 创建指定大小的 Buffer |
Buffer.alloUnsafe(size) | 创建指定大小的 Buffer,可能包含敏感数据(分配内存时不会清除内存残留的数据) |
buf.toString() | 将 Buffer 数据转为字符串 |
Buffer.from()
1 | let ba = Buffer.from("10") |
Buffer.alloc()
1 | const bAlloc1 = Buffer.alloc(10)// 创建一个大小为 10 个字节的缓冲区 |
在上面创建buffer后,则能够以 toString() 的形式进行交互,默认情况下采取 utf8 字符编码形式
1 | const buffer = Buffer.from("你好") |
如果编码与解码不是相同的格式则会出现乱码的情况
1 | const buffer = Buffer.from("你好","utf-8 ") |
当设定的范围导致字符串被截断的时候,也会存在乱码情况
1 | const buf = Buffer.from('Node.js 技术栈', 'UTF-8') |
应用场景
及然知道怎么用了,那我们就要知道在那个时候会用到它吧
主要的应用场景在
- I/O操作
- 加密解密
- zlib.js
I/O操作
通过流的形式,将一个文件的内容读取到另外一个文件
1 | const fs = require('fs') |
加解密
在一些加解密算法中会遇到使用 Buffer,例如 crypto.createCipheriv( )
的第二个参数 key 为 string 或 Buffer 类型
zlib.js
zlib.js 为 node.js 的核心库之一,其利用了缓冲区(Buffer)的功能来操作二进制数据流,提供了压缩或解压功能
剩下的内容,就请勇士你自己去探索吧,一起冒险吧
fs模块
我现在不就开始讲了吗
node.js 文件系统(fs模块)中的方法均有异步和同步两个版本,例如 读取文件内容的函数有异步的 fs.readFile()
和同步的 fs.readFileSync()
异步的方法函数最后一个参数为回调函数,回调函数的第一个 参数包含了错误信息(error)
建议使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞,实际上用异步的情况也比较多
说白了,就是一个管理文件的模块而已>0>
我在下面只介绍一些常用的API
文件
我们先从如何操控文件开始吧
打开文件
异步模式下打开文件的语法
1 | fs.open(path, flags[, mode], callback) |
path
文件的路径;
flags
文件打开的行为;
mode
设置文件模式(权限),文件创建默认的权限为 0666(可读可写);
callback
回调函数,两个参数 callback(err, fd)
;
flags 参数可以是以下值
我们举个例子
1 | let fs = require("fs") |
获取文件信息
异步模式获取文件大小、时间等信息的语法
1 | fs.stat(path, callback) |
path
文件路径;
callback
回调函数,带有两个参数 (err, stats) ,stats 是 fs.tats 对象;
当fs.stat(path)
执行后,会将 stats 类的实例返回给其回调函数,通过 stats 类中的提供方法判断文件的相关属性;
1 | let fs = require("fs") |
stats类中的方法有
最常用的也就前两个了
方法 | 作用 |
---|---|
stats.isFile() | 如果是文件返回 true,否则返回 false |
stats.isDirectory() | 如果是目录返回 true,否则返回 false |
stats.isBlockDevice() | 如果是块设备返回 true,否则返回 false |
stats.isCharacterDevice() | 如果是字符设备返回 true,否则返回 false |
stats.isSymbolicLink() | 如果是软链接返回 true,否则返回 false |
stats.isFIFO() | 如果是FIFO,返回true,否则返回 false。FIFO是UNIX中的一种特殊类型的命令管道 |
stats.isSocket() | 如果是 Socket 返回 true,否则返回 false |
写入文件内容
如果目标文件不存在, node.js 会自动创建该文件然后进行写入
覆盖式写入
异步模式下写入文件的语法
1 | fs.writeFile(file, data[, options], callback) |
fs.writeFile()
直接打开文件默认是 w 模式,所以如果文件存在,该方法写入的内容会覆盖旧的文件内容,也可以改变默认模式file
文件名路径或文件描述符
data
要写入文件的数据,可以是 String 字符串或 Buffer 缓冲对象
options
参数是一个对象 {encoding, mode, flag},默认编码 utf8,模式0666,flag为 w
实例
1 | let fs = require("fs") |
追加式写入
异步模式下写入文件的语法
1 | fs.appendFile(file, data[, options], callback) |
和上面类似,但是在文件原内容的基础上,添加内容数据,注意是在原内容的后面添加的
实例
1 | let fs = require("fs") |
读取文件内容
简单文件读取
1 | fs.readFile(path[, options], callback) |
path
:文件路径
options
:配置选项,若是字符串则指定编码格式
encoding
:编码格式flag
:打开方式
callback
:回调函数
err
:错误信息data
:读取的数据,如果未指定编码格式则返回一个 Buffer
1 | let fs = require("fs") |
常用文件方式读取
异步模式下读取文件的语法,该方法使用了文件描述符来读取文件
1 | fs.read(fd, buffer, offset, length, position, callback) |
fd
通过 fs.open()
方法返回的文件描述符;
buffer
数据写入的缓冲区;
length
要从文件中读取的字节数;
position
文件读取的起始位置;
callback
回调函数,三个参数,err 错误信息,bytesRead 读取的字节数,buffer 缓冲区对象
1 | let fs = require("fs") |
流式文件读取
- 简单文件读取的方式会一次性读取文件内容到内存中,但文件较大时,会占用过多内存影响系统性能,且读取速度慢
- 大文件适合用流式文件读取,它会分多次将文件读取到内存中
1 | let fs = require("fs") |
简洁的写法
1 | let fs = require("fs") |
关闭文件
异步模式下关闭文件的语法,使用了文件描述符来读取文件
1 | fs.close(fd, callback) |
fd
通过 fs.open()
方法返回的文件描述符;
callback
回调函数,没有参数;
1 | let fs = require("fs") |
截取文件
异步模式下截取文件的语法格式,使用文件描述符来读取文件
1 | fs.ftruncate(fd, len, callback) |
fd
通过 fs.open()
方法返回的文件描述符;
len
文件内容截取的长度;
callback
回调函数,没有参数;
1 | let fs = require("fs") |
删除文件
1 | fs.unlink(path, callback) |
path
文件路径;
callback
回调函数,没有参数;
1 | let fs = require("fs") |
检查文件是否存在
如果只是为了检查文件是否存在,但没有更多的操作,建议使用 fs.access()
不建议在调用 fs.open() 、fs.readFile()、fs.writeFile() 之前使用 fs.stat() 检查文件的存在性,而是应该直接地打开、读取或写入文件
不建议在调用 fs.open() 、fs.readFile()、fs.writeFile() 之前使用 fs.access() 检查文件的可访问性,这样做会引入竞态条件,因为其他进程可能会在两个调用之间更改文件的状态,而是应该直接地打开、读取或写入文件
1 | let fs = require("fs") |
重命名文件
1 | fs.rename(oldPath, newPath, callback) |
异步的把 oldPath
文件重命名为 newPath
提供的路径名,如果 newPath
已存在,则覆盖它
1 | let fs = require("fs") |
目录
我们现在开始讲文件目录相关的操作
创建目录
1 | fs.mkdir(path[, options], callback) |
path
文件路径;
options
参数一 recursive
是否以递归的方式创建目录,默认是 false,参数二 mode
设置目录权限,默认为 0777;
callback
回调函数,没有参数;
1 | let fs = require("fs") |
1 | let fs = require("fs") |
读取目录
1 | fs.readdir(path, callback) |
path
文件路径;
callback
回调函数,回调函数有两个参数 err, files err 为错误信息, files 为目录下的文件数组列表;
1 | let fs = require("fs") |
删除目录
1 | fs.rmdir(path, callback) |
path
文件路径;
callback
回调函数,没有参数
1 | let fs = require("fs") |
关于fs模块,我就讲到这了,下面的就靠你自己了,冲冲冲
路径动态拼接问题 __dirname
我们在使用 fs 模块操作文件时,如果提供的操作路径是以 ./
或 ../
开头的相对路径时,容易出现路径动态拼接错误的问题
原因:代码在运行的时候,会以执行 node.js 命令时所处的目录,动态拼接出被操作文件的完整路径
解决方案:在使用 fs 模块操作文件时,直接提供完整的路径,从而防止路径动态拼接的问题
__dirname
是用来提供动态的获取当前文件所属目录的绝对路径
1 | fs.readFile(__dirname + '/files/1.txt', 'utf8', function(err, data) { |
zlib模块
这个模块可以很方便的实现文件的压缩和解压,常用于服务端的 gzip 压缩,用来提高网页加载速度,也就是压缩原有文件体积,加快文件访问速度
浏览器通过 HTTP 请求头部里加上 Accept-Encoding ,告诉服务器,“你可以用 gzip,或者 defalte 算法来进行压缩资源”
这个模块,我们只需要知道,因为我们以后常用的还是第三方模块来压缩文件和解压,所以只需了解即可
文件的压缩和解压的实现
我们简单来对文件的压缩和解压进行的实现和了解
1 | const zlib = require("zlib") |
在执行压缩操作时,会生成txt.gz文件,在执行解压操作时,会生成demo,txt文件
我顺便讲一下,这里的相关知识点,虽然偏题但有点重要
pipline
stream.pipeline() 方法,是用来在流和生成器之间进行管道转发错误并且正确清理并在管道完成时提供回调
很难理解是不是,熬一熬就懂了,慢慢来
管道是一种机制,也就是将一个流的输出作为另一流的输入。它通常用于从一个流中获取数据并将该流的输出传递到另外的流。管道操作没有限制,换句话说,管道就是用于分步骤处理流的数据,也就是程序流
在进行文件压缩的时候使用 stream.pipeline() 提供一个可以完成数据流处理的管道,管道内可以传输多个流,管道任务结束后提供回调,简直美滋滋
语法
1 | stream.pipeline(source[, ...transforms], destination, callback) |
参数
source
:可读流...tranforms
:双工流(同时实现 Readable 和 Writable 接口的流)destination
:可写流callback
:管道完成时的回调
pipe
readable.pipe() 方法,可以将可写流绑定到可读流中,使其自动切换到流动模式并将其所有数据推送到绑定的可写流。 也就是说,pipe 方法的主要用途是从可读流中读取数据写入可写流,做的是流类型转换的事情
语法
1 | readable.pipe(destination[, options]) |
可以看官方的示例,简单易懂,将 readable 中的所有数据通过管道传输到名为 file.txt 的文件中
1 | const fs = require('fs') |
也可以将多个 Writable 流绑定到单个 Readable 流
readable.pipe() 方法返回对目标流的引用,从而可以建立管道流链
1 | const fs = require('fs') |
什么是stream流
到底什么是流
我们现在为上面的内容来填坑
流是用于在 node.js 中处理流数据的抽象接口, stream 模块提供了用于实现流接口的 API
流可以是可读的、可写的、或两者兼而有之,所有的流都是 EventEmitter
的实例,它才是老大
stream(流)是一种抽象的数据结构。就像数组或字符串一样,流是一堆数据的集合
但不同的是,流可以每次输出少量的数据,而且它不用存在于内存之中
比如,对服务器发起 http 请求的 request/response 对象就是 Stream流
使用流可以将文件资源拆分成小块进行处理,资源就像水流一样进行传输,减轻服务器压力,我直呼高啊
这样就是我为什么在写fs的时候,写了流式文件读取,这叫什么,这叫预判先知,这波大气层
两个流用一个管道相连,stream1 的末尾可以连接上 stream2 的开端
我的重点目标不过是你的起点目标而已,泪目
只要 stream1 有数据,就会流到 stream2
1 | let demo = fs.createReadStream("./demo.txt") |
demo是一个文件流,下面的 demo 就是我们的 http 流 res。 本来这两个流压根是没有关系的,现在我们想把文件流的数据传递给 http 流中。 很简单,用 pipe 连接就行啦,促成良缘,哈哈哈
管道原理
管道也可以认为是两个事件的封装
- 监听 data 事件,当 stream1 一有数据就塞给 stream2
- 监听 end 事件,当 stream1 停了,就停掉 stream2
1 | stream1.on("data",(chunk)=>{ |
都谈到这了,你们肯定没意见多看一会,对吧
Stream 分类
名称 | 特点 |
---|---|
Readable | 可读 |
Writable | 可写 |
Duplex | 可读可写(双向) |
Transform | 可读可写(变化) |
我们对可读和可写很好理解,但双向的怎么理解呢
Duplex 可以读写,但是读的内容和写的内容是相互独立的,没有交叉,读写的文件相互分离,而 Transform 是自己写自己读,读写的文件是一体的,很好理解吧
可读流有两种状态
- 静止态 paused
- 流动态 flowing
我们可以将可读流看成一家实体企业,节假日停止运作时就是静态的,工作日运作时就是流动态
- 可读流默认是处于 paused 态的
- 一旦添加 data 事件监听,它就变为 flowing 态
- 删掉 data 事件监听,又会变成 paused 态
- 而使用 pause() 可以将它变为 paused 态
- 而使用 resume() 可以将它变为 flowing 态
1 | const http = require("http") |
finish事件
在调用 stream.end()
之后,而且缓冲区数据都已经传给底层系统之后,触发 finish 事件
我们往文件中写入数据时,不是直接存入硬盘中,而是先放入缓冲区。 当数据到达一定大小后,才会写入硬盘,我们不生产水我们只是大自然的搬运工
点到为止,我们要讲武德哈
node.js 中的 stream
可读流VS可写流
可读流 | 可写流 |
---|---|
HTTP Response 客户端 | HTTP Request 客户端 |
HTTP Request 服务端 | HTTP Response 服务端 |
fs read stream | fs write stream |
zlib stream | zlib stream |
TCP sockets | TCP sockets |
child process stdout & stderr | child process stdin |
process.stdin | process.stdout,process.stderr |
… | … |
你是不是有点看不懂,是不是,其实我也有点哈
压缩
使用 gzip 算法
gzip 是一种数据格式,默认且目前仅使用deflate算法压缩data部分
1 | let fs = require("fs") |
使用 deflate 算法
deflate 是同时使用了 LZ77算法 与 哈夫曼编码(Huffman Coding) 的一个无损数据压缩算法
Brotli 通过变种的 LZ77 算法、Huffman 编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比,它有着更高的压缩效率
这个不理解我也帮不了你
1 | let fs = require("fs") |
解压
使用 gunzip 算法
1 | let fs = require("fs") |
使用 inflate 算法
1 | let fs = require("fs") |
crypto模块
该模块的作用是为了提供通用的加密和哈希算法,我们用纯 js 代码实现这些功能不是不可能,但在速度上会非常慢, node.js 用 C/C++ 实现这些算法后,通过 crypto 模块暴露为 js 接口,这样不但方便而且速度还快
MD5 是一种常用的哈希算法,用于给任意数据一个“签名”,通常用一个十六进制的字符串表示
算了都讲到这了,那我就浅讲一下算法吧,先来一点算法知识吧
什么是加密
在计算机领域,由于没有100%安全的系统,为了防御黑客攻击,往往最常用的防御手段,就是运用密码学,没错就是给数据进行加密
密码学这一词汇,就是从 crypto 和 graphy 两个词汇组成,点题了属于是
为了加密信息,要用到加密算法,将明文转换为密文,可以理解成将原有的数据转换为一堆乱码,而你不懂这其中的规则来破解,就不知道这是什么意思,就是和古代的暗号类似的东西
将明文转为密文的过程称为“加密”,而将密文恢复成原来的明文,也就是逆过程,称为“解密”
加密技术中核心的技术,莫过于是算法和加密
- 算法,也就是将普通的数据或者可以理解的数据与一串数字(密钥)结合,产生不可理解的密文的步骤
- 加密,也就是对数据进行编码和解密的一种算法。在安全保密中,可通过适当的 钥加密技术 和 管理机制 来保证网络的信息通信安全
其本质就是发送人用密钥加密,而接收人用相同的密钥进行解密的过程,这种也叫对称加密
替换加密
顾名思义,就是替换数据,将数据中的固定字母进行替换,导致别人不懂数据内容
但这种,有一个致命的缺陷,就是有些字母在英文中出现频率较高的,只要熟练的密码破译师可以从统计数据中发现规律,进而破译密码,这种破译的事情,在历史上也出现很多,比如玛丽女王要暗杀伊丽莎白女王的计划,就被破译,导致玛丽女王被处决,历史上还有很多
移位加密
也就是将数据改变,它的读取顺序,只有知道暗号规则的人,才可以解密
其中比较出名的是纳粹时期德国使用的 英格玛 加密通讯信息
如果你输入A-A-A ,可能会变成B-D-K,映射会随着每次按键而改变
数据加密标准
由 IBM 和 NSA 于1977年开发的,DES 最初用的是56比特的二进制密钥,意味着它有72千万亿个不同的密钥
在2000年,由电子边疆基金会组织研制的一台25万美元的计算机在两天内把 DES 的所有可能密钥,都暴力破解了一遍,只用了22.5小时,成功破解 DES加密算法,这意味着 DES 不再安全
高级加密标准
AES 的优势,很简单,没错比上面一个要长的密钥,有128位,192位,256位,这意味着什么呢,以128位的密钥为例,哪怕用现在地球上所有计算机的算力也需要上亿年才能试遍所有的组合的密钥
AES 将数据切成一块一块,每块16个字节,然后用密钥进行一系列替换加密和移位加密,然后再加上一些其他操作,进一步加密信息,每一块数据,都会重复这个过程起码10次以上
AES 在性能和安全性之间取得了平衡,如今 AES 被广泛运用比如在 WPA2协议 在 WiFi 中访问 HTTPS 网络
分类
我们想过没有如果一般密钥被黑客拦截了,那门黑客们,不就可以进行通信了吗,对了,它们的解决方案,就是所谓的 密钥交换
密钥交换是一种不发送密钥,但依然让两台计算机在密钥上达成共识的算法,通信双方交换数据建立共享密钥的过程,也就是 单向函数
单向函数是一种数学操作,很容易算出结果,但想从结果逆向推算出输入非常困难
因为对于每一个输入,函数值都容易计算(多项式时间),但是给出一个随机输入的函数值,算出原始输入却比较困难(无法在多项式时间内使用确定性图灵机计算)。 单向函数是否存在仍然是计算机科学中的一个开放性问题
对称加密
同一个密钥可以同时用作信息的加密和解密,这也称为 单密钥加密
我们上面讲的替换加密和移位加密,也属于 对称加密的范畴
所谓对称,也就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密
常用的算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK等
优点
算法公开、计算量小、加密速度快、加密效率高
缺点
在数据传送前,发送方和接收方必须商定好秘钥,然后使双方都能保存好秘钥
其次如果一方的秘钥被泄露,那么加密信息也就不安全了
另外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的独一秘钥,这会使得收、发双方所拥有的钥匙数量巨大,密钥管理成为双方的负担
非对称加密
分为公钥和私钥
公钥向外界公开,私钥只有解密的人,才可知,公钥只能加密但不能解密,私钥可以以解密公钥,像盒子和钥匙是一个道理,只有钥匙才可以开盒子,所以它叫不对称的
反过来,私钥加密后,可以用公钥解密,这种做法用于 签名,服务器可以用私钥加密,所有人都可以用服务器的公钥来进行解密,像一个不可伪造的签名,因为只有私钥的所有者,才可以对公钥进行加密,来证明正确的服务器或者个人
比较流行的技术是 RSA ,那我就简单的讲一下,这个算法吧,呜呜呜
RSA 算法是由麻省理工学院的三人创造的,而 RSA 是这三人的名字的首字母缩写,拼在一起
通常是先生成一对RSA密钥,分为私钥和公钥,可以在网络服务器中注册,为了提高安全性和保密性,RSA密钥至少为500位长度,一般推荐1024位的,因为一寸长一寸强,这可以使加密的计算量很大,但为了减少计算量,在传送信息时,常采用传统的加密方法和公开密钥加密方法相结合的方式,采用改进的 DES 或 IDES 对话密钥加密,然后使用 RSA 密钥加密对话密钥和信息摘要
到现在,我就开始讲它的算法原理了,肯定有人会说,都到现在了,你还没讲这个模块的一点方法呀,小陈,我只想说,我乐意,嘻嘻嘻
RSA 算法原理,是根据数论,寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥,关于数学方面的公式,你们感兴趣的自己去找,我怕再讲下去,就没人了
正式学习
在 node.js
中,使用 OpenSSL
类库作为内部实现加密解密的手段, OpenSSL
是一个经过严格测试的可靠的加密与解密算法的实现工具
crypto 模块是对 OpenSSL 的封装,主要功能有 哈希、对称加密以及非对称加密
哈希算法
哈希算法又称为散列算法,可以用来将任意长度的输入变换成固定长度的输出,常见的比如有md5,sha1等
特点
- 相同的输入会产生相同的输出
- 不同的输出会产生不同的输入
- 任意的输入长度输出长度是相同的
- 不能从输出推算输入的值
- 只能加密不能反向解密
是不是觉得很眼熟,没错就是上面讲的内容,所以前文是为下文做铺垫
md5 是一种常用的哈希算法,用于给任意数据一个“签名”,这个签名通常用一个十六进制的字符串表示
1 | const crypto = require("crypto") |
crypto.createHash()
方法传入需要加密的摘要算法,例如 MD5、SHA1、SHA256 和 SHA512,等,如果把案例的 MD5 改成 SHA1 ,加密之后的结果为 d17dfb160fce1ca89ecf94027a0bf39b6dda7f4e
update() 方法,默认字符串编码为 UTF-8
,也可以传入 Buffer
,update() 可以多次被调用,多次调用只是简单的把要加密的结果拼接起来
digest() 方法,表示加密之后的结果,以什么编码方式输出
- latin1 可以认为是 ASCII 扩展
- hex 十六进制
- base64 前端更熟悉的 base64 编码方式
crypto.createHash()
是根据原始文件,直接生成 md5 ,由此可以完全使用 md5 批量生成,然后存储起来进行撞库,我们为了防止这种现象的发生,会在生成 md5 的时候,我们会给原始的内容,手动加点东西,也就是密钥,然后再生成 md5 ,但 node.js 方便了我们 ,有了 createHmac()
方法,可以认为 Hmac 理解为用随机数,也就是强版的哈希算法,Hmac 是 Hash 的加强
1 | const crypto = require("crypto") |
base64的编码和解码方式
需要一个引入一个新的 API 来完成这个。这个 API 就是 Buffer.from
,它可把数据例如字符串转化成 buffer
base64 编码,对应浏览器中的 btoa
1 | const name = "CondorHero" |
base64 编码,对应浏览器中的atob
1 | const base64Name = "Q29uZG9ySGVybw==" |
Buffer.toString() 方法有三个参数
1 | (method) Buffer.toString(encoding?: BufferEncoding, start?: number, end?: number): string |
- BufferEncoding 编码
- start 截取从 start 开始
- end 截取到 end 结束
Hmac算法
攻击者可以借助“彩虹表”来破解哈希表,彩虹表在这里就不解释了
应对彩虹表的解决方案,是给密码加盐值(salt),将 pwd 和 salt 一起计算 hash 值。其中,salt 是随机生成的,越长越好,并且需要和用户名、密码对应保存在数据表中
通过加盐,实现了哈希长度扩展,但是攻击者可以通过提交密码和哈希值进行破解攻击。服务器会把提交的密码和 salt 构成字符串,然后和提交的哈希值进行对比。如果系统不能提交哈希值,不会受到此类攻击
从来没有绝对的安全的方案,但是不推荐使用密码加盐,而是使用 Hmac算法 ,因为可以使用任意的 Hash 函数,例如 md5 => HmacMD5、sha1 => HmacSHA1
1 | const crypto = require("crypto") |
对称加密 DES/AES
我们在之前已经简单介绍了DES了
那我们直接开始,不再墨迹了
crypto.createCipheriv()
方法
1 | crypto.createCipheriv(algorithm,key,iv [,options]) |
algorithm
:加密解密的类型
iv
是初始化向量,可以 为空 或者 16 字节的字符串
key
是加密密钥,根据选用的算法不同,密钥长度也不同,对应关系如下:
- des-cbc 对应 8 位长度密钥
- aes128 对应 16 位长度密钥
- aes192 对应 24 位长度秘钥
- aes256 对应 32 位长度密钥
总之,位数越长越难破解
DES 加密模式有: Electronic Codebook (ECB) , Cipher Block Chaining (CBC) , Cipher Feedback (CFB) , Output Feedback (OFB)。这里以密文分组链接模式 CBC 为例,使用了相同的 key 和 iv (Initialization Vector)
1 | const crypto = require("crypto") |
签名和验证算法
我们除了不可逆的哈希算法、数据加密算法,还有专门用于签名和验证的算法。这里也需要用 openssl 生成公钥和私钥
1 | const crypto = require("crypto") |
path模块
是 node.js 官方提供的,用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对文件路径的处理需求,总有一款适合你,嘿嘿嘿
获取路径中的文件名
path.basename ()
方法会返回 path 的最后一部分,第二部分表示要去掉的后缀,没有则不去掉文件后缀
1 | const path = require("path") |
获取路径中的目录名
path.dirname ()
方法会返回 path 的路径目录名
1 | const path = require("path") |
返回 path 的扩展名
path.extname () 方法,顾名思义就是返回文件路径的后缀名
1 | const path = require("path") |
解析地址
path.parse () 方法,返回一个对象,其属性表示 path 的有效元素
1 | const path = require("path") |
根据对象还原地址
path.format () 方法,从对象返回路径字符串,也就是上面的互补而已
1 | const path = require("path") |
拼接路径
path.join () 方法会将所有给定的 path 片段连接到一起(使用平台特定的分隔符作为定界符),然后规范化生成的路径
1 | const path = require("path") |
检测路径是否为绝对路径
path.isAbsolute () 方法,用来检验路径是否为绝对路径
1 | const path = require("path") |
获取当前文件所处目录的绝对路径并拼接地址
1 | const path = require("path") |
内置模块就讲到这了