❤ NodeJSの进阶【1】— 核心API

941 阅读23分钟

Node的诞生

NodeJS 诞生于2009年,由Ryan Dahi开发而成。它既不会一门语言也不是一门框架,它是基于Google V8 引擎的JS运行环境,使JS具备了 DOM操作(浏览器)I/O文件读写操作数据库(服务器) 等能力。 NodeJS 的包管理工具--NPM,已经成为世界开源包管理中最大的生态社区

总的来说NodeJS具备以下特点:

事件驱动
非阻塞IO模型(异步)
轻量、高效

Node核心API

Buffer

在引入 TypedArray 之前,javascript 并没有处理媒体文件二进制数据流的机制。后来 NodeJS 引入 Buffer,同于TCP流、文件系统操作,以及其他上下文中与八位字节流进行交互。

Buffer的特点

  • Buffer跟数组十分类似,但是可以存储二进制数据
  • Buffer的大小在创建时确定,且无法更改
  • Buffer类在全局作用域,不需要 require
  • Buffer内的数据是以二进制形式存储的,但是以二进制显示数据会很长,所以控制台打印 Buffer 会是十六进制的形式显示。
    • Buffer 内的元素(字节)的范围都是 00-ff(两位十六进制范围)、0-255(十进制范围)、00000000-11111111(二进制范围)
      计算机内存会被划分为无数个小格、每个小格以电信号形式(0或1)存储数据,这个小格就称为比特位(bit),8bit = 1 byte(字节),Buffer的一个元素占一个字节
  • Buffer的长度代表占内存的大小,Buffer 中的英文占一个字节 中文占三个字节

Buffer的使用

// 创建长度为10个字节的buffer。默认每个字节以 0 填充
const buf1 = Buffer.alloc(10);
console.log("buf1", buf1);
// 将0号字节位 用10进制值 88 填充  
buf1[0] = 88
// 将1号字节位 用10进制值 255 填充  
buf1[1] = 255
// 将2号字节位 用16进制值 0xaa 填充  
buf1[2] = 0xaa
// 将3号字节位 用10进制值 257 填充,257转换为二进制为1 0000 0001 超过1111 1111最大上限左侧超过8个比特位部分直接被舍掉  
buf1[3] = 257
console.log("修改buf1字节位后", buf1);
// 创建长度为10个字节的buffer。每个字节以 1 填充
const buf2 = Buffer.alloc(10, 1);
console.log("buf2", buf2);
// 创建长度为 10 的未初始化的缓冲区。
// 这比调用 Buffer.alloc() 快,
// 但返回的缓冲区实例可能包含旧数据,
// 需要使用 fill()、write() 、
// 或其他填充缓冲区内容的函数重写。
const buf3 = Buffer.allocUnsafe(10);
console.log("buf3", buf3);
// 创建包含字节 [1, 2, 3] 的缓冲区。
const buf4 = Buffer.from([1, 2, 3]);
console.log("buf4", buf4);
// 创建包含字节 [1, 1, 1, 1] 的缓冲区,
// 所有条目都使用 `(value & 255)` 截断以符合范围 0–255。
const buf5 = Buffer.from([257, 257.5, -255, '1']);
console.log("buf5", buf5);
// 创建包含字符串 'tést' 的 UTF-8 编码字节的缓冲区:
// [0x74, 0xc3, 0xa9, 0x73, 0x74](十六进制)
// [116, 195, 169, 115, 116](十进制)
const buf6 = Buffer.from('tést');
console.log("buf6", buf6);
// 创建包含 Latin-1 字节 [0x74, 0xe9, 0x73, 0x74] 的缓冲区。
const buf7 = Buffer.from('tést', 'latin1');
console.log("buf7", buf7);

let str = "Hello 二进制"

const buf8 = Buffer.from(str);
console.log("buf8", buf8);
console.log("字符串长度", str.length, "buffer长度", buf8.length);

