基于 .NET 7 的 QUIC 实现 Echo 服务
前言
随着今年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 通信的示例。
- 分别创建了 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 服务的更多相关文章
- 基于Spring Cloud、JWT 的微服务权限系统设计
		基于Spring Cloud.JWT 的微服务权限系统设计 https://gitee.com/log4j/pig https://github.com/kioyong/spring-cloud-de ... 
- 在C#/.NET应用程序开发中创建一个基于Topshelf的应用程序守护进程(服务)
		本文首发于:码友网--一个专注.NET/.NET Core开发的编程爱好者社区. 文章目录 C#/.NET基于Topshelf创建Windows服务的系列文章目录: C#/.NET基于Topshelf ... 
- Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台
		Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台: https://gitee.com/leecho/cola-cloud 
- 构建基于阿里云OSS文件上传服务
		转载请注明来源:http://blog.csdn.net/loongshawn/article/details/50710132 <构建基于阿里云OSS文件上传服务> <构建基于OS ... 
- 基于 EntityFramework 的数据库主从读写分离服务插件
		基于 EntityFramework 的数据库主从读写分离服务插件 1. 版本信息和源码 1.1 版本信息 v1.01 beta(2015-04-07),基于 EF 6.1 开发,支持 EF 6.1 ... 
- Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务
		Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具:Spr ... 
- Python网络编程--Echo服务
		Python网络编程--Echo服务 学习网络编程必须要练习的三个小项目就是Echo服务,Chat服务和Proxy服务.在接下来的几篇文章会详细介绍. 今天就来介绍Echo服务,Echo服务是最基本的 ... 
- 基于 orange(nginx+openresty) + docker 实现微服务 网关功能
		摘要 基于 orange(nginx+openresty) + docker 实现微服务 网关功能 ;以实现 docker 独立容器 来跑 独立语言独立环境 在 同一个授权下 运行相关组合程序..年初 ... 
- MaxCompute,基于Serverless的高可用大数据服务
		摘要:2019年1月18日,由阿里巴巴MaxCompute开发者社区和阿里云栖社区联合主办的“阿里云栖开发者沙龙大数据技术专场”走近北京联合大学,本次技术沙龙上,阿里巴巴高级技术专家吴永明为大家分享了 ... 
随机推荐
- what the difference betweent pin page and lock page ?
			以前在项目中,大家为了避免自己使用的page被换出,使用的方式是mlock,从mlock的实现的看,它限制了page被swap, 然后在一个swap off的系统中,这样其实和mlock调用与否没有关 ... 
- 【Java】学习路径47-线程锁synchronized
			线程安全问题: 简单来说,就是多个线程在操作同一个变量时引起的问题. 这里是用一个简单的例子说明一下: 以Runnable创建的线程为例:一个售票系统,count代表当前票数,卖出一张count--. ... 
- 第一章 kubernetes概述
			一.Kubernetes概述 1.官网地址:https://kubernetes.io 2.GuiHub:https://github.com/kubernetes/kubernetes 3.又来:谷 ... 
- 【设计模式】Java设计模式 - 桥接模式
			[设计模式]Java设计模式 - 桥接模式 不断学习才是王道 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 原创作品,更多关注我CSDN: 一个有梦有戏的人 准备将博客园.CSDN一起 ... 
- 后缀自动机(SAM)+广义后缀自动机(GSA)
			经过一顿操作之后竟然疑似没退役0 0 你是XCPC选手吗?我觉得我是! 稍微补一点之前丢给队友的知识吧,除了数论以外都可以看看,为Dhaka和新队伍做点准备... 不错的零基础教程见 IO WIKI ... 
- 我的Go并发之旅、01 并发哲学与并发原语
			注:本文所有函数名为中文名,并不符合代码规范,仅供读者理解参考. 上下文 上下文(Context)代表了程序(也可以是进程,操作系统,机器)运行时的环境和状态,联系程序整个生命周期与资源调用,是程序可 ... 
- 【java8新特性】02:常见的函数式接口
			Jdk8提供的函数式接口都在java.util.function包下,Jdk8的函数式类型的接口都有@FunctionInterface注解所标注,但实际上即使没有该注解标注的有且只有一个抽象方法的接 ... 
- Java SE Object类
			1.Object类 equals 方法 == 和 equals 的对比 == 是一个比较运算符 == 既可以判断基本类型,又可以判断引用类型 == 如果判断的是基本类型,判断的是值是否相等 == 如果 ... 
- kibana配置文件kibana.yml参数详解
			server.port: 默认值: 5601 Kibana 由后端服务器提供服务,该配置指定使用的端口号. server.host: 默认值: "localhost" 指定后端服务 ... 
- Elastic:Elasticsearch的分片管理策略
