基于 .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.mzph.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…