Heim  >  Artikel  >  Web-Frontend  >  Detaillierte Erläuterung des QUIC-Protokolls in Node.js

Detaillierte Erläuterung des QUIC-Protokolls in Node.js

青灯夜游
青灯夜游nach vorne
2021-03-26 10:49:564185Durchsuche

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.

Detaillierte Erläuterung des QUIC-Protokolls in Node.js

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“]

QUIC beseitigt die Grundursache der Head-of-Line-Blockierung

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.

Detaillierte Erläuterung des QUIC-Protokolls in Node.js

In HTTP 1 wird diese Anfrage blockiert, da jeweils nur eine vollständige Nachricht gesendet werden kann.

Detaillierte Erläuterung des QUIC-Protokolls in Node.js

Wenn in HTTP 2 ein einzelnes TCP-Paket verloren geht oder beschädigt ist, wird die Anfrage blockiert.

Detaillierte Erläuterung des QUIC-Protokolls in Node.js

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 Flexibilität, Sicherheit und geringe Latenz

QUIC bietet außerdem viele weitere tolle Funktionen:

  • QUIC-Verbindungen funktionieren unabhängig von der Netzwerktopologie. Nachdem eine QUIC-Verbindung hergestellt wurde, können sowohl die Quell- als auch die Ziel-IP-Adresse und -Ports geändert werden, ohne dass die Verbindung erneut hergestellt werden muss. Dies ist besonders nützlich für mobile Geräte, die häufig das Netzwerk wechseln (z. B. LTE zu WLAN).
  • QUIC-Verbindungen sind standardmäßig sicher und verschlüsselt. Die TLS 1.3-Unterstützung ist direkt im Protokoll enthalten und die gesamte QUIC-Kommunikation ist verschlüsselt.
  • QUIC erweitert UDP um kritische Flusskontrolle und Fehlerbehandlung und enthält wichtige Sicherheitsmechanismen, um eine Reihe von Denial-of-Service-Angriffen zu verhindern.
  • QUIC bietet Unterstützung für Zero-Trip-HTTP-Anfragen. Im Gegensatz zu HTTP über TCP-basiertem TLS, das mehrere Datenaustausche zwischen Client und Server erfordert, um eine TLS-Sitzung aufzubauen, bevor HTTP-Anfragedaten übertragen werden können, ermöglicht QUIC das Senden von HTTP-Anfrageheadern als Teil des TLS-Handshakes, wodurch die anfängliche Latenz neuer Verbindungen erheblich reduziert wird.

Detaillierte Erläuterung des QUIC-Protokolls in Node.js

Implementierung von QUIC für den Node.js-Kernel

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.

„Quic“-Modul

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服务器

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.Sockettls.TLSSocket 对象不同,QuicSocketQuicSession 都不是 ReadableWritable的流。即不能用一个对象直接向连接的对等方发送数据或从其接收数据,所以必须使用 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.

Erstellen Sie einen QUIC-Server

Ein QUIC-Server ist eine 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 实例都是双工流对象,这意味着它们都实现了 ReadableWritable 流 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')
})

由本地发起的单向 QuicStreamReadable 端在创建 QuicStream 对象时总会立即关闭,所以永远不会发出数据事件。同样,远程发起的单向 QuicStreamWritable 端将在创建后立即关闭,因此对 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!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen