基于 .NET 7 的 QUIC 實現 Echo 服務

de9e1d3cf95f19025af025b03b357c63.gif

前言

隨著今年6月份的 HTTP/3 協議的正式發布,它背后的網絡傳輸協議 QUIC,憑借其高效的傳輸效率和多路并發的能力,也大概率會取代我們熟悉的使用了幾十年的 TCP,成為互聯網的下一代標準傳輸協議。

在去年 .NET 6 發布的時候,已經可以看到 HTTP/3 和 Quic 支持的相關內容了,但是當時 HTTP/3 的 RFC 還沒有定稿,所以也只是預覽功能,而 Quic 的 API 也沒有在 .NET 6 中公開。

在最新的 .NET 7 中,.NET 團隊公開了 Quic API,它是基于 MSQuic 庫來實現的 , 提供了開箱即用的支持,命名空間為 System.Net.Quic。

9772b2ea01e77e534506a4afc56cfd26.png

Quic API

下面的內容中,我會介紹如何在 .NET 中使用 Quic。

下面是 System.Net.Quic 命名空間下,比較重要的幾個類。

QuicConnection

表示一個 QUIC 連接,本身不發送也不接收數據,它可以打開或者接收多個QUIC 流。

QuicListener

用來監聽入站的 Quic 連接,一個 QuicListener 可以接收多個 Quic 連接。

QuicStream

表示 Quic 流,它可以是單向的 (QuicStreamType.Unidirectional),只允許創建方寫入數據,也可以是雙向的(QuicStreamType.Bidirectional),它允許兩邊都可以寫入數據。

小試牛刀

下面是一個客戶端和服務端應用使用 Quic 通信的示例。

  1. 1. 分別創建了 QuicClient 和 QuicServer 兩個控制臺程序。

1da4824b87291c53dc53308418e6308e.png

項目的版本為 .NET 7, 并且設置 EnablePreviewFeatures = true。

下面創建了一個 QuicListener,監聽了本地端口 9999,指定了 ALPN 協議版本。

Console.WriteLine("Quic?Server?Running...");//?創建?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 方法可以方便地創建一個本地的測試證書。

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?expiresif?(DateTime.Parse(cert.GetExpirationDateString(),?null)?<?DateTimeOffset.UtcNow){cert?=?null;}}if?(cert?==?null){//?generate?a?new?certvar?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?purposereq.CertificateExtensions.Add(new?X509EnhancedKeyUsageExtension(new?OidCollection{new("1.3.6.1.5.5.7.3.1")?//?serverAuth},?false));//?Adds?usagereq.CertificateExtensions.Add(new?X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature,?false));//?Adds?subject?alternate?namesreq.CertificateExtensions.Add(sanBuilder.Build());//?Signusing?var?crt?=?req.CreateSelfSigned(now,?now.AddDays(14));?//?14?days?is?the?max?duration?of?a?certificate?for?thiscert?=?new(crt.Export(X509ContentType.Pfx));//?Savestore.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?connectionreturn?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 處理流數據,讀取行數據,并回復一個 ack 消息。

Console.WriteLine();await?ProcessLinesAsync(stream);Console.ReadKey();??????//?處理流數據
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)){//?讀取行數據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;}}
});

創建一個出站的雙向流。

//?打開一個出站的雙向流
var?stream?=?await?connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);?var?reader?=?PipeReader.Create(stream);
var?writer?=?PipeWriter.Create(stream);

后臺讀取流數據,然后循環寫入數據。

//?后臺讀取流數據
_?=?ProcessLinesAsync(stream);Console.WriteLine();?//?寫入數據
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 讀取流數據。

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)){?//?處理行數據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 流發送了一些消息給服務端,服務端收到消息后在控制臺輸出,并回復一個 Ack 消息,因為我們創建了一個雙向流。

程序的運行結果如下

ec40d5eb1649336d189407dc2fae629a.png

我們上面說到了一個 QuicConnection 可以創建多個流,并行傳輸數據。

改造一下服務端的代碼,支持接收多個 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();

對于客戶端,我們用多個線程創建多個 Quic 流,并同時發送消息。

默認情況下,一個 Quic 連接的流的限制是 100,當然你可以設置 QuicConnectionOptions 的 MaxInboundBidirectionalStreams 和 MaxInboundUnidirectionalStreams 參數。

for?(int?j?=?0;?j?<?5;?j++)
{_?=?Task.Run(async?()?=>?{//?創建一個出站的雙向流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();?});??
}

最終程序的輸出如下

acd3b38ac8c71044983d476565baab7c.png

完整的代碼可以在下面的 github 地址找到,希望對您有用!

https://github.com/SpringLeee/PlayQuic

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/281272.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/281272.shtml
英文地址,請注明出處:http://en.pswp.cn/news/281272.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

php.ini-development和php.ini-production的區別

使用zip版MySQL安裝時&#xff0c;需要將php.ini-development或php.ini-production改成php.ini&#xff0c;那么php.ini-development和php.ini-production的區別在哪兒呢&#xff0c;通俗的說法時&#xff0c;development是開發環境&#xff0c;production用于生產環境&#xf…

Server.MapPath()的用法

http://blog.csdn.net/qiuhaifeng_csu/article/details/19416407 Server.MapPath(string path)作用是返回與Web服務器上的指定虛擬路徑相對應的物理文件路徑。其參數path為Web 服務器的虛擬路徑&#xff0c;返回結果是與path相對應的物理文件路徑。但有時參數并非為虛擬路徑&a…

為什么阿里巴巴禁止把SimpleDateFormat定義為static類型的?

在日常開發中&#xff0c;我們經常會用到時間&#xff0c;我們有很多辦法在Java代碼中獲取時間。但是不同的方法獲取到的時間的格式都不盡相同&#xff0c;這時候就需要一種格式化工具&#xff0c;把時間顯示成我們需要的格式。 最常用的方法就是使用SimpleDateFormat類。這是一…

關于信息收集和加工的思考

隨著互聯網的發展&#xff0c;獲取信息的手段越來越多&#xff0c;我們對手機的依賴程度超乎想象&#xff0c;每天忙碌著&#xff0c;大腦接收著豐富的信息&#xff0c;感覺每天都學習到了很多的知識。但我們對學習經常會有些誤區&#xff1a;1、書買了擺在書架上&#xff0c;看…

[譯]關于NODE_ENV,哪些你應該了解

原文 Node.js開發者經常檢測環境變量NODE_ENV&#xff0c;但你是否知道設置這個值同時也具有著某些別的意義&#xff1f;閱讀本文你將發現這些。NODE_ENV是一個在Express框架中極其常用的環境變量。用其確定應用的運行環境&#xff08;諸如開發&#xff0c;staging&#xff0c;…

GatewayWorker Not Support On Windows.

thinkphp版本&#xff1a;5.1 tp5.1運行命令行php think worker:gateway出現GatewayWorker Not Support On Windows.是因為在tp5.1的命令行中做了判定&#xff0c;不支持windows環境下運行。 這里不支持windows環境并不是說gateway worker不支持windows&#xff0c;而是tp5.1的…

8支團隊正在努力構建下一代Ethereum

“我們不想在構建 Ethereum 2.0時重新造輪子。” 談到開發人員為 Ethereum 區塊鏈進行兩個獨立的升級&#xff08;一個稱為 Ethereum 2.0&#xff0c;另一個稱為 Ethereum 1x&#xff09;所作出的補充努力&#xff0c;勞爾喬丹堅持認為&#xff0c;在較短的時間內將升級包括在 …

fastjson SerializerFeature詳解

名稱含義備注QuoteFieldNames輸出key時是否使用雙引號,默認為true UseSingleQuotes使用單引號而不是雙引號,默認為false WriteMapNullValue是否輸出值為null的字段,默認為false WriteEnumUsingToStringEnum輸出name()或者original,默認為false UseISO8601DateFormatDate使用ISO…

費曼學習法中問題的提出與反問,擴展與主動查詢的學習習慣訓練過程

在2022年11月05日的對話中&#xff0c;九遷先講了女媧補天和女媧造人的故事&#xff0c;女媧造人的故事還講了兩個版本的&#xff0c;隨后提到了一個事情&#xff0c;那就是&#xff0c;如果你要找一個神仙一起度過一天&#xff0c;你想找誰&#xff0c;想做些什么&#xff1f;…

Fiddle:使用斷點:bpu,bpafter

http://www.cnblogs.com/yoyoketang/p/6778006.html轉載于:https://www.cnblogs.com/peixianping/p/7230021.html

windows環境下TP5.1使用think-worker(Workerman/GatewayWorker)

文章目錄首先是解決如何運行gatewayworker調試gatewayworker程序向指定客戶端發送消息在TP框架中調用Gateway的API總結說明測試環境 windows10&#xff1b;PHP7.2&#xff1b;TP5.1&#xff1b; 這里只介紹如何使用TP集成的workerman擴展庫think-worker&#xff0c;原生workerm…

webpack之DefinePlugin使用

DefinePlugin是webpack注入全局變量的插件&#xff0c;通常使用該插件來判別代碼運行的環境變量。在使用該插件需要注意的是&#xff0c;如果在該插件配置了相關的參數&#xff0c;必須要源碼中使用&#xff0c;webpack才會注入。例如&#xff1a; new webpack.DefinePlugin({p…

Magicodes.IE 2.7.0發布

2.7.02022.11.07使用SkiaSharp替代SixLabors.ImageSharp移除SixLabors.Fonts感謝linch90的大力支持&#xff08;具體見pr#462&#xff09;部分方法改為虛方法2.7.0-beta2022.10.27使用SixLabors.ImageSharp替代System.Drawing&#xff0c;感謝linch90 &#xff08;見pr#454&…

Mobx 與 Redux 的性能對比

在本文中你將看到我最終得出的結論是 Mobx 的性能優于 Redux。但很明顯這樣的結論是片面的&#xff0c;甚至是有失偏頗的&#xff0c;因為我只選取了一個的場景對兩者進行測試。可能真實的情況恰恰相反&#xff0c;Mobx 僅僅在我測試的這個場景中優于 Redux&#xff0c;但是在我…

linux lsof/netstat查看進程和端口號相關命令:

本文為博主原創&#xff0c;未經允許不得轉載&#xff1a; 在linux操作時&#xff0c;經常要查看運行的項目的進程和端口號&#xff0c;在這里總結了以下常用到的相關命令&#xff1a; 1.查看系統運行的java項目&#xff0c;并查看進程號 這個用到的命令為&#xff1a; ps -ef|…

C#高級編程9 第17章 使用VS2013-C#特性

C#高級編程9 第17章 使用VS2013 編輯定位到 如果默認勾選了這項&#xff0c;請去掉勾選&#xff0c;因為勾選之后解決方案的目錄會根據當前文件選中。 可以設置項目并行生成數 版本控制軟件設置 所有文本編輯器行號顯示 啟用編輯繼續 收集調試信息&#xff0c;將影響性能 Code …

還在手畫C#依賴關系圖嗎?快來試試這個工具吧!

還在手畫C#依賴關系圖嗎&#xff1f;快來試試這個工具吧&#xff01;筆者最近見到了一個不錯的工具&#xff0c;可以讓大家在看代碼的時候一鍵生成C#依賴的類圖。非常適合編寫文檔、查看和學習開源項目設計時使用&#xff0c;比如下方就是筆者通過這個工具生成的Microsoft.Exte…

Web服務器 - Apache配置介紹

基本語法 常量的定義與使用&#xff0c;使用關鍵詞 Define 可以定義常量&#xff0c;使用 ${} 插入常量&#xff0c;如下 語法規則說明示列Define定義常量Define SRVROOT “D:/srv/Apache24”${}使用常量ServerRoot “${SRVROOT}”/表示路徑時使用 / 而不使用 \D:/srv/Apache…

點火開關分為4個檔位,分別是off,acc,IG-on,和ST

off全車除了常火&#xff08;如應急燈&#xff0c;時鐘等的記憶功能&#xff09;外&#xff0c;均不供電。acc 是附件檔&#xff0c;部分車載附屬設備供電&#xff0c;如視聽系統&#xff0c;儀表燈&#xff0c;燈光等。也就是說&#xff0c;車停在哪里&#xff0c;發動機不轉&…

h5的formData 上傳文件及.net后臺

先來前端的代碼&#xff1a; html 代碼&#xff1a; <input type"file" id"files" value"" multiple/> js代碼&#xff1a; function init() {var ele_files document.querySelector("#files");ele_files.addEventListener(&qu…