❤ NodeJSの进阶【2】— 构建TCP、UDP、HTTP、HTTPS服务

1,297 阅读20分钟

基于Node的网络编程

  • Node是面向网络而生的平台
  • Node为事件驱动、无阻塞、单线程
  • Node的API十分贴合网络,适合构建灵活的网络服务
  • Node可以非常方便的搭建网络服务器,传统的 web 平台大都需要专门的 web 服务器作为容器,例如ASP需要IIS作为服务器、PHP需要搭载 ApacheNginx 环境等
  • Node提供了 netdgramhttphttps 4个模块,分别处理TCPUDPHTTPHTTPS 适用于服务器端和客户端

网络模型

网络七层模型

705728-20160424234824085-667046040.png

为了便于学习和理解这里用五层网络模型来表述

网络五层模型

src=http___www.pianshen.com_images_853_188eed3fde622e2499667f824c43237d.png&refer=http___www.pianshen.jfif

层与协议

网络模型的每一层都是为了完成一种功能,为了实现这些功能,就需要大家遵守公共的规则,大家遵守的规则,就叫做 协议

实体层

计算机之间要实现组网通信,首先要通过光缆、电缆、无线电波等物理方式连接起来。这种将计算机通过物理手段连接的方式就称为 “实体层”,它的作用只负责传输 01 的电信号

链接层

单纯的 01 没有任何意义,必须要规定解读方式,多少个电信号为一组?每个信号位有什么意义?
这就是 “链接层” 的功能,它在 “实体层” 的上层规定了电信号01的分组方式。

以太网协议

早期,每家公司都有自己的电信号分组方式。后来 以太网协议 逐渐占据了主导地位。
以太网协议 规定了一组电信号构成一个数据包,叫做 “帧”,每一帧分为 标头(Head)数据(data) 两部分

image.png
标头:包含了数据的说明项,比如数据的发送者、接收者、数据类型等,“数据” 则是数据包的具体内容

MAC地址

标头中包含了接收者和发送者,那么两者的信息是怎么标识的。
以太网协议 规定连入网络的计算机必须有网卡接口,数据包从一个网卡传输到另一个网卡。网卡的 MAC地址 就是用来标识通信双方的地址信息,通过它可以定位网卡和数据包的路径。

每块网卡在出厂时,就会有一个在世界上独一无二的MAC地址,它是一个长度为48位的二进制数,通常同12个十六进制数表示

image.png
MAC地址前6个十六位制数为厂商编号,后6个为该厂商的网卡流水号

广播

在以太网协议中,通信使用一种 “原始” 的方式,系统会将数据包发送到本子网络中所有计算机,让每台计算机自己判断是否为数据接收方。
例如同一个子网络中有五台计算机,1号计算机想给3号计算机发送数据包,所有计算机都会接收到这个包,它们会读取包的 标头,找到接收方的 MAC 地址跟自己 MAC 地址做对比,如果一致就进一步处理否则就丢弃,这个过程称之为 广播
有了数据包的定义、网卡的MAC地址、广播发送方式,链接层可以在计算机之间传输数据了。

网络层

通过广播的方式传输数据包不仅效率低,局限性很大。通信双方不在同一子网络中,无法通过广播的方式传递数据包。所以我们需要区分哪些 MAC 地址属于同一子网络。

网络层通过一套新的地址(网络IP地址)来区分不同的计算机是否属于同一子网络。这样每台计算机就有两个地址 Mac地址网络地址
这两个地址没有任何联系MAC地址 是绑定到网卡上的,网络地址 是网络管理员分配的,它们只是随机的组合在一起。网络地址可以确定数据包接收方所在的子网络,MAC地址 可以将数据包传送给子网络中的目标网卡

IP协议

规定网络地址的协议称之为 IP协议 ,它所表示的地址成为 IP地址
目前,广泛采用的是IP协议第四版(IPv4),这个版本规定网络地址由 32个二级制位 组成

image.png
我们通常用四段十进制数表示IP地址,即 0.0.0.0 到 255.255.255.255

IP协议主要有两个作用

  • 为每一台计算机分配IP地址
  • 确定哪些地址在一个子网络中

传输层

计算机在 网络层 完成双方的通信,再往上进入 传输层

端口

计算机上很多程序都需要用到数据包,当从互联网上获取一个数据包该怎么确定给哪个程序使用,这时候就需要用到 端口端口 就是网卡的程序的编号,每个数据包发到主机特定的 端口,所以不同数据能取到自己需要的数据。

