《新时期的Node.js入门》学习日记-常用模块(上)

0xGeekCat · 2020-8-27 · 次阅读


Module

JavaScript的模块规范

JavaScript对模块规范的强调恰恰是其缺陷的体现,这主要是由历史原因造成的。在其他常见编程语言例如Java、 C++中,模块规范从未被如此刻意强调过,也没有分化出像JavaScript这般多样的标准

目前流行的JavaScript模块规范有两种

  • CommonJS

    其目标很远大,它的愿景是将来JavaScript不仅仅运行在浏览器内部,而是作为一门独立的编程语言在各种领域发挥作用,为此需要一种通用的模块规范。

    CommonJS将每个文件都看作一个模块,模块内部定义的变量都是私有的,无法被其他模块使用,除非使用预定义的方法(exports, require)将内部的变量暴露,其最为出名的实现就是Node.js,其一个显著的特点就是模块的加载同步,就目前来说,受限于宽带速度,并不适用于浏览器中的JavaScript

  • AMD

    AMD意思就是异步模块定义。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。依赖这个模块的代码定义在一个回调函数中,等到加载完成之后这个回调函数才会运行。目前在前端流行的RequireJS就是AMD规范的一种实现

此外,ES6中也提出了一种模块机制,在以后章节再做解释

require及其运行机制

Node遵循CommonJS模块规范,也就是使用require关键字来加载模块

let person = {
    talk: function () {
        console.log("I'm talking")
    },

    listen: function () {
        console.log("I'm listening")
    }
}

module.exports = person

👆实现一个自定义模块,该模块提供了一个接口(person),然后使用module.exports将该接口暴露给外部使用,外部的代码想要使用person.js中的方法,需要使用require关键字引入该接口

require('./person').talk()

require关键字并不依赖于exports,也可以加载一个没有暴露任何方法的模块,但通常没什么意义

重复引入

在C++中,通常使用#IFNDEF(if not defined)等关键字来避免头文件的重复引入,在Node中无须关心这个问题,因为Node默认先从缓存中加载模块,一个模块被第一次加载后,就会在缓存中维持一个副本,如果遇到重复加载的模块会直接提取缓存中的副本,也就是说在任何情况下每个模块都只在缓存中有一个实例

require()加载模块是同步而非异步原因有三点

  • 公共依赖的模块,自然要一步加载到位
  • 模块个数有限,且Node会自动缓存已经加载的模块,再加上访问的都是本地文件,产生的IO开销几乎可以忽略
  • Node程序运行在服务器端,很少遇到需要频繁重启服务,所以在服务启动时在加载上花点时间也没有什么影响

require的缓存策略

Node会自动缓存经过require引入的文件,使得下次再引入不需要经过文件系统而是直接从缓存中读取。这种缓存是基于文件路径定位的,这表示即使有两个完全相同的文件,但它们位于不同的路径下,也会在缓存中维持两份

console.log(require.cache)

截屏2020-08-26 下午8.02.30

上面输出的是test.js在缓存中的信息,其中path表示的是模块引入时Node的查找路径,即从当前目录下的node_modules开始,一直到磁盘根目录为止

require的隐患

当调用require加载一个模块时,模块内部的代码都会被调用,有时候这可能会带来隐藏的bug

// module.js
function test() {
    setInterval(function () {
        console.log('test')
    }, 1000)
}
test() 👈

module.exports = test

// test.js
let test = require('./module')

test.js试着运行一下会发现会每隔一秒输出test,同时run.js进程不会自主退出

加载一个模块相当于执行模块内部的代码,在module.js中由于设置了一个不间断的定时器,导致run.js也会一直运行

设想一种情景,当调用某个已经编写完成的模块时,明明所有的调用都已经结束,但调用者进程无论如何都不会退出,这很可能是被调用的模块内部有一个隐蔽的循环或者一个一直打开的数据库连接,这个问题在开发过程中可能不会被注意到或者不会被触发,如果真正到了生产环境,这种情况可能导致严重的内存泄露

这一方面告诉我们要对引用未知的模块保持警惕,即使那个模块是自己写的;另一方面也揭示了测试的重要性

模块化与作用域

