您好,登錄后才能下訂單哦!
這篇文章主要介紹了Node.js中QUIC協議的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
在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 教程》】
在 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 連接后,源 IP 地址和目標 IP 地址和端口都可以更改,而無需重新建立連接。這對于經常進行網絡切換(例如 LTE 到 WiFi)的移動設備特別有用。
默認 QUIC 連接是安全的并加密的。 TLS 1.3 支持直接包含在協議中,并且所有 QUIC 通信都經過加密。
QUIC 為 UDP 添加了關鍵的流控制和錯誤處理,并包括重要的安全機制以防止一系列拒絕服務攻擊。
QUIC 添加了對零行程 HTTP 請求的支持,這與基于 TCP 的 TLS 之上的 HTTP 不同,后者要求客戶端和服務器之間進行多次數據交換來建立 TLS 會話,然后才能傳輸 HTTP 請求數據,QUIC 允許 HTTP 請求頭作為 TLS 握手的一部分發送,從而大大減少了新連接的初始延遲。
為 Node.js 內核實現 QUIC 的工作從 2019 年 3 月開始,并由 NearForm 和 Protocol Labs 共同贊助。我們利用出色的 ngtcp2 庫來提供大量的低層實現。因為 QUIC 是許多 TCP 特性的重新實現,所以對 Node.js 意義重大,并且與 Node.js 中當前的 TCP 和 HTTP 相比能夠支持更多特性。同時對用戶隱藏了大量的復雜性。
在實現新的 QUIC 支持的同時,我們用了新的頂級內置 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
實例。
QUIC 協議的更重要特征之一是客戶端可以在不打開初始流的情況下啟動與服務器的新連接,并且服務器可以在不等待來自客戶端的初始流的情況下先啟動其自己的流。這個功能提供了許多非常有趣的玩法,而這在當前 Node.js 內核中的 HTTP 1 和 HTTP 2 是不可能提供的。
QUIC 客戶端和服務器之間幾乎沒有什么區別:
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 董事會任職。
感謝你能夠認真閱讀完這篇文章,希望小編分享的“Node.js中QUIC協議的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。