端口 是0到65535的一个整数,正好为16个二进制位。0到1023的端口被系统所占用,用户只能选用大于1023的端口,不管浏览网页还是聊天,应用程序会随机选用一个端口,与服务器响应端口联系

传输层 的功能建立了 端口到端口 的通信,这种客户端服务器端之间 端口与端口 的通信方式也叫做 套接字通信(Socket)。相比之下,网络层 的功能则建立了 主机到主机的通信,在确定 主机端口 后就可以实现程序之间的交流。

UDP协议

在数据包中加入端口信息,就得规范一个新协议,最简单的实现就是 UDP协议

UDP数据包也是由 “标头”“数据” 组成,UDP数据包“标头” 定义了接收和传送的端口,“数据” 部分包含了具体数据。而整个 UDP数据包 被放置在 IP数据包数据 部分,如果从 以太网数据包 算起,整个数据包的结构关系大概如下

以太网数据包 包含了 以太网数据标头 + 以太网数据
以太网数据 包含了 IP数据标头 + IP数据
IP数据 包含了 UDP数据标头 + UDP数据

UDP标头 部分一共只有8个字节,总长度不超过65535字节,可以正好放进一个 IP数据包 中。

UDP协议 本身不太可靠,通信双方在传输数据时,可能会造成数据丢失,双方进行会话时,无法确保对方是否成功接收到数据,无法保证数据的完整一致性。

TCP协议

UDP协议作为一种非面向连接的协议,具有不可靠性,TCP 的出现就是为了解决这个问题。
TCP协议 是一种面向连接、可靠的、基于字节流的传输层通信协议。TCP协议 在发送数据前要通过三次握手建立连接,当因为某些原因数据发送失败时,会重新建立连接。当数据发送完成后,还需要断开连接减少系统资源的占用。

TCP协议与UDP协议的区别

特性/协议TCPUDP
是否面向连接面向连接非面向连接
传输可靠性可靠不可靠
使用场景少量数据大量数据
速度

总的来说 TCP协议是面向连接的,它传输数据可靠性高,但是占用系统资源高传输效率低。如果对数据完整性、数据正确顺序有要求应该使用 TCP协议,例如,浏览器中的数据、网络邮箱的数据。UDP协议协议虽然不太可靠、容易丢包,但是传输效率极高,一些大量数据流场景常用到它,例如,语音通话、视频通话、直播等。

应用层

无论是 TCP协议 还是 UDP协议 ,相比较而言都算是偏底层的协议。我们常接触到的基本在应用层中。
传输层 实现数据的传输和接收,由于互联网是开放架构,数据来源也是五花八门,我们需要制定规范解读数据,实现双方的数据通讯。

应用层 的作用就是规定不同应用程序的数据格式
TCP协议 可以为各种应用传输数据,比如 EmailWWWFTP,这些应用也要根据不同协议规定数据的规范,这些应用的协议就构成 应用层

应用层 是最高的一层,它是直接面向用户的,他的数据放在 TCP 的数据包部分 即如下:

image.png

搭建TCP服务

通信双方建立TCP连接时,需要进行三次握手。三次握手成功确保TCP成功建立连接后,开始进行通信。

TCP三次握手

客户端 首先向 服务器端 发送数据,进行 第一次握手服务器端 接收到数据后向 客户端 作出响应,进行 第二次握手客户端 接收到响应后,会进行 第三次握手 ,对 服务器端 再次发送数据告知 服务器端 可以接收到数据。
客户端服务器端 经过三次握手后,TCP连接建立成功接下来就可以传输数据了。

net模块

Nodenet 模块可以搭建 TCP 服务

// 服务器端  
const Net = require('net')
const app = Net.createServer()

// 当客户端建立连接成功
app.on('connection', clientSocket => {
  console.log('建立连接成功');
  // 向单当前连接的客户端发送数据
  clientSocket.write('hello')
})

// 将服务放到3000端口上启动
app.listen(3000, function () {
  console.log('TCP服务运行');
})

// 客户端  
const Net = require('net')

// 客户端创建连接
const client = Net.createConnection({
  // 服务器的域名、端口号
  host: '127.0.0.1',
  port: '3000'
})

// 连接成功时
client.on('connect', () => {
  console.log('成功连接到服务器');
})

// 服务器端响应数据时
client.on('data', data => {
  console.log('服务器返回数据', data.toString());
})