image.png

字符编码

人有人的语言,计算机有计算机的语言。计算机的机器语言是通过二进制处理数据,人的语言要想跟机器语言进行交互,就需要一个 字典(字符集)

字符集为每一个字符分配唯一的编号,通过这个编号可以找到对应的字符。 字符依据字符集转换成唯一的编号这个过程称之为 字符编码,反之为 字符解码

ASCII码对照表

字符A的ASCII码对照关系表

BINSymbol
0100 0001A

对于每个字符编号转换、内存存储,ASCII 制定了一个规范。ASCII 码有8位数,每位是一个比特 (bit),8位就是一个字节 (byte)。除了第一位是0, 其他7位都可以有0 或者 1 两个选择,所以 ASCII 一共可以表示 2^7 ,也就是 128 个字符。

对于英语来说,128个字符包含了字母、数组和标点符号等。但是对于其他语言,比如汉语,是远远不够的

Unicode统一编码

如果用一个标准方案来展示世界上所有语言中的所有字符,Unicode 的出现很好的解决了这个问题。它是一个很大的字符集合,计算机支持的字符都在其中。它可以容纳几百万个字符。每个字符的编号(码点)都不一样。比如,U+0041表示英语的大写字母AU+4E25表示汉字。可以查看 unicode字符对照表unicode汉字对应表

Unicode的问题

Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

比如:汉字Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),这意味着表示这一个字符,至少需要两个字节。表示其他字符可能需要三个或四个,甚至更多。

这里涉及了两个问题:

  • 编码表意不明,无法区别 Unicode 和 ASCII。对于计算机而言,他无法判断三个字节代表一个字符还是三个字符。
  • 浪费存储空间,用Unicode表示字符可能需要3、4个字节,表示英文一个字节就够了,如果都统一用4个字节来表示字符,英文的其他两个或三个字节都用0来填充。 对存储来说是极大地浪费。

UTF-8

Unicode 只提供了字符集,没有说明这些码点的编码方式是什么。后来 UTF-8 成为在互联网上使用最广的一种 Unicode 的实现方式,其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

Dgram

dgram 模块提供了 UDP 数据报套接字的实现。

  • 新建 dgram/server.js,并执行 node server.js 运行服务
    // 服务端
    const dgram = require('dgram')
    const server = dgram.createSocket('udp4')
    
    // 发生错误
    server.on('error', (err) => {
      console.log(`服务端异常:\n${err.stack}`)
      server.close()
    })
    
    // 接收请求
    server.on('message', (msg, info) => {
      // 获取客户端IP+端口号   
      console.log(`服务端接收来自 ${info.address}:${info.port}${msg}`)
    })
    
    server.on('listening', () => {
      const address = server.address()
      console.log(`服务器监听 ${address.address}:${address.port}`)
    })
    
    // 在3005端口运行服务
    server.bind(41234)
    
  • 新建 dgram/client.js,并执行 node server.js 访问服务
    // 客户端
    const dgram = require('dgram')
    const message = Buffer.from('一些字符串')
    const client = dgram.createSocket('udp4')
    console.log('客户端执行了');
    client.send(message, 41234, 'localhost', (err) => {
      // 链接建立成功后,要及时关闭否则会一直占用资源
      client.close()
    })
    

Event

Node的大部分核心API都是通过 异步事件驱动架构 实现的,在该架构中,某些类型的对象(称为"触发器")触发命名事件,使 Function 对象("监听器")被调用
例如:net.Server 对象在每次有连接时触发事件;fs.ReadStream 在打开文件时触发事件;在每当有数据可供读取时触发事件。
所有触发事件的对象都是 EventEmitter 类的实例,这些实例提供 EventEmitter.on(事件类型,事件回调) 监听某个时间,EventEmitter.emit(事件类型)派发

示例

