前言

随着今年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 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 可以接收多个 连接。

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

扫码关注【半栈程序员】,获取最新文章。

基于 .NET 7 的 QUIC 实现 Echo 服务的更多相关文章

  1. 基于Spring Cloud、JWT 的微服务权限系统设计

    基于Spring Cloud.JWT 的微服务权限系统设计 https://gitee.com/log4j/pig https://github.com/kioyong/spring-cloud-de ...

  2. 在C#/.NET应用程序开发中创建一个基于Topshelf的应用程序守护进程(服务)

    本文首发于:码友网--一个专注.NET/.NET Core开发的编程爱好者社区. 文章目录 C#/.NET基于Topshelf创建Windows服务的系列文章目录: C#/.NET基于Topshelf ...

  3. Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台

    Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台: https://gitee.com/leecho/cola-cloud

  4. 构建基于阿里云OSS文件上传服务

    转载请注明来源:http://blog.csdn.net/loongshawn/article/details/50710132 <构建基于阿里云OSS文件上传服务> <构建基于OS ...

  5. 基于 EntityFramework 的数据库主从读写分离服务插件

    基于 EntityFramework 的数据库主从读写分离服务插件 1. 版本信息和源码 1.1 版本信息 v1.01 beta(2015-04-07),基于 EF 6.1 开发,支持 EF 6.1 ...

  6. Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务

    Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具:Spr ...

  7. Python网络编程--Echo服务

    Python网络编程--Echo服务 学习网络编程必须要练习的三个小项目就是Echo服务,Chat服务和Proxy服务.在接下来的几篇文章会详细介绍. 今天就来介绍Echo服务,Echo服务是最基本的 ...

  8. 基于 orange(nginx+openresty) + docker 实现微服务 网关功能

    摘要 基于 orange(nginx+openresty) + docker 实现微服务 网关功能 ;以实现 docker 独立容器 来跑 独立语言独立环境 在 同一个授权下 运行相关组合程序..年初 ...

  9. MaxCompute,基于Serverless的高可用大数据服务

    摘要:2019年1月18日,由阿里巴巴MaxCompute开发者社区和阿里云栖社区联合主办的“阿里云栖开发者沙龙大数据技术专场”走近北京联合大学,本次技术沙龙上,阿里巴巴高级技术专家吴永明为大家分享了 ...

随机推荐

  1. LOJ2312 LUOGU-P3733「HAOI2017」八纵八横 (异或线性基、生成树、线段树分治)

    八纵八横 题目描述 Anihc国有n个城市,这n个城市从1~n编号,1号城市为首都.城市间初始时有m条高速公路,每条高速公路都有一个非负整数的经济影响因子,每条高速公路的两端都是城市(可能两端是同一个 ...

  2. KingbaseES 数据库本地化配置 LC_CTYPE 和 LC_COLLATE

    区域支持指的是应用遵守文化偏好的问题,包括字母表.排序.数字格式等.PostgreSQL使用服务器操作系统提供的标准 ISO C 和POSIX的区域机制.更多的信息请参考你的系统的文档. 概述 区域支 ...

  3. 【读书笔记】C#高级编程 第六章 数组

    (一)同一类型和不同类型的多个对象 如果需要使用同一类型的多个对象,就可以使用数组或集合(后面章讲). 如果需要使用不同类型的多个对象,可以使用Tuple(元组)类型. (二)简单数组 如果需要使用同 ...

  4. 微软出品自动化神器Playwright,不用写一行代码(Playwright+Java)系列(一) 之 环境搭建及脚本录制

    一.前言 半年前,偶然在视频号刷到某机构正在直播讲解Playwright框架的使用,就看了一会,感觉还不错,便被种草,就想着自己有时间也可以自己学一下,这一想着就半年多过去了. 读到这,你可能就去百度 ...

  5. Docker(一):初识

    1.什么是Docker   Docker 是一个基于Go 语言并遵从Apache2.0协议开源的.轻量级的容器引擎,主要运行于 Linux 和 Windows,用于创建.管理和编排容器.可以让开发者打 ...

  6. 基于 Apache Hudi + Presto + AWS S3 构建开放Lakehouse

    认识Lakehouse 数据仓库被认为是对结构化数据执行分析的标准,但它不能处理非结构化数据. 包括诸如文本.图像.音频.视频和其他格式的信息. 此外机器学习和人工智能在业务的各个方面变得越来越普遍, ...

  7. 修改NodePort的范围

    在 Kubernetes 集群中,NodePort 默认范围是 30000-32767,某些情况下,因为您所在公司的网络策略限制,您可能需要修改 NodePort 的端口范围 修改kube-apise ...

  8. 谣言检测(PLAN)——《Interpretable Rumor Detection in Microblogs by Attending to User Interactions》

    论文信息 论文标题:Interpretable Rumor Detection in Microblogs by Attending to User Interactions论文作者:Ling Min ...

  9. 【软件学习】如何将Typora中的本地图片上传到博客

    1. 配置方法 下载软件: 点击程序输入博客信息进行配置: 进行偏好设置: 2. 配置中出现的一些问题 解决方法:

  10. 故障复盘究竟怎么做?美图SRE结合10年经验做了三大总结(附模板)

    美图崇尚的故障文化是 "拥抱故障,卓越运维",倡导的基准是 No-Blame, 即「不指责,重改进」.今年 9 月 TakinTalks 社区曾经分享过美图的三段式故障治理方法(美 ...