客户端和服务器端实现双向通信

// 服务器端
// 服务器端在建立连接成功 监听客户端发送得数据  
// 当客户端建立连接成功
app.on('connection', clientSocket => {
  console.log('建立连接成功');
  // 向当前连接的客户端发送数据
  clientSocket.write('hello')
  // 监听客户端发送得数据
  clientSocket.on('data', (data) => {
    console.log('客户端传输数据:', data.toString());
  })
})

// 客户端  
// 连接成功时
client.on('connect', () => {
  console.log('成功连接到服务器');
  // 向服务器端发送数据
  client.write('Hello,我是客户端')
})

客户端将终端信息发送到服务器端

这里实现一个小功能,将客户端终端输入得数据发送到服务器端。

// 客户端  
client.on('connect', () => {
  console.log('成功连接到服务器');
  client.write('Hello,我是客户端')

  // 建立连接后,获取终端输入内容并传输给服务器端  
  process.stdin.on('data', data => {
    client.write(data)
  })

})

image.png

搭建UDP服务

UDP

UDP 全称为 User Datagram Protocol,又称为用户数据包协议

  • UDPTCP 相同,都是位于 网络传输层 用于传输数据包
  • 无连接、传输速度快、数据传输不可靠,不提供数据包分组,无法对数据包进行排序,数据是无序的,无法保证传输时数据的完整性、正确性。
  • UDP 支持一对一通信,也支持一对多通信。
  • UDP 适合对传输速度要求高,对数据传输质量要求不严谨的应用。例如,流媒体、多人游戏、实时音视频等。

UDP传播方式

UDP 传播数据有三种方式,分别为:单播广播(多播)组播

  • 单播
    单播 指的是 UDP 以点对点,单一目标的一种传播方式。地址范围为0.0.0.0 - 223.255.255.255
  • 广播
    UDP 还支持 广播 的形式传输数据,广播 目标地址为当前局域网内的所有设备
    地址范围分为 受限广播直接广播
    • 受限广播 只在当前路由或交换机中广播,不会转发到其他路由中。受限广播的目标IP地址的网络字段和主机字段全为1,即地址为255.255.255.255
    • 直接广播 会被路由转发,IP地址网络字段会定义这个地址,主机字段则为1,例如192.168.10.255
  • 组播
    广播组播 都是一对多传输数据,组播 是将同样的数据传输给一组目标主机,广播 是将数据传输给局域网中的所有主机,组播 则是将多个组件组成一组并发送数据。

不同传播在 “一对多” 场景的应用

  • 单播 实现 “一对多”
    服务器在UDP协议中,以 单播 方式向多个主机传输数据。需要发送多个 单播 数据包,发送次数与接收者数目相同,且每个数据包中都有确切目标主机的IP地址。如果接受者成百上千,就会加大服务器的负担。
  • 广播 实现 “一对多”
    广播 被限制在局域网中,一旦有设备发送广播数据,则广播域内所有设备都会收到数据,并耗费资源去处理。大量广播数据包会消耗网络带宽和设备资源
    在IPV6,广播报文传输方式被取消
  • 组播 实现 “一对多” 组播 非常适合“一对多的”传输场景, 只有加入特定 组播组 的主机才能接收到组播数据。传输组播数据包时,无需发送多个仅发送一个就可以,组播设备会根据情况进行转发或拷贝组播数据。
    相同的组播报文,在同链路上只有一份报文,这样大提高了网络资源利用率。

dgram 模块

在Node中,使用dgram模块搭建UDP服务
使用dgram来创建套接字,套接字既可以作为客户端接收数据,也可以作为服务器端发送数据

const dgram = require('dgram')
const socket = dgram.createSocket('udp4')

socket的方法

API说明
bind()绑定端口和主机
address()返回Socket地址对象
close()关闭Socket并停止监听
send()发送信息
addMembership()添加组播成员
dropMembership()移除组播成员
setBroadcast()设置是否启动广播
setTTL()设置数据报存活时间
setMulticastTTL()设置组播数据报存活时间

socket事件

事件名触发时机
listening监听成功时触发,仅触发一次
message接收到消息时触发
error发生错误时触发
close关闭socket时触发

UDP单播实现

服务器端

const dgram = require('dgram')
const server = dgram.createSocket('udp4')