const EventEmitter = require('events')
class MyEmitter extends EventEmitter { }
const myEmitter = new MyEmitter()
myEmitter.on('sayHello', () => {
  console.log("触发 sayHello 事件");
})
myEmitter.emit('sayHello')

image.png

事件回调 this 指向

const EventEmitter = require('events')
class MyEmitter extends EventEmitter { }
const myEmitter = new MyEmitter()
// 如果事件回调为普通函数,事件回调的this 指向 myEmitter
myEmitter.on('sayHello', function (a, b) {
  console.log(a, b, this, this === myEmitter);
})

// 如果事件回调为箭头函数,事件回调的this指向会丢失
myEmitter.on('sayHello', (a, b) => {
  console.log(a, b, this, this === myEmitter);
})
myEmitter.emit('sayHello', '参数1', '参数2')

image.png

只触发一次事件

const EventEmitter = require('events')
class MyEmitter extends EventEmitter { }
const myEmitter = new MyEmitter()
myEmitter.once('sayHellow', function () {
  console.log("触发发 sayHello 事件");
})

myEmitter.emit('sayHellow')
// 使用 once 注册事件 事件触发一次后会自动解绑
myEmitter.emit('sayHellow')

image.png

fs

NodeJS 中所有文件模块都是通过 fs 模块实现的,包括文件目录的创建、删除和查询以及文件的读取和写入。在 fs 模块中,所欲方法都分为同步、异步两种实现方法,具有 sync 后缀的为同步方法

权限位 mode

fs 用来操作文件,这个涉及文件的权限问题

权限项执行
字符表示rwx
数字表示421

如上表,文章的操作权限分为三种,读、写和操作。数字表示为八进制位,具有权限的八进制为4、2、1,不具备权限位0

标识位 flag

NodeJS中,标识位代表着对文件的操作方式,如可读、可写、既可读又可写等。下面是各个标识符和其对应的含义

符号含义
r读取文件,如果文件不存在就抛出异常
r+读取并写入文件,如果文件不存在就抛出异常
rs读取并写入文件,指示操作系统绕开本地文件系统缓存
w写入文件,文件不存在会被创建。存在则清空后写入
wx写入文件,排它方式打开
w+读取并写入文件,文件不存在则创建文件,存在则清空后写入
wx+和w+类似,排它方式打开
a追加写入,文件不存在则创建文件
ax与a类似,排它方式打开
a+读取并追加写入,不存在则创建
ax+与a+类似,排它方式打开

简单概括一下就是:

  • r:读取
  • w:写入
  • s:同步
  • +:增加相反操作
  • x:排它方式

r+ 和 w+ 的区别,当文件不存在时,r+ 不会创建文件。二回抛出异常,但是 w+ 会创建文件;如果文件存在 r+ 不会自动清空文件,单 w+ 会自动把文件已有内容清空

读取文件

  • 同步读取

    同步读取方法 readcFileSync,它有两个参数:

    • 第一个参数为读取 文件路径文件描述符
    • 第二个参数为 配置对象(options) 或者 字符串(encoding) ,默认值为null
      • encoding:编码类型,默认为 null
      • flag:标识位,默认为r
    const fs = require('fs')
    const path = require('path')
    // 同步读取文件
    const buffer = fs.readFileSync(path.resolve(__dirname, './1.text'))
    // 同步步读取文件 使用 UTF-8 编码读取
    const text = fs.readFileSync(path.resolve(__dirname, './1.text'), 'utf-8')
    
    console.log('读取结果', buffer, text);
    
  • 异步读取

    异步读取方法 readcFile,它有三个参数:

    • 前两个参数与 readcFileSync 一样
    • 第三个参数为 回调函数,函数有两个参数,分别为 err(错误)和 data(数据)
    const fs = require('fs')
    const path = require('path')
    
    // 异步读取文件  
    fs.readFile(path.resolve(__dirname, './1.text'), 'utf-8', (err, data) => {
      console.log('读取结果', err, data);
    })
    