Node和JavaScript中的this指向有一些区别,其中Node控制台和脚本文件的策略也不一样。对于浏览器中的JavaScript来说,无论是在脚本或者是Chrome控制台中,其this的指向和行为都是一致的;而Node则不是

  • 控制台中的this

    截屏2020-08-26 下午11.11.23

    在Chrome控制台中this指向global对象,全局变量会被挂载到global下

    ❗️注意是var a不能用let alet在之后的章节进行讨论

  • 脚本中的this

    console.log(this) 👈 {}
    
    var a = 10
    console.log(this.a) 👈 undefined 
    console.log(global.a) 👈 undefined

    全都是undefined,说明第一行代码定义的变量a并没有挂载在全局的this或者global对象

    如果声明变量时不使用var或者let关键字

    a = 10
    console.log(this.a) 👈 undefined 
    console.log(global.a) 👈 10 

    Node脚本文件中定义的全局this实际上指向module.exports

    this.a = 10
    console.log(module.exports.a) 👈 10

    🔔控制台的全局this和global可以看作是同一对象,而在脚本文件中,二者并不等价

Node中的作用域种类

  • 全局作用域

    如果一个变量没有用var、 let或者const之类的关键字修饰,那么它就是属于全局作用域,定义在全局作用域上的变量可以通过global对象访问;位于全局作用域中的变量,即使是在不同的文件中也能访问到

  • 模块作用域

    代码文件顶层(不在任何方法,对象中)使用var、let或者const修饰的变量都位于模块作用域中,不同模块作用域之间的作用域是隔离的。模块作用域中的this指向module.exports

  • 函数作用域

    有效范围位于函数体内

  • 块级作用域

    ES2015中引入的let关键字提供了块级作用域的支持

Buffer

Buffer是Node特有(区别于浏览器JavaScript)的数据类型,主要用来处理二进制数据,在前端JavaScript中,和二进制数据打交道的机会比较少(ES2015增加了ArrayBuffer类型,用来操作二进制数据流)。而Node在进行Web开发时经常需要和前端进行数据通信,二进制数据流十分常见(例如传输一张GIF图片),因此Node除了String外,还内置了Buffer这一数据类型,它是Node作为运行时对JavaScript做的扩展

Buffer属于固有(built-in)类型,因此无须使用require进行引入

在文件操作和网络操作中,如果不显式声明编码格式,其返回数据的默认类型就是Buffer

require('fs').readFile('foo.txt', function (err, results) {
    console.log(results) 👈 <Buffer 49 27 6d 20 61 20 68 61 63 6b 65 72>
})

最后打印出的是十六进制的数据,由于纯二进制格式太长而且难以阅读,Buffer通常表现为十六进制的字符串

Buffer的构建与转换

可以使用Buffer类初始化一个Buffer对象,参数可以是由二进制数据组成的数组也可以是字符串

var buffer = Buffer.from([0x49, 0x27, 0x6d, 0x20, 0x61, 0x20, 0x68, 0x61, 0x63, 0x6b, 0x65, 0x72]);
var buffer = Buffer.from("I'm a hacker")

👆代码写法与书中例子不相符,因为在现如今的Node API中,Buffer()方法被标记为Deprecated,表示已不推荐使用,因为其存在漏洞;目前推荐的是使用Buffer.from方法来初始化一个Buffer对象,如果想把一个Buffer对象转成字符串形式,还需要使用toString方法

截屏2020-08-27 上午12.05.56

Buffer支持的编码类型种类有限,仅支持ASCII,Base64,Binary,Hex,UTF-8,UTF-16LE/UCS-2

Buffer还提供了isEncoding方法来判断是否支持转换为目标编码格式

console.log(Buffer.isEncoding("UTF-8"))
console.log(Buffer.isEncoding("UTF-7"))

如果toString在调用时不包含任何参数,那么就会默认采用UTF-8编码,并转换整个Buffer对象

var buffer = Buffer.from([0x49, 0x27, 0x6d, 0x20, 0x61, 0x20, 0x68, 0x61, 0x63, 0x6b, 0x65, 0x72]);
console.log(buffer.toString('UTF-8', 0, 12)) 👈 I'm a hacker

Buffer的拼接

Buffer一个常见的使用场景是用来处理HTTP的post请求

var body = ''
req.setEncoding('utf8')
req.on('data', function (chunk) {
    body += chunk 👈 此处包含了一个隐式的编码转换
})
req.on('end', function () {})

body += chunk相当于body += chunk.toString(),当上传字符全都是英文的时候固然没关系,但如果字符串中包含中文或者其他语言,由于toString方法默认使用utf-8编码,这时就有可能出现乱码

var rs = require('fs').createReadStream('foo.txt', {highWaterMark: 10})

