2022年11月 .NET CORE工具案例-.NET 7中的Quic通信
Posted 微软MVP Eleven
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2022年11月 .NET CORE工具案例-.NET 7中的Quic通信相关的知识,希望对你有一定的参考价值。
前言
QUIC(Quick UDP Internet Connection)是谷歌制定的一种基于UDP的低时延的互联网传输层协议。我们知道,TCP/IP协议族是互联网的基础。其中传输层协议包括TCP和UDP协议。与TCP协议相比,UDP更为轻量,但是错误校验也要少得多。这意味着UDP往往效率更高(不经常跟服务器端通信查看数据包是否送达或者按序),但是可靠性比不上TCP。通常游戏、流媒体以及VoIP等应用均采用UDP,而网页、邮件、远程登录等大部分的应用均采用TCP。
QUIC很好地解决了当今传输层和应用层面临的各种需求,包括处理更多的连接,安全性,和低延迟。QUIC融合了包括TCP,TLS,HTTP/2等协议的特性,但基于UDP传输。QUIC的一个主要目标就是减少连接延迟,当客户端第一次连接服务器时,QUIC只需要1RTT(Round-Trip Time)的延迟就可以建立可靠安全的连接,相对于TCP+TLS的1-3次RTT要更加快捷。之后客户端可以在本地缓存加密的认证信息,再次与服务器建立连接时可以实现0-RTT的连接建立延迟。QUIC同时复用了HTTP/2协议的多路复用功能(Multiplexing),但由于QUIC基于UDP所以避免了HTTP/2的队头阻塞(Head-of-Line Blocking)问题。因为QUIC基于UDP,运行在用户域而不是系统内核,使得QUIC协议可以快速的更新和部署,从而很好地解决了TCP协议部署及更新的困难。
以下是TCP和Quic通信过程的示例图:
一、.NET 7中的Quic通信
1.下载.NET 7预览版
下载地址:https://dotnet.microsoft.com/zh-cn/download/dotnet/7.0
2.vs2022配置使用预览版SDK
3. .NET 中使用 Quic
下面是 System.Net.Quic 命名空间下,比较重要的几个类。
- QuicConnection
表示一个 QUIC 连接,本身不发送也不接收数据,它可以打开或者接收多个QUIC 流。
- QuicListener
用来监听入站的 Quic 连接,一个 QuicListener 可以接收多个 Quic 连接。
- QuicStream
表示 Quic 流,它可以是单向的 (QuicStreamType.Unidirectional),只允许创建方写入数据,也可以是双向的(QuicStreamType.Bidirectional),它允许两边都可以写入数据。
4. .NET 中使用 Quic代码解析
4.1 服务端
建了一个 QuicListener,监听了本地端口 9999,指定了 ALPN 协议版本。
// 创建 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 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 可以接收多个连接。并接收一个入站的 Quic 流, 一个 QuicConnection 可以支持多个流。
var connection = await listener.AcceptConnectionAsync();
Console.WriteLine($"Client [connection.RemoteEndPoint]: connected");
var stream = await connection.AcceptInboundStreamAsync();
Console.WriteLine($"Stream [stream.Id]: created");
使用 System.IO.Pipeline 处理流数据,读取行数据,并回复一个 ack 消息。
await ProcessLinesAsync(stream);
// 处理流数据
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))
// Process the line.
ProcessLine(line);
// Ack
//await writer.WriteAsync(System.Text.Encoding.UTF8.GetBytes($"ack: DateTime.Now.ToString("HH:mm:ss") \\n"));
// Tell the PipeReader how much of the buffer has been consumed.
reader.AdvanceTo(buffer.Start, buffer.End);
// Stop reading if there's no more data coming.
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)
// Look for a EOL in the buffer.
SequencePosition? position = buffer.PositionOf((byte)'\\n');
if (position == null)
line = default;
return false;
// Skip the line + the \\n.
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();
4.2 客户端
4.2.1 单个流
直接使用 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 消息,因为我们创建了一个双向流。
4.2.2 多个流
我们上面说到了一个 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();
);
目录
完整服务端:
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.IO.Pipes;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
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()
)
);
// 生成证书
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以上是关于2022年11月 .NET CORE工具案例-.NET 7中的Quic通信的主要内容,如果未能解决你的问题,请参考以下文章
2022年11月 .NET CORE工具案例-.NET 7中的Quic通信
2022年11月 .NET CORE工具案例-StackExchange.Redis代码变量方式实现商品秒杀
2022年11月 .NET CORE工具案例-StackExchange.Redis代码变量方式实现商品秒杀
愚公系列2022年12月 .NET CORE工具案例-.NET Core使用PaddleOCRSharp进行身份证和车牌识别