前言
隨著今年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。

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

項目的版本為 .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 消息,因為我們創建了一個雙向流。
程序的運行結果如下

我們上面說到了一個 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();?});??
}
最終程序的輸出如下

完整的代碼可以在下面的 github 地址找到,希望對您有用!
https://github.com/SpringLeee/PlayQuic