var data = ''
rs.on('data', function (chunk) {
    data += chunk
})
rs.on('end', function () {
    console.log(data)
})

highWaterMark

最高水位线表示内部缓冲区最多能容纳的字节数,如果超过这个大小,就停止读取资源文件,默认值是64KB

假设文件大小为100KB,那么在默认情况下,系统就会每次从文件里读取64KB大小的数据,随后触发data事件;chunk的大小即为highWaterMark的大小;然后接着读取36KB大小的文件,再次触发data事件;随后文件读取结束,触发end事件;如果highWaterMark设置得很小,那么就会发生多次系统调用,这会对性能造成影响

运行例子,可以看到输出中产生了乱码

截屏2020-08-27 上午8.48.04

在utf-8中一个汉字占三个字节,那么将highwatermark设置为10后,每三个字之后都会有一个字被截断,因此在调用toString方法的时候出现了乱码

打印例子观察chunk,可以发现刚好在这里字符被截断

截屏2020-08-27 上午8.52.26

目前👆代码已被舍弃,官方的推荐做法是使用push方法来拼接Buffer

var rs = require('fs').createReadStream('foo.txt', {highWaterMark: 10})

var data = []
rs.on('data', function (chunk) {
    data.push(chunk)
})
rs.on('end', function () {
    var buf = Buffer.concat(data)
    console.log(buf.toString())
})
// 你好,世界

先将Buffer放到数组里面,等待传输完成后再进行转换就不会出现乱码

File System

File System是Node中使用最为频繁的模块之一,该模块提供了读写文件的能力,是借助于底层的linuvC++ API实现的

浏览器中的JavaScript没有读写本地文件系统的能力(忽略IE中的ActiveX),而Node作为服务器编程语言,文件系统API是必需的

👇学习几种常见API

readFile

截屏2020-08-27 上午10.07.22

异步读取文本文件中的内容

require('fs').readFile('foo.txt', function (err, data) {
    if (err)
        throw err
    console.log(data)
})

readFile会将一个文件的全部内容都读到内存中,适用于体积较小的文本文件;如果你有一个数百MB大小的文件需要读取,建议选择stream。readFile读出的数据需要在回调方法中获取,而readFileSync直接返回文本数据内容

var data = require('fs').readFileSync('foo.txt', {encoding: "UTF-8"})
console.log(data)

如果不指定readFile的encoding配置,readFile会直接返回Buffer格式

截屏2020-08-27 上午9.46.53

writeFile

截屏2020-08-27 上午10.08.25

WriteFile的第一个参数为文件名,如果不存在,则会创建它(默认的flag为w)

require('fs').writeFile('foo.txt', "I'm a hacker", {flag: 'a', encoding: 'utf-8'},
    function (err) {
    if (err) {
        console.log(err)
    } else {
        console.log('success')
    }
})

截屏2020-08-27 上午10.12.54

fs.stat(path, callback)

stat方法通常用来获取文件的状态,通常开发者可以在调用open、read或者write方法之前调用fs.stat方法,用来判断该文件是否存在

require('fs').stat('foo.txt', function (err, result) {
    if (err) {
        console.log(err)
    } else {
        console.log(result)
    }
})

如果文件存在,result就会返回文件的状态信息

截屏2020-08-27 上午11.36.16

如果文件不存在,则会出现Error: ENOENT: no such file or directory的错误

fs.statfs.fstat的区别

File System模块还有一个fstat方法

截屏2020-08-27 上午11.40.36

两个方法唯一的区别是fstat方法第一个参数是文件描述符,因此其通常搭配open方法使用,因为open方法返回的结果就是一个文件描述符

👇代码运行结果与fs.stat相同

var fs = require('fs')

fs.open('foo.txt', 'a', function (err, fd) {
    if (err) {
        console.log(err)
    } else {
        console.log(fd)
    }

    fs.fstat(fd, function (err, stats) {
        if (err) {
            console.log(err)
        } else {
            console.log(stats)
        }
    })
})

readdir

截屏2020-08-27 下午5.44.26

获取目录下的所有文件名是一个常见的需求,实现这个功能只需要fs.readdir以及fs.stat两个API,readdir用于获取目录下的所有文件或者子目录,stat用来判断具体每条记录是文件还是子目录

var fs = require('fs')

function getAllFileFromPath(path) {
    fs.readdir(path, function (err, res) {
        for (var subPath of res) {
            var statObj = fs.statSync(path + '/' + subPath)
            if (statObj.isDirectory()) {
                console.log("Dir:", subPath)
                getAllFileFromPath(path + '/' + subPath)
            } else {
                console.log("File:", subPath)
            }
        }
    })
}

