溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Node.js中QUIC協(xié)議的示例分析

發(fā)布時間:2021-03-26 11:17:53 來源:億速云 閱讀:442 作者:小新 欄目:web開發(fā)

這篇文章主要介紹了Node.js中QUIC協(xié)議的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

在2019年3月,受到 NearForm 和 Protocol Labs 的支持,我開始為 Node.js 實(shí)現(xiàn) QUIC 協(xié)議 支持。這個基于 UDP 的新傳輸協(xié)議旨在最終替代所有使用 TCP 的 HTTP 通信。

Node.js中QUIC協(xié)議的示例分析

熟悉 UDP 的人可能會產(chǎn)生質(zhì)疑。眾所周知 UDP 是不可靠的,數(shù)據(jù)包經(jīng)常會有丟失、亂序、重復(fù)等情況。 UDP 不保證高級協(xié)議(例如 HTTP)嚴(yán)格要求的 TCP 所支持的可靠性和順序。那就是 QUIC 進(jìn)來的地方。

QUIC 協(xié)議在 UDP 之上定義了一層,該層為 UDP 引入了錯誤處理、可靠性、流控制和內(nèi)置安全性(通過 TLS 1.3)。實(shí)際上它在 UDP 之上重新實(shí)現(xiàn)了大多數(shù) TCP 的特效,但是有一個關(guān)鍵的區(qū)別:與 TCP 不同,仍然可以不按順序傳輸數(shù)據(jù)包。了解這一點(diǎn)對于理解 QUIC 為什么優(yōu)于 TCP 至關(guān)重要。

【相關(guān)推薦:《nodejs 教程》】

QUIC 消除了隊(duì)首阻塞的根源

在 HTTP 1 中,客戶端和服務(wù)器之間所交換的所有消息都是連續(xù)的、不間斷的數(shù)據(jù)塊形式。雖然可以通過單個 TCP 連接發(fā)送多個請求或響應(yīng),但是在發(fā)送下一條完整消息之前,必須先等上一條消息完整的傳輸完畢。這意味著,如果要發(fā)送一個 10 兆字節(jié)的文件,然后發(fā)送一個 2 兆字節(jié)的文件,則前者必須完全傳輸完畢,然后才能啟動后者。這就是所謂的隊(duì)首阻塞,是造成大量延遲和不良使用網(wǎng)絡(luò)帶寬的根源。

HTTP 2 嘗試通過引入多路復(fù)用來解決此問題。 HTTP 2 不是將請求和響應(yīng)作為連續(xù)的流傳輸,而是將請求和響應(yīng)分成了被稱為幀的離散塊,這些塊可以與其他幀交織。一個 TCP 連接理論上可以處理無限數(shù)量的并發(fā)請求和響應(yīng)流。盡管從理論上講這是可行的,但是 HTTP 2 的設(shè)計(jì)沒有考慮 TCP 層出現(xiàn)隊(duì)首阻塞的可能性。

TCP 本身是嚴(yán)格排序的協(xié)議。數(shù)據(jù)包被序列化并按照固定順序通過網(wǎng)絡(luò)發(fā)送。如果數(shù)據(jù)包未能到達(dá)其目的地,則會阻止整個數(shù)據(jù)包流,直到可以重新傳輸丟失的數(shù)據(jù)包為止。有效的順序是:發(fā)送數(shù)據(jù)包1,等待確認(rèn),發(fā)送數(shù)據(jù)包2,等待確認(rèn),發(fā)送數(shù)據(jù)包3……。使用 HTTP 1,在任何給定時間只能傳輸一個 HTTP 消息,如果單個 TCP 數(shù)據(jù)包丟失,那么重傳只會影響單個 HTTP 請求/響應(yīng)流。但是使用 HTTP 2,則會在丟失單個 TCP 數(shù)據(jù)包的情況下阻止無限數(shù)量的并發(fā) HTTP 請求/響應(yīng)流的傳輸。在通過高延遲、低可靠性網(wǎng)絡(luò)進(jìn)行 HTTP 2 通信時,與 HTTP 1 相比,整體性能和網(wǎng)絡(luò)吞吐量會急劇下降。

Node.js中QUIC協(xié)議的示例分析

在 HTTP 1 中,該請求會被阻塞,因?yàn)橐淮沃荒馨l(fā)送一條完整的消息。