写入文件

  • 同步写入

    同步写入方法 writeFileSync,它有三个参数:

    • 第一个参数为读取 文件路径文件描述符
    • 第二个参数为写入的数据,类型为 StringBuffer
    • 第三个参数为 配置对象(options) 或者 字符串(encoding) ,默认值为null
      • encoding:编码类型,默认为 utf-8
      • flag:标识位,默认为 w
      • mode:权限位,默认为 0o666
    const fs = require('fs')
    const path = require('path')
    const buffer = Buffer.from('hello code')
    // 同步写入 写入String
    fs.writeFileSync(path.resolve(__dirname, '2.text'), 'hello world')
    // 同步写入 写入Buffer
    fs.writeFileSync(path.resolve(__dirname, '3.text'), buffer)
    
  • 异步写入

    异步写入方法 writeFile,它有四个参数:

    • 前三个参数与 writeFileSync 一样
    • 第四个参数为 回调函数,函数有一个参数,为 err(错误)
    const fs = require('fs')
    const path = require('path')
    // 异步写入 字符串
    fs.writeFile(path.resolve(__dirname, '4.text'), 'hello world', (err) => {
      // 写入完成
      if (!err) {
        // 读取刚写入的内容
        const content = fs.readFileSync(path.resolve(__dirname, '4.text'), 'utf-8')
        console.log('content', content);
      }
    })
    
  • 同步追加写入

    前面的写入方式都是以覆盖的形式写入内容,同步追加写入为 appendFileSync,他跟 writeFileSync 一样也是有三个参数

    const fs = require('fs')
    const path = require('path')
    // 追加写入内容
    fs.appendFileSync(path.resolve(__dirname, '1.text'), 'Hello Code')
    // 读取文件
    const content = fs.readFileSync(path.resolve(__dirname, './1.text'), 'utf-8')
    console.log('content', content);
    
  • 异步追加写入

    异步写入方法 appendFile,它有四个参数,每个参数用法跟 writeFile 一样

    const fs = require('fs')
    const path = require('path')
    
    // 异步写入 字符串
    fs.appendFile(path.resolve(__dirname, '1.text'), 'hello zhangsan', (err) => {
      // 写入完成
      if (!err) {
        // 读取刚写入的内容
        const content = fs.readFileSync(path.resolve(__dirname, '1.text'), 'utf-8')
        console.log('content', content);
      }
    })
    

fs 模块高级用法

  • 打开文件
    操作文件前需要选对文件进行打开操作。open 方法用于打开文件描述符,它有四个参数:
    • path:文件路径
    • flag:标识位
    • mode:权限位,默认 0o666
    • callback:回调函数,有两个参数 err(错误)和 fd(文件描述符),打开文件后执行
const fs = require('fs')
const path = require('path')
fs.open(path.resolve(__dirname, '4.text'), 'r', (err, fd) => {
  console.log('data', err, fs)
})
  • 关闭文件 close 方法有两个参数:
    • 第一个参数为:关闭文件的文件描述符
    • 第二个参数为:回调函数,回调函数有一个参数 err(错误),关闭文件会执行
const fs = require('fs')
const path = require('path')
fs.open(path.resolve(__dirname, '4.text'), 'r', (err, fd) => {
  // 关闭文件描述符
  fs.close(fd, err => {
    console.log('关闭成功')
  })
})
  • 读取文件
    可以使用 read 方法读取文件,它与 readFile 不同,一般针对文件太大,无法一次性读取全部内容到缓存中或文件大小未知的情况,多是多次读取到 Buffer
    read 方法中有六个参数:
  • fd:文件描述符,需要先用 open 打开
  • buffer:要将内容读取到 Buffer
  • offset:整数,向 Buffer 写入的初始位置
  • length:整数,读取文件的长度
  • position:整数,读取文件初始位置
  • classback:回调函数,有三个参数err(错误)、bytesRead(实际读取的字节数)、buffer(被写入的缓存区对象)、读取执行完成后执行。
const fs = require('fs')
const path = require('path')
// 创建一个占用内存6的缓存区
const buf = Buffer.alloc(6)
fs.open(path.resolve(__dirname, '1.text'), 'r', (err, fd) => {
  console.log('fd', err, fd);
  // 读取文件
  fs.read(fd, buf, 0, 3, 0, (err, bytesRead, buffer) => {
    console.log(bytesRead, buffer, buffer.toString());
  })
})
  • 创建可读流
    也可以通过 createReadStream 来创建可读流,它有两个参数:
    • 第一个参数为 文件路径文件描述符
    • 第二个参数为 配置对象,默认为 null
      • flags:标识位,默认:'r'
      • encoding:编码类型
      • ......
const fs = require('fs')
const path = require('path')
const reader = fs.createReadStream(path.resolve(__dirname, '1.text'), { encoding: 'utf-8' })

// 读取文件发生错误
reader.on('error', (err) => {
  console.log('发生异常', err);
})
// 已打开要读取的文件
reader.on('open', (fd) => {
  console.log('文件已打开', fd);
})
// 文件已就位,可用于读取事件
reader.on('ready', () => {
  console.log('文件已准备好...');
})
// 文件读取中
reader.on('data', (chunk) => {
  console.log('读取文件数据', chunk);
})
// 文件读取完成
reader.on('end', () => {
  console.log('读取已完成...');
})
// 文件已关闭
reader.on('close', () => {
  console.log('文件已关闭...');
})

HTTP模块

创建一个简单的HTTP服务

const http = require('http')
// 创建一个服务
const server = http.createServer()
// 监听请求  
server.on('request', function (request, response) {
  // 配置响应头
  response.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
  // 通过 request.write 响应数据
  response.write('<h1>Hello World</h1>')
  // 通过 response.end 结束响应,防止客户端一直等待
  response.end('<p>响应结束了</p>')
})

// 绑定端口号,启动服务
server.listen(3002, function () {
  console.log('服务在3002端口启动了');
})

创建HTTP2服务

const http2 = require('http2')
const fs = require('fs')

// http2默认使用https  https = http + ssl协议
const server = http2.createSecureServer({
  // 配置公钥、私钥。 这俩一般需要去第三方证书机构认证
  // 这里可以通过openssl 生成公钥 私钥
  key: fs.readFileSync('localhost-privkey.pem'),
  cert: fs.readFileSync('localhost-cert.pem')
});
server.on('error', (err) => console.error(err));

server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html; charset=utf-8',
    ':status': 200
  });
  stream.end('<h1>Hello World</h1>');
});

// 将服务挂载8443端口运行
server.listen(8443);

HTTP发展历史

HTTP创建之初主要是为了,将超文本标记语言(HTML)从web服务器发送到客户端浏览器中。随着互联网的普及和发展,WEB使用环境也变得越来越复杂。我们不得不对HTPP进行优化。

HTTP的基本优化

影响HTTP 网络请求的因素主要有两个:带宽延迟

带宽:现在网络基建做的很好,带宽得到了很大的提升。我们不再会担心带宽影响网速。
延迟

  • 浏览器阻塞:浏览器会因为一些原因阻塞请求,浏览器对同一个域名,同时只能有4个连接(根据浏览器不同内核可能会有差异)。超过浏览器最大连接数限制,后续请求就会被阻塞。
  • DNS查询:浏览器访问域名时,需要通过DNS查询将域名转换为 IP 地址。这个过程可以通过DNS缓存来进行优化
  • 建立连接:HTTP 是基于TCP协议,每个HTTP请求都需要进行三次握手才能成功建立连接。但是这些连接无法复用,会导致每次请求都经历三次握手和慢启动。