getAllFileFromPath(__dirname)

截屏2020-08-27 下午5.08.56

HTTP服务

HTTP模块是Node的核心模块,主要提供一系列用于网络传输的API,这些API大都位于比较底层的位置,可以让开发者自由地控制整个HTTP传输过程

创建HTTP服务器

var http = require('http')
var server = http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'})
    res.end("0xGeekCat")
})

server.listen(3000)

👆使用createServer方法创建一个简单的HTTP服务器,该方法返回一个http.server类的实例,createServer方法包含一个匿名的回调函数,该函数有两个参数reqres,它们是InComingMessage和ServerResponse的实例。分别表示HTTP的request和response对象,服务器创建完成后Node进程开始循环监听端口

访问127.0.0.1:3000的结果

截屏2020-08-27 下午5.19.47

HTTP请求还会触发connection和request事件,👇监听来自客户端的事件

截屏2020-08-27 下午5.38.35

❗️观察结果发现发生了两次连接请求适应浏览器还访问了favicon.ico

截屏2020-08-27 下午5.40.17

简单的静态文件服务器

var http = require('http')
var fs = require('fs')

var server = http.createServer(function (req, res) {
    if (req.url === '/') {
        var fileList = fs.readdirSync('./')
        res.writeHead(200, {"Content-Type": "text/plain"})
        res.end(fileList.toString())
    } else {
        var path = req.url
        fs.readFile('.' + path, function (err, data) {
            if (err) {
                res.end("Internal error")
                throw err
            }
            res.writeHead(200, {"Content-Type": "text/plain"})
            res.end(data)
        })
    }
})

var port = 3000
server.listen(port)
console.log("Listening on 3000")

process.on("uncaughtException", function () {
    console.log("got error")
})

处理HTTP请求

当处理HTTP请求时,最先做的事就是获取请求的URL、method等信息。Node将相关的信息都封装在req对象中,该对象是IncomingMessage的实例

method

var method = req.method

HTTP请求method的值通常是get、post、put、delete、update这5个关键字之一,以get和post最为常见

var headers = req.headers

截屏2020-08-27 下午7.53.17

request body

Node使用stream处理HTTP的请求体,stream注册了data和end两个事件

var body = []
req.on('data', function (chunk) {
    body.push(chunk)
}).on('end', function () {
    body = Buffer.concat(body).toString()
})

Response对象

statusCode

Node中如果开发者不手动设置,那么状态码的值会默认为200。但200并不适用所有场景

response header

通过setHeader方法设置response的头部信息,但其只能设置response header单个属性,如果想要一次性设置所有的响应头和状态码,则使用writeHead方法,writeHead方法用于定义HTTP响应头,包括状态码等一系列属性;调用该方法后,服务器向客户端发送HTTP响应头

res.writeHead(200, {
    'Content-Length': Buffer.byteLength(body),
    'Content-Type': 'text/plain'
})

有时开发者并不会显式调用该方法,当调用end方法时也会调用writeHead方法,此时statusCode会自动设置成200

response body

response对象是一个writableStream实例,可以直接调用write方法进行写入,写入完成后再调用end方法将该stream发送到客户端

res.write('<html>')
res.write('<body>')
res.write('<h1>0xGeekCat</h1>')
res.write('</body>')
res.write('</html>')
res.end()

更好的方法是直接将response body作为end方法的参数进行返回

res.end('<html><body><h1>0xGeekCat</h1></body></html>')

response.end

end方法在每个HTTP请求的最后都会被调用,当客户端的请求完成之后,开发者应该调用该方法来结束HTTP请求。通常情况下,如果不调用end方法,用户最直观的感受通常是浏览器位于地址栏左边的叉号会一直存在,表示该请求尚未完成

👇已完成

截屏2020-08-27 下午10.13.32

👇未完成

截屏2020-08-27 下午10.13.54

end方法支持字符串或者buffer作为参数,可以指定HTTP请求最后返回的数据,该数据会在浏览器页面上显示;如果定义了回调方法,那么会在end返回后调用

res.end("0xGeekCat", function () {
    console.log('end')
})

上传数据

传统的Web开发中,最常用的HTTP请求只有get和post两种。get请求的报文内容很简单,只有请求行和请求头部;post请求由于要上传数据,因此需要包含请求体的内容,有两个相关的属性经常被用到,分别是content-typecontent-length