// 绑定端口号成功时触发
server.on('listening', () => {
  const address = server.address()
  console.log('服务器运行在:' + address.address + ':' + address.port);
})
// 接收到消息时触发
server.on('message', (msg, remoteInfo) => {
  console.log(`服务器接收到来自:${remoteInfo.address}:${remoteInfo.port}的信息:${msg}`);
  // 向客户端发送信息  
  server.send('客户端你好,我是服务器端', remoteInfo.port, remoteInfo.address);
})
// 发生错误时触发
server.on('error', err => {
  console.log(`发生错误了,${JSON.stringify(err)}`);
})

// 将服务绑定到某个端口上运行 
server.bind(3000)

客户端

const dgram = require('dgram')  
const client = dgram.createSocket('udp4')

// 向localhost:3000 发送信息
client.send('hello world',3000,'localhost')
// 绑定端口号成功时触发
client.on('listening',()=>{
    const address = client.address()
    console.log('客户端运行在:'+address.address+':'+address.port);
})
// 接收到消息时触发
client.on('message',(msg,remoteInfo)=>{
    console.log(`客户端接收到来自:${remoteInfo.address}:${remoteInfo.port}的信息:${msg}`); 
})
// 发生错误时触发
client.on('error',err=>{
    console.log(`发生错误了,${JSON.stringify(err)}`); 
})

image.png

UDP广播实现

客户端

const dgram = require('dgram')
const client = dgram.createSocket('udp4')

client.on('listening', () => {
  const address = client.address()
  console.log('客户端运行在 ' + address.address + ':' + address.port);
})

client.on('message', (msg, remoteInfo) => {
  console.log(`客户端接收到来自 ${remoteInfo.address}:${remoteInfo.port}的信息:${msg}`);
})

client.on('error', err => {
  console.log(`发生错误了,${JSON.stringify(err)}`);
})
// 客户端绑定在8000端口上运行
client.bind(8000)

服务器端

const dgram = require('dgram')
const server = dgram.createSocket('udp4')

// 广播次数
let sendTime = 1

server.on('listening', () => {
  const address = server.address()
  console.log('服务器运行在 ' + address.address + ':' + address.port);
  // 服务运行成功后启用广播传输方式
  server.setBroadcast(true)
  // 关闭广播
  // server.setBroadcast(false)

  // 每隔两秒广播一次数据
  setInterval(() => {
    server.send('客户端你好,我是服务器端广播数据 *' + sendTime++, 8000, '255.255.255.255');
  }, 2000)
})

server.on('message', (msg, remoteInfo) => {
  console.log(`服务器接收到来自 ${remoteInfo.address}:${remoteInfo.port}的信息:${msg}`);
  server.send('客户端你好,我是服务器端', remoteInfo.port, remoteInfo.address);
})

server.on('error', err => {
  console.log(`发生错误了,${JSON.stringify(err)}`);
})

// 将服务绑定到某个端口上运行 
server.bind(3000)

image.png

UDP组播实现

客户端

const dgram = require('dgram')
const client = dgram.createSocket('udp4')


client.on('listening', () => {
  const address = client.address()
  console.log('客户端运行在 ' + address.address + ':' + address.port);
  // 当前客户端添加到组播组中
  client.addMembership('224.0.1.100')
})

client.on('message', (msg, remoteInfo) => {
  console.log(`客户端接收到来自 ${remoteInfo.address}:${remoteInfo.port}的信息:${msg}`);
})

client.on('error', err => {
  console.log(`发生错误了,${JSON.stringify(err)}`);
})
// 客户端绑定在8000端口上运行
client.bind(8000)

服务器端

const dgram = require('dgram')
const server = dgram.createSocket('udp4')

// 组播次数
let sendTime = 1

server.on('listening', () => {
  const address = server.address()
  console.log('服务器运行在 ' + address.address + ':' + address.port);
  // 每隔两秒组播一次数据
  setInterval(() => {
    server.send('客户端你好,我是服务器端组播数据 *' + sendTime++, 8000, '224.0.1.100');
  }, 2000)
})

server.on('message', (msg, remoteInfo) => {
  console.log(`服务器接收到来自 ${remoteInfo.address}:${remoteInfo.port}的信息:${msg}`);
  server.send('客户端你好,我是服务器端', remoteInfo.port, remoteInfo.address);
})

server.on('error', err => {
  console.log(`发生错误了,${JSON.stringify(err)}`);
})

// 将服务绑定到某个端口上运行 
server.bind(3000)

image.png

搭建HTTP服务