Node.js中QUIC協(xié)議的示例分析

在 HTTP 2 中,當(dāng)單個 TCP 數(shù)據(jù)包丟失或損壞時,該請求將被阻塞。

Node.js中QUIC協(xié)議的示例分析

在QUIC中,數(shù)據(jù)包彼此獨(dú)立,能夠以任何順序發(fā)送(或重新發(fā)送)。

幸運(yùn)的是有了 QUIC 情況就不同了。當(dāng)數(shù)據(jù)流被打包到離散的 UDP 數(shù)據(jù)包中傳輸時,任何單個數(shù)據(jù)包都能夠以任意順序發(fā)送(或重新發(fā)送),而不會影響到其他已發(fā)送的數(shù)據(jù)包。換句話說,線路阻塞問題在很大程度上得到解決。

QUIC 引入了靈活性、安全性和低延遲

QUIC 還引入了許多其他重要功能:

  • QUIC 連接的運(yùn)行獨(dú)立于網(wǎng)絡(luò)拓?fù)浣Y(jié)構(gòu)。在建立了 QUIC 連接后,源 IP 地址和目標(biāo) IP 地址和端口都可以更改,而無需重新建立連接。這對于經(jīng)常進(jìn)行網(wǎng)絡(luò)切換(例如 LTE 到 WiFi)的移動設(shè)備特別有用。

  • 默認(rèn) QUIC 連接是安全的并加密的。 TLS 1.3 支持直接包含在協(xié)議中,并且所有 QUIC 通信都經(jīng)過加密。

  • QUIC 為 UDP 添加了關(guān)鍵的流控制和錯誤處理,并包括重要的安全機(jī)制以防止一系列拒絕服務(wù)攻擊。

  • QUIC 添加了對零行程 HTTP 請求的支持,這與基于 TCP 的 TLS 之上的 HTTP 不同,后者要求客戶端和服務(wù)器之間進(jìn)行多次數(shù)據(jù)交換來建立 TLS 會話,然后才能傳輸 HTTP 請求數(shù)據(jù),QUIC 允許 HTTP 請求頭作為 TLS 握手的一部分發(fā)送,從而大大減少了新連接的初始延遲。

Node.js中QUIC協(xié)議的示例分析

為 Node.js 內(nèi)核實(shí)現(xiàn) QUIC

為 Node.js 內(nèi)核實(shí)現(xiàn) QUIC 的工作從 2019 年 3 月開始,并由 NearForm 和 Protocol Labs 共同贊助。我們利用出色的 ngtcp2 庫來提供大量的低層實(shí)現(xiàn)。因?yàn)?QUIC 是許多 TCP 特性的重新實(shí)現(xiàn),所以對 Node.js 意義重大,并且與 Node.js 中當(dāng)前的 TCP 和 HTTP 相比能夠支持更多特性。同時對用戶隱藏了大量的復(fù)雜性。

“quic” 模塊

在實(shí)現(xiàn)新的 QUIC 支持的同時,我們用了新的頂級內(nèi)置 quic 模塊來公開 API。當(dāng)該功能在 Node.js 核心中落地時,是否仍將使用這個頂級模塊,將在以后確定。不過當(dāng)在開發(fā)中使用實(shí)驗(yàn)性支持時,你可以通過 require('quic') 使用這個 API。

const { createSocket } = require('quic')

quic 模塊公開了一個導(dǎo)出:createSocket 函數(shù)。這個函數(shù)用來創(chuàng)建 QuicSocket 對象實(shí)例,該對象可用于 QUIC 服務(wù)器和客戶端。

QUIC 的所有工作都在一個單獨(dú)的 GitHub 存儲庫 中進(jìn)行,該庫 fork 于 Node.js master 分支并與之并行開發(fā)。如果你想使用新模塊,或者貢獻(xiàn)自己的代碼,可以從那里獲取源代碼,請參閱 Node.js 構(gòu)建說明。不過它現(xiàn)在仍然是一項(xiàng)尚在進(jìn)行中的工作,你一定會遇到 bug 的。

創(chuàng)建QUIC服務(wù)器

QUIC 服務(wù)器是一個 QuicSocket 實(shí)例,被配置為等待遠(yuǎn)程客戶端啟動新的 QUIC 連接。這是通過綁定到本地 UDP 端口并等待從對等方接收初始 QUIC 數(shù)據(jù)包來完成的。在收到 QUIC 數(shù)據(jù)包后,QuicSocket 將會檢查是否存在能夠用于處理該數(shù)據(jù)包的服務(wù)器 QuicSession 對象,如果不存在將會創(chuàng)建一個新的對象。一旦服務(wù)器的 QuicSession 對象可用,則該數(shù)據(jù)包將被處理,并調(diào)用用戶提供的回調(diào)。這里有一點(diǎn)很重要,處理 QUIC 協(xié)議的所有細(xì)節(jié)都由 Node.js 在其內(nèi)部處理。

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 實(shí)例創(chuàng)建默認(rèn)配置
  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 協(xié)議內(nèi)置并要求支持 TLS 1.3。這意味著每個 QUIC 連接必須有與其關(guān)聯(lián)的 TLS 密鑰和證書。與傳統(tǒng)的基于 TCP 的 TLS 連接相比,QUIC 的獨(dú)特之處在于 QUIC 中的 TLS 上下文與 QuicSession 相關(guān)聯(lián),而不是 QuicSocket。如果你熟悉 Node.js 中 TLSSocket 的用法,那么你一定注意到這里的區(qū)別。

QuicSocket(和 QuicSession)的另一個關(guān)鍵區(qū)別是,與 Node.js 公開的現(xiàn)有 net.Sockettls.TLSSocket 對象不同,QuicSocketQuicSession 都不是 ReadableWritable的流。即不能用一個對象直接向連接的對等方發(fā)送數(shù)據(jù)或從其接收數(shù)據(jù),所以必須使用 QuicStream 對象。

在上面的例子中創(chuàng)建了一個 QuicSocket 并將其綁定到本地 UDP 的 5678 端口。然后告訴這個 QuicSocket 偵聽要啟動的新 QUIC 連接。一旦 QuicSocket 開始偵聽,將會發(fā)出 ready 事件。

當(dāng)啟動新的 QUIC 連接并創(chuàng)建了對應(yīng)服務(wù)器的 QuicSession 對象后,將會發(fā)出 session 事件。創(chuàng)建的 QuicSession 對象可用于偵聽新的客戶端服務(wù)器端所啟動的 QuicStream 實(shí)例。

QUIC 協(xié)議的更重要特征之一是客戶端可以在不打開初始流的情況下啟動與服務(wù)器的新連接,并且服務(wù)器可以在不等待來自客戶端的初始流的情況下先啟動其自己的流。這個功能提供了許多非常有趣的玩法,而這在當(dāng)前 Node.js 內(nèi)核中的 HTTP 1 和 HTTP 2 是不可能提供的。

創(chuàng)建QUIC客戶端

QUIC 客戶端和服務(wù)器之間幾乎沒有什么區(qū)別:

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) => { /.../ })
})

對于服務(wù)器和客戶端,createSocket() 函數(shù)用于創(chuàng)建綁定到本地 UDP 端口的 QuicSocket 實(shí)例。對于 QUIC 客戶端來說,僅在使用客戶端身份驗(yàn)證時才需要提供 TLS 密鑰和證書。

QuicSocket 上調(diào)用 connect() 方法將新創(chuàng)建一個客戶端 QuicSession 對象,并與對應(yīng)地址和端口的服務(wù)器創(chuàng)建新的 QUIC 連接。啟動連接后進(jìn)行 TLS 1.3 握手。握手完成后,客戶端 QuicSession 對象會發(fā)出 secure 事件,表明現(xiàn)在可以使用了。

與服務(wù)器端類似,一旦創(chuàng)建了客戶端 QuicSession 對象,就可以用 stream 事件監(jiān)聽服務(wù)器啟動的新 QuicStream 實(shí)例,并可以調(diào)用 openStream()  方法來啟動新的流。

單向流和雙向流

所有的 QuicStream 實(shí)例都是雙工流對象,這意味著它們都實(shí)現(xiàn)了 ReadableWritable 流 Node.js API。但是,在 QUIC 中,每個流都可以是雙向的,也可以是單向的。

雙向流在兩個方向上都是可讀寫的,而不管該流是由客戶端還是由服務(wù)器啟動的。單向流只能在一個方向上讀寫??蛻舳税l(fā)起的單向流只能由客戶端寫入,并且只能由服務(wù)器讀?。豢蛻舳松喜粫l(fā)出任何數(shù)據(jù)事件。服務(wù)器發(fā)起的單向流只能由服務(wù)器寫入,并且只能由客戶端讀?。环?wù)器上不會發(fā)出任何數(shù)據(jù)事件。