HTTP 1.0、1.1的区别

  • 缓存处理
    在HTTP1.0中,主要使用header中的 If-Modifyied-Since、Expires 来作为缓存判断标准,HTTP1.1这引入了更多缓存控制策略例如 Entity tagIf-Unmodified-SinceIf-MatchIf-None-Match等更多可供选择的缓存头控制缓存策略

  • 带宽及网络连接的使用
    HTTP1.0中,存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象传输过来,并且不支持断点续传功能,HTTP1.1则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是206,这样既提高了开发的自由性,同时充分利用了带宽和连接

  • 错误通知的管理 在HTTP1.1中新增了24个错误状态码,例如:409表示请求的资源与资源当前的状态发生冲突,410代表服务器的某个资源被永久性删除

  • Host头处理
    在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname),但是随着虚拟机技术的发展,在同一台物理服务器可以存在多个虚拟主机,并且它们共享一个IP地址,例如有一台 ip 地址为 61.135.169.125 的服务器,在这台服务器上部署着谷歌、百度、淘宝的网站。为什么我们访问 https://www.google.com 时,看到的是 Google 的首页而不是百度或者淘宝的首页,这就是 Host 起的作用
    HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中没有Host头域会报告一个 400 错误

  • 长链接
    HTTP1.1支持长连接和请求流水线处理,在一个TCP连接上可以传送多个请求和响应,减少了建立和关闭连接的等待和延迟。HTTP1.1中 Connection:keep-alive 默认是开启长连接的

HTTP2.0的新特性

  • 新的二进制格式
    HTTP1.X的解析是基于文本,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,必须要根据它的应用场景考虑其健壮性。二进制则不同,只存在0和1方便且健壮。
  • 多路复用
    多路复用可以看做HTTP1.1长连接的升级,在HTTP1.0每次连接只能发送一次请求,请求结束后关闭连接。 HTTP1.1的长连接可以在一个连接中发送若干个请求,但是会出现线头阻塞问题,即若干请求排队单线程依次执行处理,当某个请求超时,后续请求只能被阻塞。
    多路复用可以做到连接中的若干请求随机混杂一起各不影响,当某个任务耗时严重也不会影响到其他请求执行。
  • header压缩
    请求的header有大量信息,并且重复发送。HTTP2.0通过encoder方式来减少传输的header大小,通讯双方各自一份 缓存的header fields表,通过表对照传输的数据。这样既避免了header重复传输又减少了传输数据的大小
  • 服务端推送 即 服务端具有 server push 功能

HTTPS模块

NodeHTTPS 模块与 HTTP2 模块API基本相似,这里不再过多阐述可以查看 HTTPS模块使用文档 这里重点介绍下 HTTPS 的发展
HTTP请求是明文传输的,数据有被拦截的风险。为了解决这个问题 Netscape 公司制定了 HTTPS协议,HTTPS 可以将数据加密传输,也就是传输密文,及时传输数据被拦截也无法破译,这就保证了网络通讯的安全。

openssl

openssl 是一个安全套接字层密码库,囊括主要的密码算法、常用密钥、证书封装管理功能及实现ssl协议。我们可以使用它来完成数据的加解密。

  • 安装
    进入 openssl.exe下载地址 随便选择一个适合自己版本下载,安装时 一定要 记住安装路径!!!

  • 配置环境变量
    桌面 -- 右击我的电脑 -- 属性 -- 高级系统设置 -- 环境变量 -- 编辑path

    image.png

  • 在任意地方新建文件夹存放私钥公钥,在该文件夹下打开命令栏窗口

    • 输入指令生成公钥

      openssl genrsa 1024 > key.pem
      
    • 输入指令生成私钥

      openssl req -x509 -new -key key.pem > key-cert.pem
      

      输入指令后,会依次输入国家、省份、地市等信息,输入完成自动生成私钥,如果输入上面指令报错 如下:

      image.png 这个错误是公钥编码格式不对导致的,可以借助vscode将公钥保存为utf-8格式解决

    • 搭建HTTPS服务

      const https = require('https');
      const fs = require('fs');
      
      const options = {
        // 绑定公钥、私钥 
        key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
        cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
      };
      
      https.createServer(options, (req, res) => {
        res.writeHead(200);
        res.end('hello world\n');
      }).listen(8000);
      