TCPUDP 是网络传输层的协议,它们可以构建高效的网络应用,但是对于经典的浏览器和服务器通信场景使用传输层协议会很麻烦。
对于上述场景,基于传输层之上制定了更上一层的通信协议:HTTP,由于 HTTP 协议本身并不不考虑数据如何传输和其他细节问题,所以他属于网络应用层。
Node 提供 httphttps 模块,用于 HTTPHTTPS 的封装

http模块

const HTTP = require('http')
const server = HTTP.createServer()

server方法

方法说明
close关闭服务
listening获取服务状态

server事件

方法说明
close关闭服务
require获取服务状态

请求对象request

属性说明
method请求方式
url请求地址
headers请求头
httpVersion请求HTTP协议版本

响应对象response

API说明
end()结束响应
setHeader(name,value)设置响应头
removeHeader(name,value)删除响应头
statusCode设置响应状态码
write写入响应数据
writeHead写入响应头

创建基本HTTP服务

const HTTP = require('http')
const hostName = '127.0.0.1'
const port = 3000
const server = HTTP.createServer((req, res) => {
  // 设置响应状态码
  res.statusCode = 200
  // 设置响应状态码,设置响应体内容为文本格式
  res.setHeader('Content-Type', 'text/plain')
  // 结束响应,返回数据
  res.end('Hello World\n')
})

// 服务运行成功
server.listen(port, hostName, () => {
  console.log(`服务运行在:${hostName}:${port}`)
})

获取url

const http = require('http')

const port = 3000
const hostName = '127.0.0.1'

const server = http.createServer((req, res) => {
  // 获取url
  const url = req.url
  if (url === '/') {
    res.end('Hello Home')
  } else if (url === '/a') {
    res.end('Hello A')
  } else if (url === '/b') {
    res.end('Hello B')
  } else {
    // 访问接口成功,响应状态码默认为200, 这里要手动设置为404
    res.statusCode = 404
    res.end('404 Not Found')
  }
})

server.listen(port, hostName, () => {
  console.log('服务运行成功');
})

image.png

image.png

image.png

响应HTML内容

http 响应的内容默认为文本格式的,可以通过设置响应头响应 HTML 内容

const http = require('http')
const port = 3000
const hostName = '127.0.0.1'
const server = http.createServer((req, res) => {
  // 设置响应头,返回内容为HTML格式,为了避免中文乱码 编码采用UTF-8
  res.setHeader('Content-Type', 'text/html;charset=utf-8')
  res.end('<h1>Hello World></h1><div>你好,世界</div>')
})
server.listen(port, hostName, () => {
  console.log('服务运行成功');
})

image.png

处理项目中的静态资源

当后端响应的HTML内容中包含静态资源,这些静态资源也会以请求的形式发送给后端。

  • 新建 index.html ,访问根路径时向前端返回 html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./assets/css/index.css">
</head>

<body>
  <h1>Hello World</h1>
  <div>你好,世界</div>
  <script src="./assets/js/index.js"></script>
</body>

</html>
  • 新建 assets 文件夹,存放 cssjs 文件
/* assets/css/index.css */ 
h1 {
  color: pink;
}

// assets/js/index.js  
console.log('assets/index.js 加载了');
  • 搭建web服务
const http = require('http')
const fs = require('fs')
const path = require('path')
const port = 3000
const hostName = '127.0.0.1'
const server = http.createServer((req, res) => {
  // 获取url
  const url = req.url
  // 访问根路径返回html资源
  if (url === '/') {
    // 获取html资源的绝对路径
    const HTML_Path = path.resolve(__dirname, 'index.html')
    fs.readFile(HTML_Path, (err, data) => {
      if (err) throw err
      // 设置响应状态码
      res.statusCOde = 200
      // 设置响应内容的文本类型、编码格式
      res.setHeader('Content-Type', 'text/html;charset=utf-8;')
      // 响应html资源
      res.end(data)
    })
    // Html中的访问css资源
  } else if (url === '/assets/css/index.css') {
    // 获取css资源的绝对路径
    const HTML_Path = path.resolve(__dirname, 'assets/css/index.css')
    fs.readFile(HTML_Path, (err, data) => {
      if (err) throw err
      // 设置响应状态码
      res.statusCOde = 200
      // 设置响应内容的文本类型、编码格式
      res.setHeader('Content-Type', 'text/css;charset=utf-8;')
      // 响应html资源
      res.end(data)
    })
  } else if (url === '/assets/js/index.js') {
    // 获取js资源的绝对路径
    const HTML_Path = path.resolve(__dirname, 'assets/js/index.js')
    fs.readFile(HTML_Path, (err, data) => {
      if (err) throw err
      // 设置响应状态码
      res.statusCOde = 200
      // 设置响应内容的文本类型、编码格式
      res.setHeader('Content-Type', 'text/javascript;charset=utf-8;')
      // 响应html资源
      res.end(data)
    })
  }

})
server.listen(port, hostName, () => {
  console.log('服务运行成功');
})