// 創(chuàng)建雙向流
const stream = req.openStream()

// 創(chuàng)建單向流
const stream = req.openStream({ halfOpen: true })

每當(dāng)遠(yuǎn)程對等方啟動流時,無論是服務(wù)器還是客戶端的 QuicSession 對象都會發(fā)出提供 QuicStream 對象的 stream 事件??梢杂脕頇z查這個對象確定其來源(客戶端或服務(wù)器)及其方向(單向或雙向)

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')
})

由本地發(fā)起的單向 QuicStreamReadable 端在創(chuàng)建 QuicStream 對象時總會立即關(guān)閉,所以永遠(yuǎn)不會發(fā)出數(shù)據(jù)事件。同樣,遠(yuǎn)程發(fā)起的單向 QuicStreamWritable 端將在創(chuàng)建后立即關(guān)閉,因此對 write() 的調(diào)用也會始終失敗。

就是這樣

從上面的例子可以清楚地看出,從用戶的角度來看,創(chuàng)建和使用 QUIC 是相對簡單的。盡管協(xié)議本身很復(fù)雜,但這種復(fù)雜性幾乎不會上升到面向用戶的 API。實(shí)現(xiàn)中包含一些高級功能和配置選項(xiàng),這些功能和配置項(xiàng)在上面的例子中沒有說明,在通常情況下,它們在很大程度上是可選的。

在示例中沒有對 HTTP 3 的支持進(jìn)行說明。在基本 QUIC 協(xié)議實(shí)現(xiàn)的基礎(chǔ)上實(shí)現(xiàn) HTTP 3 語義的工作正在進(jìn)行中,并將在以后的文章中介紹。

QUIC 協(xié)議的實(shí)現(xiàn)還遠(yuǎn)遠(yuǎn)沒有完成。在撰寫本文時,IETF 工作組仍在迭代 QUIC 規(guī)范,我們在 Node.js 中用于實(shí)現(xiàn)大多數(shù) QUIC 的第三方依賴也在不斷發(fā)展,并且我們的實(shí)現(xiàn)還遠(yuǎn)未完成,缺少測試、基準(zhǔn)、文檔和案例。但是作為 Node.js v14 中的一項(xiàng)實(shí)驗(yàn)性新功能,這項(xiàng)工作正在逐步著手進(jìn)行。希望 QUIC 和 HTTP 3 支持在 Node.js v15 中能夠得到完全支持。我們希望你的幫助!如果你有興趣參與,請聯(lián)系 https://www.nearform.com/cont... !

鳴謝

在結(jié)束本文時,我要感謝 NearForm 和 Protocol Labs 在財(cái)政上提供的贊助,使我能夠全身心投入于對 QUIC 的實(shí)現(xiàn)。兩家公司都對 QUIC 和 HTTP 3 將如何發(fā)展對等和傳統(tǒng) Web 應(yīng)用開發(fā)特別感興趣。一旦實(shí)現(xiàn)接近完成,我將會再寫一文章來闡述 QUIC 協(xié)議的一些奇妙的用例,以及使用 QUIC 與 HTTP 1、HTTP 2、WebSockets 以及其他方法相比的優(yōu)勢。

James Snell( @jasnell)是 NearForm Research 的負(fù)責(zé)人,該團(tuán)隊(duì)致力于研究和開發(fā) Node.js 在性能和安全性方面的主要新功能,以及物聯(lián)網(wǎng)和機(jī)器學(xué)習(xí)的進(jìn)步。 James 在軟件行業(yè)擁有 20 多年的經(jīng)驗(yàn),并且是 Node.js 社區(qū)中的知名人物。他曾是多個 W3C 語義 web 和 IETF 互聯(lián)網(wǎng)標(biāo)準(zhǔn)的作者、合著者、撰稿人和編輯。他是 Node.js 項(xiàng)目的核心貢獻(xiàn)者,是 Node.js 技術(shù)指導(dǎo)委員會(TSC)的成員,并曾作為 TSC 代表在 Node.js Foundation 董事會任職。

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“Node.js中QUIC協(xié)議的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI