基于.NET?7?的?QUIC?實現(xiàn)?Echo?服務的詳細過程
前言
隨著今年6月份的 HTTP/3 協(xié)議的正式發(fā)布,它背后的網(wǎng)絡傳輸協(xié)議 QUIC,憑借其高效的傳輸效率和多路并發(fā)的能力,也大概率會取代我們熟悉的使用了幾十年的 TCP,成為互聯(lián)網(wǎng)的下一代標準傳輸協(xié)議。
在去年 .NET 6 發(fā)布的時候,已經可以看到 HTTP/3 和 Quic 支持的相關內容了,但是當時 HTTP/3 的 RFC 還沒有定稿,所以也只是預覽功能,而 Quic 的 API 也沒有在 .NET 6 中公開。
在最新的 .NET 7 中,.NET 團隊公開了 Quic API,它是基于 MSQuic 庫來實現(xiàn)的 , 提供了開箱即用的支持,命名空間為 System.Net.Quic。
Quic API
下面的內容中,我會介紹如何在 .NET 中使用 Quic。
下面是 System.Net.Quic 命名空間下,比較重要的幾個類。
QuicConnection
表示一個 QUIC 連接,本身不發(fā)送也不接收數(shù)據(jù),它可以打開或者接收多個QUIC 流。
QuicListener
用來監(jiān)聽入站的 Quic 連接,一個 QuicListener 可以接收多個 Quic 連接。
QuicStream
表示 Quic 流,它可以是單向的 (QuicStreamType.Unidirectional),只允許創(chuàng)建方寫入數(shù)據(jù),也可以是雙向的(QuicStreamType.Bidirectional),它允許兩邊都可以寫入數(shù)據(jù)。
小試牛刀
下面是一個客戶端和服務端應用使用 Quic 通信的示例。
1.分別創(chuàng)建了 QuicClient 和 QuicServer 兩個控制臺程序。
項目的版本為 .NET 7, 并且設置 EnablePreviewFeatures = true。
下面創(chuàng)建了一個 QuicListener,監(jiān)聽了本地端口 9999,指定了 ALPN 協(xié)議版本。
Console.WriteLine("Quic Server Running..."); // 創(chuàng)建 QuicListener var listener = await QuicListener.ListenAsync(new QuicListenerOptions { ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 }, ListenEndPoint = new IPEndPoint(IPAddress.Loopback,9999), ConnectionOptionsCallback = (connection,ssl, token) => ValueTask.FromResult(new QuicServerConnectionOptions() { DefaultStreamErrorCode = 0, DefaultCloseErrorCode = 0, ServerAuthenticationOptions = new SslServerAuthenticationOptions() { ApplicationProtocols = new List<SslApplicationProtocol>() { SslApplicationProtocol.Http3 }, ServerCertificate = GenerateManualCertificate() } }) });
因為 Quic 需要 TLS 加密,所以要指定一個證書,GenerateManualCertificate 方法可以方便地創(chuàng)建一個本地的測試證書。
X509Certificate2 GenerateManualCertificate() { X509Certificate2 cert = null; var store = new X509Store("KestrelWebTransportCertificates", StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); if (store.Certificates.Count > 0) { cert = store.Certificates[^1]; // rotate key after it expires if (DateTime.Parse(cert.GetExpirationDateString(), null) < DateTimeOffset.UtcNow) { cert = null; } } if (cert == null) { // generate a new cert var now = DateTimeOffset.UtcNow; SubjectAlternativeNameBuilder sanBuilder = new(); sanBuilder.AddDnsName("localhost"); using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256); CertificateRequest req = new("CN=localhost", ec, HashAlgorithmName.SHA256); // Adds purpose req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new("1.3.6.1.5.5.7.3.1") // serverAuth }, false)); // Adds usage req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false)); // Adds subject alternate names req.CertificateExtensions.Add(sanBuilder.Build()); // Sign using var crt = req.CreateSelfSigned(now, now.AddDays(14)); // 14 days is the max duration of a certificate for this cert = new(crt.Export(X509ContentType.Pfx)); // Save store.Add(cert); } store.Close(); var hash = SHA256.HashData(cert.RawData); var certStr = Convert.ToBase64String(hash); //Console.WriteLine($"\n\n\n\n\nCertificate: {certStr}\n\n\n\n"); // <-- you will need to put this output into the JS API call to allow the connection return cert; }
阻塞線程,直到接收到一個 Quic 連接,一個 QuicListener 可以接收多個 連接。
var connection = await listener.AcceptConnectionAsync(); Console.WriteLine($"Client [{connection.RemoteEndPoint}]: connected");
接收一個入站的 Quic 流, 一個 QuicConnection 可以支持多個流。
var stream = await connection.AcceptInboundStreamAsync(); Console.WriteLine($"Stream [{stream.Id}]: created");
接下來,使用 System.IO.Pipeline 處理流數(shù)據(jù),讀取行數(shù)據(jù),并回復一個 ack 消息。
Console.WriteLine(); await ProcessLinesAsync(stream); Console.ReadKey(); // 處理流數(shù)據(jù) async Task ProcessLinesAsync(QuicStream stream) { var reader = PipeReader.Create(stream); var writer = PipeWriter.Create(stream); while (true) { ReadResult result = await reader.ReadAsync(); ReadOnlySequence<byte> buffer = result.Buffer; while (TryReadLine(ref buffer, out ReadOnlySequence<byte> line)) { // 讀取行數(shù)據(jù) ProcessLine(line); // 寫入 ACK 消息 await writer.WriteAsync(Encoding.UTF8.GetBytes($"Ack: {DateTime.Now.ToString("HH:mm:ss")} \n")); } reader.AdvanceTo(buffer.Start, buffer.End); if (result.IsCompleted) { break; } } Console.WriteLine($"Stream [{stream.Id}]: completed"); await reader.CompleteAsync(); await writer.CompleteAsync(); } bool TryReadLine(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> line) { SequencePosition? position = buffer.PositionOf((byte)'\n'); if (position == null) { line = default; return false; } line = buffer.Slice(0, position.Value); buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); return true; } void ProcessLine(in ReadOnlySequence<byte> buffer) { foreach (var segment in buffer) { Console.WriteLine("Recevied -> " + System.Text.Encoding.UTF8.GetString(segment.Span)); } Console.WriteLine(); }
以上就是服務端的完整代碼了。
接下來我們看一下客戶端 QuicClient 的代碼。
直接使用 QuicConnection.ConnectAsync 連接到服務端。
Console.WriteLine("Quic Client Running..."); await Task.Delay(3000); // 連接到服務端 var connection = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions { DefaultCloseErrorCode = 0, DefaultStreamErrorCode = 0, RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 9999), ClientAuthenticationOptions = new SslClientAuthenticationOptions { ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 }, RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => { return true; } } });
創(chuàng)建一個出站的雙向流。
// 打開一個出站的雙向流 var stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional); var reader = PipeReader.Create(stream); var writer = PipeWriter.Create(stream);
后臺讀取流數(shù)據(jù),然后循環(huán)寫入數(shù)據(jù)。
// 后臺讀取流數(shù)據(jù) _ = ProcessLinesAsync(stream); Console.WriteLine(); // 寫入數(shù)據(jù) for (int i = 0; i < 7; i++) { await Task.Delay(2000); var message = $"Hello Quic {i} \n"; Console.Write("Send -> " + message); await writer.WriteAsync(Encoding.UTF8.GetBytes(message)); } await writer.CompleteAsync(); Console.ReadKey();
ProcessLinesAsync 和服務端一樣,使用 System.IO.Pipeline 讀取流數(shù)據(jù)。
async Task ProcessLinesAsync(QuicStream stream) { while (true) { ReadResult result = await reader.ReadAsync(); ReadOnlySequence<byte> buffer = result.Buffer; while (TryReadLine(ref buffer, out ReadOnlySequence<byte> line)) { // 處理行數(shù)據(jù) ProcessLine(line); } reader.AdvanceTo(buffer.Start, buffer.End); if (result.IsCompleted) { break; } } await reader.CompleteAsync(); await writer.CompleteAsync(); } bool TryReadLine(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> line) { SequencePosition? position = buffer.PositionOf((byte)'\n'); if (position == null) { line = default; return false; } line = buffer.Slice(0, position.Value); buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); return true; } void ProcessLine(in ReadOnlySequence<byte> buffer) { foreach (var segment in buffer) { Console.Write("Recevied -> " + System.Text.Encoding.UTF8.GetString(segment.Span)); Console.WriteLine(); } Console.WriteLine(); }
到這里,客戶端和服務端的代碼都完成了,客戶端使用 Quic 流發(fā)送了一些消息給服務端,服務端收到消息后在控制臺輸出,并回復一個 Ack 消息,因為我們創(chuàng)建了一個雙向流。
程序的運行結果如下
我們上面說到了一個 QuicConnection 可以創(chuàng)建多個流,并行傳輸數(shù)據(jù)。
改造一下服務端的代碼,支持接收多個 Quic 流。
var cts = new CancellationTokenSource(); while (!cts.IsCancellationRequested) { var stream = await connection.AcceptInboundStreamAsync(); Console.WriteLine($"Stream [{stream.Id}]: created"); Console.WriteLine(); _ = ProcessLinesAsync(stream); } Console.ReadKey();
對于客戶端,我們用多個線程創(chuàng)建多個 Quic 流,并同時發(fā)送消息。
默認情況下,一個 Quic 連接的流的限制是 100,當然你可以設置 QuicConnectionOptions 的 MaxInboundBidirectionalStreams 和 MaxInboundUnidirectionalStreams 參數(shù)。
for (int j = 0; j < 5; j++) { _ = Task.Run(async () => { // 創(chuàng)建一個出站的雙向流 var stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional); var writer = PipeWriter.Create(stream); Console.WriteLine(); await Task.Delay(2000); var message = $"Hello Quic [{stream.Id}] \n"; Console.Write("Send -> " + message); await writer.WriteAsync(Encoding.UTF8.GetBytes(message)); await writer.CompleteAsync(); }); }
最終程序的輸出如下
完整的代碼可以在下面的 github 地址找到,希望對您有用!
https://github.com/SpringLeee/PlayQuic
到此這篇關于基于 .NET 7 的 QUIC 實現(xiàn) Echo 服務的文章就介紹到這了,更多相關.NET 7 實現(xiàn) Echo 服務內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
WPF自定義Expander控件樣式實現(xiàn)酷炫Style
這篇文章介紹了WPF自定義Expander控件樣式實現(xiàn)酷炫Style的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-01-01Visual Studio 2017無法加載Visual Studio 2015創(chuàng)建的SharePoint解決方法
這篇文章主要為大家詳細介紹了Visual Studio 2017無法加載Visual Studio 2015創(chuàng)建的SharePoint的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03Asp.Net Core 企業(yè)微信靜默授權的實現(xiàn)
這篇文章主要介紹了Asp.Net Core 企業(yè)微信靜默授權的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-10-10asp.net編程實現(xiàn)刪除文件夾及文件夾下文件的方法
這篇文章主要介紹了asp.net編程實現(xiàn)刪除文件夾及文件夾下文件的方法,涉及asp.net針對文件與目錄的遍歷及刪除操作實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11