密码学基础

在深入了解HTTPS协议之前,我们要知道一些密码学的知识。

  • 明文:明文指的是未被加密过的原始数据

  • 密文:明文通过某种加密算法加密之后,会变成密文。密文也可以解密得到原始的明文

  • 密钥:密钥是一种参数,明文与密文之间通过某个算法相互转换,密钥作为算法的参数使用

  • 对称加密:对称加密又叫做私钥加密,即数据的接收方和发送方使用同一个密钥去加密解密数据。对称加密的特点是:算法公开加密和解密速度快,适合于对大数据量进行加密。常见的算法有DES3DESTDEABlowfishRCSIDEA

    • 加密过程:明文+加密算法+密钥 => 密文
    • 解密过程:密文+解密算法+密钥 => 明文
      对称加密中用到的密钥叫做私钥,加密解密使用的是同一个私钥。由于算法是公开的,私钥一旦泄露密文很容易被破解,所以对称加密的缺点就是密钥的安全管理比较困难。
  • 非对称加密:非对称加密也叫做公钥加密,它比对称加密安全性更好。非对称加密使用一堆密钥,即公钥和私钥,并且二者成对出现,私钥被自己保存,不能对对泄露。公钥是公共的密钥,任何人都可以获得该密钥,用公钥或私钥中的任何一个进行加密,用另一个进行解密

HTTPS通信过程

HTTPS 数据传输的过程中,需要用 SSL/TLS 对数据进行加密和解密,需要用HTTPS对加密后的数据进行传输,由此可以看出 HTTPS 是由 HTTPSSL / TLS 一起合作完成的,即 HTTPS协议 = HTTP协议 + SSL/TLS协议

HTTPS为了兼顾效率和安全,同时使用了对称加密和非对称加密,数据通过对称加密传输的,对称加密过程需要客户端的一个密钥,为了确保安全,通过非对称加密对该密钥进行加密传输给服务器。即对数据进行对称加密,对称加密使用的密钥通过非对称加密传输。

服务端的公钥和私钥,用来进行非对称加密
客户端生成的随机密钥,用来进行对称加密

数字证书

HTTP不会对通信双方进行身份校验,所以身份有可能会被伪造造成安全问题,数字证书得出现就是为了解决这个问题。使用流程如下:

  • 服务器先向一个权威的第三方机构申请一个身份证书
  • 客户端向服务端建立通信前要先向服务器端请求获取服务器的证书
  • 服务器接收到请求后,将数字证书发送给客户端
  • 客户端获取服务器证书后,与可信任的第三方机构证书进行验证,验证通过后可进行正常的通信。

数字签名

HTTP不会对数据的完整性进行验证,这样如果通信过程中数据被篡改了通信的双方也没办法知道,所以有了数字签名技术

数字签名主要有两个作用:

  • 验证数据是否为目标对象发出的
  • 对数据的完整性进行验证,验证数据是否被篡改过。
    • 对发送的内容进行摘要
      这个过程跟webpack每次打包生成的hashmap非常类似,通信双方约定每次发送方对数据通过哈希算法得到哈希值,这样每次数据发生变化就会生成新的哈希值,发送方会将哈希值作为数据的摘要传给另一方。
      另一方接收到数据和摘要后,通过约定的哈希算法将数据进行哈希,得到哈希值后跟传送过来的摘要进行比对,从而判断数据的完整性。
    • 对摘要信息进行签名
      对摘要进行签名主要是确认数据发送人的身份,签名技术使用了非对称加密原理