详解Node.js中的QUIC协议


本文摘自PHP中文网,作者青灯夜游,侵删。

在2019年3月,受到 NearForm 和 Protocol Labs 的支持,我开始为 Node.js 实现 QUIC 协议 支持。这个基于 UDP 的新传输协议旨在最终替代所有使用 TCP 的 HTTP 通信。

1.png

熟悉 UDP 的人可能会产生质疑。众所周知 UDP 是不可靠的,数据包经常会有丢失、乱序、重复等情况。 UDP 不保证高级协议(例如 HTTP)严格要求的 TCP 所支持的可靠性和顺序。那就是 QUIC 进来的地方。

QUIC 协议在 UDP 之上定义了一层,该层为 UDP 引入了错误处理、可靠性、流控制和内置安全性(通过 TLS 1.3)。实际上它在 UDP 之上重新实现了大多数 TCP 的特效,但是有一个关键的区别:与 TCP 不同,仍然可以不按顺序传输数据包。了解这一点对于理解 QUIC 为什么优于 TCP 至关重要。

【相关推荐:《nodejs 教程》】

QUIC 消除了队首阻塞的根源

在 HTTP 1 中,客户端和服务器之间所交换的所有消息都是连续的、不间断的数据块形式。虽然可以通过单个 TCP 连接发送多个请求或响应,但是在发送下一条完整消息之前,必须先等上一条消息完整的传输完毕。这意味着,如果要发送一个 10 兆字节的文件,然后发送一个 2 兆字节的文件,则前者必须完全传输完毕,然后才能启动后者。这就是所谓的队首阻塞,是造成大量延迟和不良使用网络带宽的根源。

HTTP 2 尝试通过引入多路复用来解决此问题。 HTTP 2 不是将请求和响应作为连续的流传输,而是将请求和响应分成了被称为帧的离散块,这些块可以与其他帧交织。一个 TCP 连接理论上可以处理无限数量的并发请求和响应流。尽管从理论上讲这是可行的,但是 HTTP 2 的设计没有考虑 TCP 层出现队首阻塞的可能性。

TCP 本身是严格排序的协议。数据包被序列化并按照固定顺序通过网络发送。如果数据包未能到达其目的地,则会阻止整个数据包流,直到可以重新传输丢失的数据包为止。有效的顺序是:发送数据包1,等待确认,发送数据包2,等待确认,发送数据包3……。使用 HTTP 1,在任何给定时间只能传输一个 HTTP 消息,如果单个 TCP 数据包丢失,那么重传只会影响单个 HTTP 请求/响应流。但是使用 HTTP 2,则会在丢失单个 TCP 数据包的情况下阻止无限数量的并发 HTTP 请求/响应流的传输。在通过高延迟、低可靠性网络进行 HTTP 2 通信时,与 HTTP 1 相比,整体性能和网络吞吐量会急剧下降。

2.png

在 HTTP 1 中,该请求会被阻塞,因为一次只能发送一条完整的消息。

3.png

在 HTTP 2 中,当单个 TCP 数据包丢失或损坏时,该请求将被阻塞。

4.png

在QUIC中,数据包彼此独立,能够以任何顺序发送(或重新发送)。

幸运的是有了 QUIC 情况就不同了。当数据流被打包到离散的 UDP 数据包中传输时,任何单个数据包都能够以任意顺序发送(或重新发送),而不会影响到其他已发送的数据包。换句话说,线路阻塞问题在很大程度上得到解决。

QUIC 引入了灵活性、安全性和低延迟

QUIC 还引入了许多其他重要功能:

  • QUIC 连接的运行独立于网络拓扑结构。在建立了 QUIC 连接后,源 IP 地址和目标 IP 地址和端口都可以更改,而无需重新建立连接。这对于经常进行网络切换(例如 LTE 到 WiFi)的移动设备特别有用。
  • 默认 QUIC 连接是安全的并加密的。 TLS 1.3 支持直接包含在协议中,并且所有 QUIC 通信都经过加密。
  • QUIC 为 UDP 添加了关键的流控制和错误处理,并包括重要的安全机制以防止一系列拒绝服务攻击。
  • QUIC 添加了对零行程 HTTP 请求的支持,这与基于 TCP 的 TLS 之上的 HTTP 不同,后者要求客户端和服务器之间进行多次数据交换来建立 TLS 会话,然后才能传输 HTTP 请求数据,QUIC 允许 HTTP 请求头作为 TLS 握手的一部分发送,从而大大减少了新连接的初始延迟。

5.png

为 Node.js 内核实现 QUIC