image.png

对资源进行统一处理

在上述操作中,对静态资源进行了处理。但是每种资源单独判断处理增加了很多重复代码,也不利于后期新增其他类型资源的维护工作,这里可以对资源进行统一处理。

  • 只能请求的url以 /assets/ 开头是视为在请求资源
  • 使用npm包 mime 可以根据文件后缀名,获取对应的http内容格式,例如,.css的资源类型为 text/css
const http = require('http')
const fs = require('fs')
const path = require('path')
const mime = require('mime')
const port = 3000
const hostName = '127.0.0.1'
const server = http.createServer((req, res) => {
  // 获取url
  const url = req.url
  // 访问根路径返回html资源
  if (url === '/') {
    // 获取html资源的绝对路径
    const HTML_Path = path.resolve(__dirname, 'index.html')
    fs.readFile(HTML_Path, (err, data) => {
      if (err) throw err
      // 设置响应状态码
      res.statusCOde = 200
      // 设置响应内容的文本类型、编码格式
      res.setHeader('Content-Type', 'text/html;charset=utf-8;')
      // 响应html资源
      res.end(data)
    })
  } else if (url.startsWith('/assets/')) {
    // 获取资源路径
    const assetsPath = path.resolve(__dirname, url)
    fs.readFile(assetsPath, (err, data) => {
      if (err) throw err
      // 设置响应状态码
      res.statusCOde = 200
      const type = mime.getType(url)
      // 设置响应内容的文本类型、编码格式
      res.setHeader('Content-Type', type + ';charset=utf-8;')
    })
  }
})
server.listen(port, hostName, () => {
  console.log('服务运行成功');
})

搭建HTTPS服务

HTTP协议非常不安全,它存在以下隐患

  • 通信内容被窃听,第三方可以截获并查看通信内容
  • 通信内容被篡改,第三方可以截获并修改通信内容
  • 通信身份被冒充,第三方可以冒充客户端和服务器端参与通信 HTTPS的出现就是为了解决这些问题,HTTPS是基于TLS/SSL的HTTP协议,在HTTP中数据是以明文形式传输的,而HTTPS是将数据加密后进行传输。所以HTTPS可以说是HTTP的安全版本。

HTTPS协议 = HTTP协议+SSL/TLS协议
在HTTPS数据传输过程中,需要用SSL/TLS对数据进行加密和解密,需要用HTTP对加密后的数据进行传输;TLS/SSL是一对公钥/私钥结构,传输的数据采用对称加密,对数据加密的密钥采用非对称进行加密,在一定程度上保证了数据的安全。
但是如此还是无法解决,第三方冒充身份参与通信的问题,例如,正常是客户端与服务器直接通信,如果第三方介入的话,他可以伪装成服务器截取密钥对,对数据造成泄漏。

image.png

这个解决有第三方冒充身份参与通信的问题,TLS/SSL引入了数字证书来认证,数字证书是指在网络通信各方身份信息的一个数字认证,人们可以在网上用它来识别对方的身份。他与直接使用公钥不同,数字证书包含了服务器的名称和主机名、服务器的公钥、签名颁发机构的名称、来自签名颁发机构的签名,连接建立前,会通过证书中的签名确认收到的公钥是否是来自目标服务器的,而非冒充身份的服务器。
数字证书是由第三方数字证书授权机构(简称CA机构)所颁发的,CA机构也有很多代理商,例如阿里腾讯等。

模拟CA机构,生成本地证书

数字证书去可以在阿里、腾讯那里申请,大部分是收费的而且申请数字证书的服务器有域名等要求,这里只模拟CA机构,生成本地证书

// 执行以下代码生成 本地证书
openssl req -newkey rsa:2048 -nodes -keyout rsa_private.key -x509 -days 365 -out cert.crt

证书、私钥和公钥都是通过 openssl 生成的,使用 openssl 需要先安装配置环境,详情请移步 ❤ NodeJSの进阶【1】— 核心API

搭建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);