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

在2019年3月,受到 NearForm 和 Protocol Labs 的支持,我开始为 Node.js 实现 QUIC 协议 支持。这个基于 UDP 的新传输协议旨在最终替代所有使用 TCP 的 HTTP 通信。
熟悉 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 相比,整体性能和网络吞吐量会急剧下降。
在 HTTP 1 中,该请求会被阻塞,因为一次只能发送一条完整的消息。
在 HTTP 2 中,当单个 TCP 数据包丢失或损坏时,该请求将被阻塞。
在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 握手的一部分发送,从而大大减少了新连接的初始延迟。
为 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 |
|
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 |
|
如前所述,QUIC 协议内置并要求支持 TLS 1.3。这意味着每个 QUIC 连接必须有与其关联的 TLS 密钥和证书。与传统的基于 TCP 的 TLS 连接相比,QUIC 的独特之处在于 QUIC 中的 TLS 上下文与 QuicSession
相关联,而不是 QuicSocket
。如果你熟悉 Node.js 中 TLSSocket
的用法,那么你一定注意到这里的区别。
QuicSocket
(和 QuicSession
)的另一个关键区别是,与 Node.js 公开的现有 net.Socket
和 tls.TLSSocket
对象不同,QuicSocket
和 QuicSession
都不是 Readable
或 Writable
的流。即不能用一个对象直接向连接的对等方发送数据或从其接收数据,所以必须使用 QuicStream
对象。
在上面的例子中创建了一个 QuicSocket
并将其绑定到本地 UDP 的 5678 端口。然后告诉这个 QuicSocket
侦听要启动的新 QUIC 连接。一旦 QuicSocket
开始侦听,将会发出 ready
事件。
当启动新的 QUIC 连接并创建了对应服务器的 QuicSession
对象后,将会发出 session
事件。创建的 QuicSession
对象可用于侦听新的客户端服务器端所启动的 QuicStream
实例。
相关阅读 >>
如何使用ppa在ubuntu上安装最新的node.js和npm
javascript中promise.all和promise.race方法的介绍(附代码)
更多相关阅读请进入《node.js》频道 >>

Vue.js 设计与实现 基于Vue.js 3 深入解析Vue.js 设计细节
本书对 Vue.js 3 技术细节的分析非常可靠,对于需要深入理解 Vue.js 3 的用户会有很大的帮助。——尤雨溪,Vue.js作者