为 Node.js 内核实现 QUIC 的工作从 2019 年 3 月开始,并由 NearForm 和 Protocol Labs 共同赞助。我们利用出色的 ngtcp2 库来提供大量的低层实现。因为 QUIC 是许多 TCP 特性的重新实现,所以对 Node.js 意义重大,并且与 Node.js 中当前的 TCP 和 HTTP 相比能够支持更多特性。同时对用户隐藏了大量的复杂性。

“quic” 模块

在实现新的 QUIC 支持的同时,我们用了新的顶级内置 quic 模块来公开 API。当该功能在 Node.js 核心中落地时,是否仍将使用这个顶级模块,将在以后确定。不过当在开发中使用实验性支持时,你可以通过 require('quic') 使用这个 API。

1

const { createSocket } = require('quic')

quic 模块公开了一个导出:createSocket 函数。这个函数用来创建 QuicSocket 对象实例,该对象可用于 QUIC 服务器和客户端。

QUIC 的所有工作都在一个单独的 GitHub 存储库 中进行,该库 fork 于 Node.js master 分支并与之并行开发。如果你想使用新模块,或者贡献自己的代码,可以从那里获取源代码,请参阅 Node.js 构建说明。不过它现在仍然是一项尚在进行中的工作,你一定会遇到 bug 的。

创建QUIC服务器

QUIC 服务器是一个 QuicSocket 实例,被配置为等待远程客户端启动新的 QUIC 连接。这是通过绑定到本地 UDP 端口并等待从对等方接收初始 QUIC 数据包来完成的。在收到 QUIC 数据包后,QuicSocket 将会检查是否存在能够用于处理该数据包的服务器 QuicSession 对象,如果不存在将会创建一个新的对象。一旦服务器的 QuicSession 对象可用,则该数据包将被处理,并调用用户提供的回调。这里有一点很重要,处理 QUIC 协议的所有细节都由 Node.js 在其内部处理。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

const { createSocket } = require('quic')

const { readFileSync } = require('fs')

 

const key = readFileSync('./key.pem')

const cert = readFileSync('./cert.pem')

const ca = readFileSync('./ca.pem')

const requestCert = true

const alpn = 'echo'

 

const server = createSocket({

  // 绑定到本地 UDP 5678 端口

  endpoint: { port: 5678 },

  // 为新的 QuicServer Session 实例创建默认配置

  server: {

    key,

    cert,

    ca,

    requestCert

    alpn

  }

})

 

server.listen()

server.on('ready', () => {

  console.log(`QUIC server is listening on ${server.address.port}`)

})

 

server.on('session', (session) => {

  session.on('stream', (stream) => {

    // Echo server!

    stream.pipe(stream)

  })

 

  const stream = session.openStream()

  stream.end('hello from the server')

})

如前所述,QUIC 协议内置并要求支持 TLS 1.3。这意味着每个 QUIC 连接必须有与其关联的 TLS 密钥和证书。与传统的基于 TCP 的 TLS 连接相比,QUIC 的独特之处在于 QUIC 中的 TLS 上下文与 QuicSession 相关联,而不是 QuicSocket。如果你熟悉 Node.js 中 TLSSocket 的用法,那么你一定注意到这里的区别。

QuicSocket(和 QuicSession)的另一个关键区别是,与 Node.js 公开的现有 net.Sockettls.TLSSocket 对象不同,QuicSocketQuicSession 都不是 ReadableWritable的流。即不能用一个对象直接向连接的对等方发送数据或从其接收数据,所以必须使用 QuicStream 对象。

在上面的例子中创建了一个 QuicSocket 并将其绑定到本地 UDP 的 5678 端口。然后告诉这个 QuicSocket 侦听要启动的新 QUIC 连接。一旦 QuicSocket 开始侦听,将会发出 ready 事件。

当启动新的 QUIC 连接并创建了对应服务器的 QuicSession 对象后,将会发出 session 事件。创建的 QuicSession 对象可用于侦听新的客户端服务器端所启动的 QuicStream 实例。

阅读剩余部分

相关阅读 >>

node.js和java后台服务器的简单比较

聊聊node.js中json格式和excel格式的双向转换

node.js为什么总用mongo

深入了解node.js中的express框架

详解node.js buffer的使用

node.js爬取豆瓣数据实例

浅谈 node.js 中间件的工作原理

分享一个实用nodejs npm包:koa-csrf

node.js使用multer中间件上传文件

vue刷新404的问题解决方法

更多相关阅读请进入《node.js》频道 >>




打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

评论

管理员已关闭评论功能...