首先判断请求方法的类型

var http = require('http')

var server = http.createServer(function (req, res) {
    if (req.method === 'get') {
        // TODO
    }
    if (req.method === 'post') {
        // TODO
    }
})

提交表单

<form action="/login" method="post" id="form1">
    <input type="text" name="username" id="username"><br>
    <input type="password" name="password" id="password"><br>
    <input type="submit" name="submit" id="submit">
</form>

server端的代码

var http = require('http')
var fs = require('fs')

var server = http.createServer(function (req, res) {
    if (req.url === '/login') {
        switch (req.method) {
            case 'GET':
                // 使用流来加载login.html
                fs.createReadStream('login.html').pipe(res) 👈 将Readable流中的数据传输给Writable流 readable.pipe(writeable)
                break
            case 'POST':
                console.log(req.headers)
                break
            default :
                console.log('other request')
                break
        }
    } else {
        res.writeHead(302, {
            'Location': '/login'
        })
        res.end()
    }
})

server.listen(3000)

截屏2020-08-28 上午10.45.27

如果不使用Express之类的Web框架,Node实现的服务器代码通常都是上面这种结构,获取请求的URL之后,再针对不同的HTTP method进行处理,缺点就是要写很多条件控制语句

当用户在浏览器输入用户名、密码并提交后,浏览器向127.0.0.1:3000/login发起post请求

打印头部信息

截屏2020-08-27 下午10.43.21

以表单形式提交数据时,请求头中的content-type为application/x-www-form-urlencoded

报文主体中的内容是通过数据流的形式来传输的,通过监听流事件的方式来获取数据

打印请求体

case 'POST':
    var body = []
    req.on('data', function (chunk) {
        body.push(chunk)
    }).on('end', function () {
        body = Buffer.concat(body).toString()
        console.log(body)
    })
    break

截屏2020-08-27 下午10.51.03

使用post上传文件

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" id=""><br>
    <input type="submit" value="submit" name="submit">
</form>

和只有字段值的表单不同的是,上传文件的表单要设置enctype="multipart/form-data"属性,自然文件上传时的header信息也有所不同

截屏2020-08-27 下午11.11.29

服务器处理上传文件

function dealUpload(req, res) {
    var form = new formidable.IncomingForm() 👈 创建formidable.IncomingFrom对象
    form.keepExtensions = true 👈 保持原有扩展名
    form.uploadDir = __dirname 👈上传目录为当前目录
    form.parse(req, function (err, fields, files) {
        if (err)
            throw err
        console.log(fields) 👈 { submit: 'submit' }
        console.log(files)
        res.writeHead(200, {"Content-Type": "text/plain"})
        res.end("upload finished")
    })
}

截屏2020-08-28 上午8.39.46

此时浏览器界面回显

截屏2020-08-28 上午8.40.51

如果想获取特定属性比如name

console.log(files.file.name)

HTTP客户端服务

HTTP模块除了能在服务端处理客户端请求之外,还可以作为客户端向服务器发起请求。这也是Node也能做出像electron那样的桌面软件的基础

截屏2020-08-28 上午8.57.01

var http = require('http')
http.get("http://127.0.0.1:3000/", function (res) {
    var statueCode = res.statusCode
    if (statueCode === 200) {
        var result = ""
        res.on("data", function (data) {
            result += data
        })
        res.on("end", function () {
            console.log(result.toString())
        })
        res.on("error", function (e) {
            console.log(e.message)
        })
    }
})

截屏2020-08-28 上午9.09.05

创建代理服务器

代理服务器相当于在客户端和目标服务器之间建立了一个中转,所有的访问和流量都经过这个服务器进行中转

var http = require("http");
var url = require("url");

http.createServer(function(req,res){
    console.log(req.url);
    var options = url.parse(req.url);
    options.headers = req.headers;

    var proxyRequest = http.request(options,function(pres){
        res.writeHead(pres.statusCode,pres.headers);
        pres.on('data',function (data) {
            res.write(data);
        });
        pres.on('end',function () {
            res.end();
        });

    });

    req.on('data',function(data){
        proxyRequest.write(data);
    });

    req.on('end',function(){
        proxyRequest.end();
    });

}).listen(80);

在本地创建了一个HTTP服务器,请求经由127.0.0.1:80进行转发,不过并不太清楚👆代码的具体使用,暂时仅简单了解

代理服务器可以有很多应用领域,例如使用它来缓存文件或者很多企业都会使用代理服务器来过滤掉一些广告和垃圾网站的URL,或者限制员工使用公司网络访问社交网站。有的企业访问npm下载第三方模块也需要配置代理。一些常用的屏蔽广告的浏览器插件大都也是依靠本地启动代理服务器来实现广告过滤的

反向代理

如果一个代理服务器可以代理外部的访问来访问内部网络时,这种代理方式就被称为反向代理

如果一个网站购买了CDN服务,那么当有来自外部(客户端)的请求时,并没有直接访问服务器的内容,而是访问距离用户最近的CDN节点。对于服务器来说,CDN就起到了反向代理的功能

TCP服务

如果开发者大多数时间都在进行Web站点的开发,那么TCP服务和HTTP服务相比,出场率并不高

TCP和Socket

❓网络服务需要Socket编程,TCP协议是用来传输数据的,但TCP协议和Socket有哪些区别

Socket是对TCP协议的一种封装方式,Socket本身并不是协议,而是一个编程接口,如果一种编程语言实现了socket接口,那么它就可以通过socket接口预定义的方法来解析使用TCP协议传输的数据流;socket并不是专门为TCP协议设计的,在设计之初就期望能兼容多种传输层协议

创建TCP服务器

Node中有三种Socket,分别对应实现TCP、UDP以及UNIX Socket,与这些相关的代码都位于Net模块中;UNIX Socket即UNIX Domain Socket,和面向网络的TCP、UDP不同,主要用于本地系统的进程间通信

var net = require('net')

var server = net.createServer(function (c) {
    console.log('client connected')
    c.on('end', function () {
        console.log('client disconnected')
    })
    c.write('hello\r\n')
    c.pipe(c) 
})

server.on('error', function (err) {
    throw err
})

server.listen(8124, function () {
    console.log('server bound')
})

如果服务器收到了一个连接请求,就会返回一个Hello字符串;如果在浏览器里打开localhost:8124的方式来访问,会出现GET http://localhost:8124/ net::ERR_INVALID_HTTP_RESPONSE的错误。原因也很简单,一CP服务器不会返回符合HTTP协议标准的响应

截屏2020-08-28 上午10.55.28

使用nc连接服务器后,服务器端出现👇回显表示服务器正常运行

截屏2020-08-28 上午10.56.32

c.pipe(c) 如果不添加的话会导致对服务器的输入无法输出,原因在之前对pipe的解释中可以理解

👇前者是不添加后者为添加

截屏2020-08-28 上午11.27.47

TCP客户端

const net =require('net')
const client = net.connect({port: 8124}, function () {
    console.log('connected to server')
    client.write('0xGeekCat')
})

client.on('data', function (data) {
    console.log(data.toString())
    client.end()
})

client.on("end", function () {
    console.log('disconnected from server')
})

截屏2020-08-28 上午11.31.12

SSL更安全的传输方式

对于企业网站,使用更加安全的数据传输是必要的,使用单纯的HTTP连接,所有的内容都以明文传输,这种方式是极不安全,就连通常被认为安全的post操作,其安全性也无法保证,因此需要更安全的HTTPS(HTTP+SSL)

👇描述SSL在网络通信中的位置

截屏2020-08-28 上午11.35.36

SSL - Secure Sockets Layer & TLS - Transport Layer Security

安全套接层协议及其继任者传输层安全协议是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。 SSL的一大优势在于它独立于上层协议,和HTTP结合即为HTTPS,和WebSocket结合即为WSS

通常认为SSL和TLS指代同一个标准,两者之间的差异极小

SSL原理

不同的SSL握手过程存在差异,有👇三种

  • 只验证服务器
  • 验证服务器和客户端
  • 恢复原有会话

👇是第一种建立连接的过程

客户端发送Client Hello消息,该消息包括SSL版本信息、一个随机数(假设它是random1)、一个session id(用来避免后续请求的握手)和浏览器支持的密码套件cipher suite,其内容和其的含义一样,是一个由加密算法名称组成的字符串,包括了以下4种用途的加密算法

  • 密钥交换算法:常用的有RSA、PSK等
  • 数据加密算法:常用的例如AES 256、RC4等
  • 报文认证信息码(MAC)算法:常用的例如MD5、SHA等。伪随机数(PRF)算法

👇字符串就是一个cipher suite

截屏2020-08-28 上午11.53.10

  • ECDHE_RSA:密钥交换算法
  • AES_128_GCM:数据加密算法

reference

《新时期的Node.js入门》 李锴