Heim > Artikel > Web-Frontend > Detaillierte Erläuterung des QUIC-Protokolls in Node.js
Im März 2019 begann ich mit Unterstützung von NearForm und Protocol Labs mit der Implementierung der QUIC-Protokollunterstützung für Node.js. Dieses neue UDP-basierte Transportprotokoll soll irgendwann die gesamte HTTP-Kommunikation über TCP ersetzen.
Personen, die mit UDP vertraut sind, haben möglicherweise Zweifel. Es ist bekannt, dass UDP unzuverlässig ist und Datenpakete häufig verloren gehen, nicht in der richtigen Reihenfolge sind und dupliziert werden. UDP garantiert nicht die von TCP unterstützte Zuverlässigkeit und Reihenfolge, die für Protokolle höherer Ebenen wie HTTP unbedingt erforderlich sind. Hier kommt QUIC ins Spiel.
Das QUIC-Protokoll definiert eine Schicht über UDP, die Fehlerbehandlung, Zuverlässigkeit, Flusskontrolle und integrierte Sicherheit (über TLS 1.3) in UDP einführt. Es implementiert tatsächlich die meisten Spezialeffekte von TCP zusätzlich zu UDP neu, allerdings mit einem entscheidenden Unterschied: Im Gegensatz zu TCP können Pakete immer noch in der falschen Reihenfolge übertragen werden. Dies zu verstehen ist entscheidend, um zu verstehen, warum QUIC TCP überlegen ist.
[Verwandte Empfehlung: „nodejs-Tutorial“]
In HTTP 1 sind alle zwischen dem Client und dem Server ausgetauschten Nachrichten kontinuierliche und ununterbrochene Datenblöcke bilden. Obwohl Sie mehrere Anfragen oder Antworten über eine einzige TCP-Verbindung senden können, müssen Sie warten, bis die vorherige Nachricht vollständig übertragen wurde, bevor Sie die nächste vollständige Nachricht senden. Das heißt, wenn Sie eine 10-Megabyte-Datei und dann eine 2-Megabyte-Datei versenden möchten, muss erstere vollständig übertragen werden, bevor letztere gestartet werden kann. Dies wird als Head-of-Line-Blockierung bezeichnet und ist die Ursache für hohe Latenz und eine schlechte Nutzung der Netzwerkbandbreite.
HTTP 2 versucht, dieses Problem durch die Einführung von Multiplexing zu lösen. Anstatt Anfragen und Antworten als kontinuierlichen Stream zu übertragen, unterteilt HTTP 2 Anfragen und Antworten in diskrete Blöcke, sogenannte Frames, die mit anderen Frames verschachtelt werden können. Eine TCP-Verbindung kann theoretisch eine unbegrenzte Anzahl gleichzeitiger Anforderungs- und Antwortströme verarbeiten. Obwohl dies theoretisch möglich ist, wurde HTTP 2 nicht darauf ausgelegt, die Möglichkeit einer Head-of-Line-Blockierung auf der TCP-Ebene zu berücksichtigen.
TCP selbst ist ein streng geordnetes Protokoll. Pakete werden serialisiert und in einer festen Reihenfolge über das Netzwerk gesendet. Wenn ein Paket sein Ziel nicht erreicht, wird der gesamte Paketfluss blockiert, bis das verlorene Paket erneut übertragen werden kann. Die gültige Reihenfolge ist: Paket 1 senden, auf Bestätigung warten, Paket 2 senden, auf Bestätigung warten, Paket 3 senden ... Mit HTTP 1 kann immer nur eine HTTP-Nachricht übertragen werden, und wenn ein einzelnes TCP-Paket verloren geht, wirken sich erneute Übertragungen nur auf einen einzelnen HTTP-Anforderungs-/Antwortstrom aus. Aber mit HTTP 2 wird eine unbegrenzte Anzahl gleichzeitiger HTTP-Anfrage-/Antwortströme blockiert, ohne dass ein einziges TCP-Paket verloren geht. Bei der Kommunikation über HTTP 2 über ein Netzwerk mit hoher Latenz und geringer Zuverlässigkeit nehmen die Gesamtleistung und der Netzwerkdurchsatz im Vergleich zu HTTP 1 dramatisch ab.
In HTTP 1 wird diese Anfrage blockiert, da jeweils nur eine vollständige Nachricht gesendet werden kann.
Wenn in HTTP 2 ein einzelnes TCP-Paket verloren geht oder beschädigt ist, wird die Anfrage blockiert.
In QUIC sind Pakete unabhängig voneinander und können in beliebiger Reihenfolge gesendet (oder erneut gesendet) werden.
Glücklicherweise ist das bei QUIC anders. Wenn ein Datenstrom zur Übertragung in diskrete UDP-Pakete verpackt wird, kann jedes einzelne Paket in beliebiger Reihenfolge gesendet (oder erneut gesendet) werden, ohne dass dies Auswirkungen auf andere gesendete Pakete hat. Mit anderen Worten: Das Problem der Leitungsüberlastung ist weitgehend gelöst.
QUIC bietet außerdem viele weitere tolle Funktionen:
Die Arbeit an der Implementierung von QUIC für den Node.js-Kernel begann im März 2019 und wird von NearForm und Protocol Labs gemeinsam gesponsert. Wir nutzen die hervorragende ngtcp2-Bibliothek, um eine umfassende Low-Level-Implementierung bereitzustellen. QUIC ist für Node.js sinnvoll, da es eine Neuimplementierung vieler TCP-Funktionen ist und viel mehr Funktionen unterstützen kann als das aktuelle TCP und HTTP in Node.js. während dem Benutzer ein großer Teil der Komplexität verborgen bleibt.
Bei der Implementierung der neuen QUIC-Unterstützung haben wir ein neues integriertes quic
-Modul der obersten Ebene verwendet, um die API verfügbar zu machen. Ob dieses Top-Level-Modul weiterhin verwendet wird, wenn das Feature im Node.js-Kern implementiert wird, wird zu einem späteren Zeitpunkt festgelegt. Wenn Sie jedoch experimentelle Unterstützung in der Entwicklung verwenden, können Sie diese API über require('quic')
verwenden. quic
模块来公开 API。当该功能在 Node.js 核心中落地时,是否仍将使用这个顶级模块,将在以后确定。不过当在开发中使用实验性支持时,你可以通过 require('quic')
使用这个 API。
const { createSocket } = require('quic')
quic
模块公开了一个导出:createSocket
函数。这个函数用来创建 QuicSocket
对象实例,该对象可用于 QUIC 服务器和客户端。
QUIC 的所有工作都在一个单独的 GitHub 存储库 中进行,该库 fork 于 Node.js master 分支并与之并行开发。如果你想使用新模块,或者贡献自己的代码,可以从那里获取源代码,请参阅 Node.js 构建说明。不过它现在仍然是一项尚在进行中的工作,你一定会遇到 bug 的。
QUIC 服务器是一个 QuicSocket
实例,被配置为等待远程客户端启动新的 QUIC 连接。这是通过绑定到本地 UDP 端口并等待从对等方接收初始 QUIC 数据包来完成的。在收到 QUIC 数据包后,QuicSocket
将会检查是否存在能够用于处理该数据包的服务器 QuicSession
对象,如果不存在将会创建一个新的对象。一旦服务器的 QuicSession
对象可用,则该数据包将被处理,并调用用户提供的回调。这里有一点很重要,处理 QUIC 协议的所有细节都由 Node.js 在其内部处理。
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.Socket
和 tls.TLSSocket
对象不同,QuicSocket
和 QuicSession
都不是 Readable
或 Writable
的流。即不能用一个对象直接向连接的对等方发送数据或从其接收数据,所以必须使用 QuicStream
对象。
在上面的例子中创建了一个 QuicSocket
并将其绑定到本地 UDP 的 5678 端口。然后告诉这个 QuicSocket
侦听要启动的新 QUIC 连接。一旦 QuicSocket
开始侦听,将会发出 ready
事件。
当启动新的 QUIC 连接并创建了对应服务器的 QuicSession
对象后,将会发出 session
事件。创建的 QuicSession
对象可用于侦听新的客户端服务器端所启动的 QuicStream
const { createSocket } = require('quic') const fs = require('fs') const key = readFileSync('./key.pem') const cert = readFileSync('./cert.pem') const ca = readFileSync('./ca.pem') const requestCert = true const alpn = 'echo' const servername = 'localhost' const socket = createSocket({ endpoint: { port: 8765 }, client: { key, cert, ca, requestCert alpn, servername } }) const req = socket.connect({ address: 'localhost', port: 5678, }) req.on('stream', (stream) => { stream.on('data', (chunk) => { /.../ }) stream.on('end', () => { /.../ }) }) req.on('secure', () => { const stream = req.openStream() const file = fs.createReadStream(__filename) file.pipe(stream) stream.on('data', (chunk) => { /.../ }) stream.on('end', () => { /.../ }) stream.on('close', () => { // Graceful shutdown socket.close() }) stream.on('error', (err) => { /.../ }) })Das Modul
quic
stellt einen Export bereit: die Funktion createSocket
. Mit dieser Funktion wird eine Instanz eines QuicSocket
-Objekts erstellt, das von QUIC-Servern und -Clients verwendet werden kann. Alle Arbeiten an QUIC finden in einem separaten GitHub-Repository statt, das vom Node.js-Masterzweig abgespalten und parallel dazu entwickelt wird. Wenn Sie das neue Modul verwenden oder Ihren eigenen Code beisteuern möchten, können Sie den Quellcode von dort erhalten, siehe die Node.js-Build-Anweisungen. Es ist jedoch noch in Arbeit und Sie werden mit Sicherheit auf Fehler stoßen. QuicSocket
-Instanz, die so konfiguriert ist, dass sie darauf wartet, dass Remote-Clients neue QUIC-Verbindungen initiieren. Dies erfolgt durch Bindung an den lokalen UDP-Port und Warten auf den Empfang des ersten QUIC-Pakets vom Peer. Nach dem Empfang eines QUIC-Pakets prüft QuicSocket
, ob ein Server-QuicSession
-Objekt vorhanden ist, das zur Verarbeitung des Pakets verwendet werden kann, und erstellt, falls nicht vorhanden, ein neues Objekt. Sobald das QuicSession
-Objekt des Servers verfügbar ist, wird das Paket verarbeitet und der vom Benutzer bereitgestellte Rückruf wird aufgerufen. Hierbei ist zu beachten, dass alle Details zur Handhabung des QUIC-Protokolls intern von Node.js verwaltet werden. 🎜// 创建双向流 const stream = req.openStream() // 创建单向流 const stream = req.openStream({ halfOpen: true })🎜Wie bereits erwähnt verfügt das QUIC-Protokoll über integrierte und erforderliche Unterstützung für TLS 1.3. Das bedeutet, dass jeder QUIC-Verbindung ein TLS-Schlüssel und ein Zertifikat zugeordnet sein müssen. Das Besondere an QUIC im Vergleich zu herkömmlichen TCP-basierten TLS-Verbindungen ist, dass der TLS-Kontext in QUIC mit einer
QuicSession
und nicht mit einem QuicSocket
verknüpft ist. Wenn Sie mit der Verwendung von TLSSocket
in Node.js vertraut sind, müssen Sie den Unterschied hier bemerkt haben. 🎜🎜 Ein weiterer wesentlicher Unterschied zwischen QuicSocket
(und QuicSession
) besteht darin, dass es sich von den vorhandenen net.Socket
und unterscheidet, die von Node bereitgestellt werden. js Das tls.TLSSocket
-Objekt unterscheidet sich weder von QuicSocket
noch von QuicSession
und ist ein Stream von Readable
oder WritableCode>. Das heißt, Sie können ein Objekt nicht verwenden, um Daten direkt an einen verbundenen Peer zu senden oder von diesem zu empfangen. Sie müssen daher ein <code>QuicStream
-Objekt verwenden. 🎜🎜Im obigen Beispiel wird ein QuicSocket
erstellt und an den lokalen UDP-Port 5678 gebunden. Weisen Sie dann diesen QuicSocket
an, auf die Initiierung neuer QUIC-Verbindungen zu warten. Sobald ein QuicSocket
mit dem Abhören beginnt, wird ein ready
-Ereignis ausgegeben. 🎜🎜Wenn eine neue QUIC-Verbindung gestartet und das QuicSession
-Objekt des entsprechenden Servers erstellt wird, wird das session
-Ereignis ausgegeben. Das erstellte QuicSession
-Objekt kann verwendet werden, um auf neue vom Client-Server initiierte QuicStream
-Instanzen zu warten. 🎜🎜Eine der wichtigeren Funktionen des QUIC-Protokolls besteht darin, dass der Client eine neue Verbindung zum Server starten kann, ohne den ersten Stream zu öffnen, und der Server kann zuerst seinen eigenen Stream starten, ohne auf den ersten Stream vom Client warten zu müssen. Diese Funktion ermöglicht viele sehr interessante Gameplays, die mit HTTP 1 und HTTP 2 im aktuellen Node.js-Kern nicht möglich sind. 🎜🎜🎜Erstellen Sie einen QUIC-Client 🎜🎜🎜QUIC Es gibt fast keinen Unterschied zwischen Client und Server: 🎜const { createSocket } = require('quic') const fs = require('fs') const key = readFileSync('./key.pem') const cert = readFileSync('./cert.pem') const ca = readFileSync('./ca.pem') const requestCert = true const alpn = 'echo' const servername = 'localhost' const socket = createSocket({ endpoint: { port: 8765 }, client: { key, cert, ca, requestCert alpn, servername } }) const req = socket.connect({ address: 'localhost', port: 5678, }) req.on('stream', (stream) => { stream.on('data', (chunk) => { /.../ }) stream.on('end', () => { /.../ }) }) req.on('secure', () => { const stream = req.openStream() const file = fs.createReadStream(__filename) file.pipe(stream) stream.on('data', (chunk) => { /.../ }) stream.on('end', () => { /.../ }) stream.on('close', () => { // Graceful shutdown socket.close() }) stream.on('error', (err) => { /.../ }) })
对于服务器和客户端,createSocket()
函数用于创建绑定到本地 UDP 端口的 QuicSocket
实例。对于 QUIC 客户端来说,仅在使用客户端身份验证时才需要提供 TLS 密钥和证书。
在 QuicSocket
上调用 connect()
方法将新创建一个客户端 QuicSession
对象,并与对应地址和端口的服务器创建新的 QUIC 连接。启动连接后进行 TLS 1.3 握手。握手完成后,客户端 QuicSession
对象会发出 secure
事件,表明现在可以使用了。
与服务器端类似,一旦创建了客户端 QuicSession
对象,就可以用 stream
事件监听服务器启动的新 QuicStream
实例,并可以调用 openStream()
方法来启动新的流。
所有的 QuicStream
实例都是双工流对象,这意味着它们都实现了 Readable
和 Writable
流 Node.js API。但是,在 QUIC 中,每个流都可以是双向的,也可以是单向的。
双向流在两个方向上都是可读写的,而不管该流是由客户端还是由服务器启动的。单向流只能在一个方向上读写。客户端发起的单向流只能由客户端写入,并且只能由服务器读取;客户端上不会发出任何数据事件。服务器发起的单向流只能由服务器写入,并且只能由客户端读取;服务器上不会发出任何数据事件。
// 创建双向流 const stream = req.openStream() // 创建单向流 const stream = req.openStream({ halfOpen: true })
每当远程对等方启动流时,无论是服务器还是客户端的 QuicSession
对象都会发出提供 QuicStream
对象的 stream
事件。可以用来检查这个对象确定其来源(客户端或服务器)及其方向(单向或双向)
session.on('stream', (stream) => { if (stream.clientInitiated) console.log('client initiated stream') if (stream.serverInitiated) console.log('server initiated stream') if (stream.bidirectional) console.log('bidirectional stream') if (stream.unidirectional) console.log(‘’unidirectional stream') })
由本地发起的单向 QuicStream
的 Readable
端在创建 QuicStream
对象时总会立即关闭,所以永远不会发出数据事件。同样,远程发起的单向 QuicStream
的 Writable
端将在创建后立即关闭,因此对 write()
的调用也会始终失败。
从上面的例子可以清楚地看出,从用户的角度来看,创建和使用 QUIC 是相对简单的。尽管协议本身很复杂,但这种复杂性几乎不会上升到面向用户的 API。实现中包含一些高级功能和配置选项,这些功能和配置项在上面的例子中没有说明,在通常情况下,它们在很大程度上是可选的。
在示例中没有对 HTTP 3 的支持进行说明。在基本 QUIC 协议实现的基础上实现 HTTP 3 语义的工作正在进行中,并将在以后的文章中介绍。
QUIC 协议的实现还远远没有完成。在撰写本文时,IETF 工作组仍在迭代 QUIC 规范,我们在 Node.js 中用于实现大多数 QUIC 的第三方依赖也在不断发展,并且我们的实现还远未完成,缺少测试、基准、文档和案例。但是作为 Node.js v14 中的一项实验性新功能,这项工作正在逐步着手进行。希望 QUIC 和 HTTP 3 支持在 Node.js v15 中能够得到完全支持。我们希望你的帮助!如果你有兴趣参与,请联系 https://www.nearform.com/cont... !
在结束本文时,我要感谢 NearForm 和 Protocol Labs 在财政上提供的赞助,使我能够全身心投入于对 QUIC 的实现。两家公司都对 QUIC 和 HTTP 3 将如何发展对等和传统 Web 应用开发特别感兴趣。一旦实现接近完成,我将会再写一文章来阐述 QUIC 协议的一些奇妙的用例,以及使用 QUIC 与 HTTP 1、HTTP 2、WebSockets 以及其他方法相比的优势。
James Snell( @jasnell)是 NearForm Research 的负责人,该团队致力于研究和开发 Node.js 在性能和安全性方面的主要新功能,以及物联网和机器学习的进步。 James 在软件行业拥有 20 多年的经验,并且是 Node.js 社区中的知名人物。他曾是多个 W3C 语义 web 和 IETF 互联网标准的作者、合著者、撰稿人和编辑。他是 Node.js 项目的核心贡献者,是 Node.js 技术指导委员会(TSC)的成员,并曾作为 TSC 代表在 Node.js Foundation 董事会任职。
原文地址:https://www.nearform.com/blog/a-quic-update-for-node-js/
作者:James Snell
译文地址:https://segmentfault.com/a/1190000039308474
翻译:疯狂的技术宅
更多编程相关知识,请访问:编程视频!!
Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung des QUIC-Protokolls in Node.js. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!