基于.NET?7?的?QUIC?實(shí)現(xiàn)?Echo?服務(wù)的詳細(xì)過(guò)程
前言
隨著今年6月份的 HTTP/3 協(xié)議的正式發(fā)布,它背后的網(wǎng)絡(luò)傳輸協(xié)議 QUIC,憑借其高效的傳輸效率和多路并發(fā)的能力,也大概率會(huì)取代我們熟悉的使用了幾十年的 TCP,成為互聯(lián)網(wǎng)的下一代標(biāo)準(zhǔn)傳輸協(xié)議。
在去年 .NET 6 發(fā)布的時(shí)候,已經(jīng)可以看到 HTTP/3 和 Quic 支持的相關(guān)內(nèi)容了,但是當(dāng)時(shí) HTTP/3 的 RFC 還沒(méi)有定稿,所以也只是預(yù)覽功能,而 Quic 的 API 也沒(méi)有在 .NET 6 中公開(kāi)。
在最新的 .NET 7 中,.NET 團(tuán)隊(duì)公開(kāi)了 Quic API,它是基于 MSQuic 庫(kù)來(lái)實(shí)現(xiàn)的 , 提供了開(kāi)箱即用的支持,命名空間為 System.Net.Quic。

Quic API
下面的內(nèi)容中,我會(huì)介紹如何在 .NET 中使用 Quic。
下面是 System.Net.Quic 命名空間下,比較重要的幾個(gè)類。
QuicConnection
表示一個(gè) QUIC 連接,本身不發(fā)送也不接收數(shù)據(jù),它可以打開(kāi)或者接收多個(gè)QUIC 流。
QuicListener
用來(lái)監(jiān)聽(tīng)入站的 Quic 連接,一個(gè) QuicListener 可以接收多個(gè) Quic 連接。
QuicStream
表示 Quic 流,它可以是單向的 (QuicStreamType.Unidirectional),只允許創(chuàng)建方寫(xiě)入數(shù)據(jù),也可以是雙向的(QuicStreamType.Bidirectional),它允許兩邊都可以寫(xiě)入數(shù)據(jù)。
小試牛刀
下面是一個(gè)客戶端和服務(wù)端應(yīng)用使用 Quic 通信的示例。
1.分別創(chuàng)建了 QuicClient 和 QuicServer 兩個(gè)控制臺(tái)程序。

項(xiàng)目的版本為 .NET 7, 并且設(shè)置 EnablePreviewFeatures = true。
下面創(chuàng)建了一個(gè) QuicListener,監(jiān)聽(tīng)了本地端口 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()
}
})
}); 因?yàn)?Quic 需要 TLS 加密,所以要指定一個(gè)證書(shū),GenerateManualCertificate 方法可以方便地創(chuàng)建一個(gè)本地的測(cè)試證書(shū)。
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;
}
阻塞線程,直到接收到一個(gè) Quic 連接,一個(gè) QuicListener 可以接收多個(gè) 連接。
var connection = await listener.AcceptConnectionAsync();
Console.WriteLine($"Client [{connection.RemoteEndPoint}]: connected");接收一個(gè)入站的 Quic 流, 一個(gè) QuicConnection 可以支持多個(gè)流。
var stream = await connection.AcceptInboundStreamAsync();
Console.WriteLine($"Stream [{stream.Id}]: created");接下來(lái),使用 System.IO.Pipeline 處理流數(shù)據(jù),讀取行數(shù)據(jù),并回復(fù)一個(gè) 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);
// 寫(xiě)入 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();
}
以上就是服務(wù)端的完整代碼了。
接下來(lái)我們看一下客戶端 QuicClient 的代碼。
直接使用 QuicConnection.ConnectAsync 連接到服務(wù)端。
Console.WriteLine("Quic Client Running...");
await Task.Delay(3000);
// 連接到服務(wù)端
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)建一個(gè)出站的雙向流。
// 打開(kāi)一個(gè)出站的雙向流 var stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional); var reader = PipeReader.Create(stream); var writer = PipeWriter.Create(stream);
后臺(tái)讀取流數(shù)據(jù),然后循環(huán)寫(xiě)入數(shù)據(jù)。
// 后臺(tái)讀取流數(shù)據(jù)
_ = ProcessLinesAsync(stream);
Console.WriteLine();
// 寫(xiě)入數(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 和服務(wù)端一樣,使用 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();
}
到這里,客戶端和服務(wù)端的代碼都完成了,客戶端使用 Quic 流發(fā)送了一些消息給服務(wù)端,服務(wù)端收到消息后在控制臺(tái)輸出,并回復(fù)一個(gè) Ack 消息,因?yàn)槲覀儎?chuàng)建了一個(gè)雙向流。
程序的運(yùn)行結(jié)果如下

我們上面說(shuō)到了一個(gè) QuicConnection 可以創(chuàng)建多個(gè)流,并行傳輸數(shù)據(jù)。
改造一下服務(wù)端的代碼,支持接收多個(gè) 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(); 對(duì)于客戶端,我們用多個(gè)線程創(chuàng)建多個(gè) Quic 流,并同時(shí)發(fā)送消息。
默認(rèn)情況下,一個(gè) Quic 連接的流的限制是 100,當(dāng)然你可以設(shè)置 QuicConnectionOptions 的 MaxInboundBidirectionalStreams 和 MaxInboundUnidirectionalStreams 參數(shù)。
for (int j = 0; j < 5; j++)
{
_ = Task.Run(async () => {
// 創(chuàng)建一個(gè)出站的雙向流
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 地址找到,希望對(duì)您有用!
https://github.com/SpringLeee/PlayQuic
到此這篇關(guān)于基于 .NET 7 的 QUIC 實(shí)現(xiàn) Echo 服務(wù)的文章就介紹到這了,更多相關(guān).NET 7 實(shí)現(xiàn) Echo 服務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
WPF自定義Expander控件樣式實(shí)現(xiàn)酷炫Style
這篇文章介紹了WPF自定義Expander控件樣式實(shí)現(xiàn)酷炫Style的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01
Visual Studio 2017無(wú)法加載Visual Studio 2015創(chuàng)建的SharePoint解決方法
這篇文章主要為大家詳細(xì)介紹了Visual Studio 2017無(wú)法加載Visual Studio 2015創(chuàng)建的SharePoint的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Asp.Net Core 企業(yè)微信靜默授權(quán)的實(shí)現(xiàn)
這篇文章主要介紹了Asp.Net Core 企業(yè)微信靜默授權(quán)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
.NET Core授權(quán)失敗自定義響應(yīng)信息的操作方法
這篇文章主要介紹了.NET Core授權(quán)失敗自定義響應(yīng)信息的操作方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-07-07
asp.net編程實(shí)現(xiàn)刪除文件夾及文件夾下文件的方法
這篇文章主要介紹了asp.net編程實(shí)現(xiàn)刪除文件夾及文件夾下文件的方法,涉及asp.net針對(duì)文件與目錄的遍歷及刪除操作實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11

