2022年11月.netcore工具案例-.net7中的quic通信(代码片段)

微软MVPEleven 微软MVPEleven     2022-12-10     239

关键词:

前言

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 = Convert2022年11月.netcore工具案例-.netcore执行javascript(代码片段)

文章目录前言一、.NETCore执行JavaScript1.nuget安装Jint2.Jint的相关操作2.1执行表达式2.2函数映射2.3类赋值2.4函数执行2.5对象转换备注前言Jint是适用于.NET的开源Javascript解释器,功能强大,它可以在任何现代.NET平台上运行,... 查看详情

2022年10月.netcore工具案例-diffplex文本差异组件(代码片段)

文章目录前言一、DiffPlex文本差异组件1.Nuget安装DiffPlex组件2.比较代码前言对于开发人员来说,Git是我们经常使用的工具,在每次编写完代码并提交后,我们可以通过gitdiff来对比不同版本之间的代码的差异,当然... 查看详情

2023年01月.netcore工具案例-.net7中的webtransport通信(代码片段)

文章目录前言1.技术背景2.QUIC相关概念3.HTTP/3.0一、WebTransport1.WebTransport概念2.WebTransport在js中的使用3.WebTransport在.NET7中的使用3.1创建项目3.2侦听HTTP/3端口3.3获取WebTransport会话3.4监听WebTransport请求4.WebTransport在JavaScript中使用4.1创建We 查看详情

2022年12月.netcore工具案例-csredis执行lua脚本实现商品秒杀(代码片段)

文章目录前言一、CSRedis执行Lua脚本实现商品秒杀1.单线程模拟多线程进行秒杀2.多线程进行秒杀前言下面是Redis分布式锁常用的概念说明:设置、获取、过期时间、删除。1、Setnx命令:SETNXkeyvalue说明:将key的值设为valu... 查看详情

愚公系列2022年12月.netcore工具案例-.netcore使用paddleocrsharp进行身份证和车牌识别(代码片段)

文章目录前言一、.NETCore使用PaddleOCRSharp进行身份证识别1.安装nueget包2.测试身份证识别3.测试车牌识别二、可在桌面直接使用的软件前言PaddleOCRSharp是一个基于百度飞桨PaddleOCR的C++代码修改并封装的.NET的OCR工具类库。包含文... 查看详情

2022年11月.netcore工具案例-stackexchange.redis代码变量方式实现商品秒杀(代码片段)

文章目录前言一、StackExchange.Redis执行Lua脚本实现商品秒杀1.StackExchange.Redis封装2.秒杀代码3.效果前言下面是Redis分布式锁常用的概念说明:设置、获取、过期时间、删除。1、Setnx命令:SETNXkeyvalue说明:将key的值设为valu... 查看详情

2022年11月.netcore工具案例-stackexchange.redis代码变量方式实现商品秒杀(代码片段)

文章目录前言一、StackExchange.Redis执行Lua脚本实现商品秒杀1.StackExchange.Redis封装2.秒杀代码3.效果前言下面是Redis分布式锁常用的概念说明:设置、获取、过期时间、删除。1、Setnx命令:SETNXkeyvalue说明:将key的值设为valu... 查看详情

2022年11月influxdb数据库-.netcore中的使用(代码片段)

前言InfluxDB是一个开源的时间序列数据库。它在单个二进制文件中拥有时间序列平台所需的一切-多租户时间序列数据库、UI和仪表板工具、后台处理和监视代理。所有这些都使部署和设置变得轻而易举且更易于保护。InfluxDB平台... 查看详情

2023年01月.netcore工具案例-.net7中的webtransport通信(代码片段)

文章目录前言1.技术背景2.QUIC相关概念3.HTTP/3.0一、WebTransport1.WebTransport概念2.WebTransport在js中的使用3.WebTransport在.NET7中的使用3.1创建项目3.2侦听HTTP/3端口3.3获取WebTransport会话3.4监听WebTransport请求4.WebTransport在JavaScript中使用4.1创建We... 查看详情

#yyds干货盘点#愚公系列2023年03月.netcore工具案例-.netcore使用questpdf(代码片段)

...estPDFAPI文档地址:https://www.questpdf.com/api-reference/index一、.NETCore使用QuestPDF1.Nuget安装包Install-PackageQuestPDF2.官方案例usingQuestPDF.Fluent;usingQuestPDF.Helpers;usingQuestPDF.Infrastructure;//codeinyourmainmethodDocument.Create(container=>container.Pag 查看详情

2022年12月.netcore工具案例-csredis执行lua脚本实现商品秒杀(代码片段)

文章目录前言一、CSRedis执行Lua脚本实现商品秒杀1.单线程模拟多线程进行秒杀2.多线程进行秒杀前言下面是Redis分布式锁常用的概念说明:设置、获取、过期时间、删除。1、Setnx命令:SETNXkeyvalue说明:将key的值设为valu... 查看详情

2022年11月influxdb数据库-.netcore中的使用(代码片段)

...ps://github.com/influxdata/influxdb-client-csharp/tree/master/Client一、.NetCore中的使用1.下载InfluxDB,并配置因为官网下载需要注册下面直接给出下载地址:https://dl.influxdata.com/influxdb/releases/influxdb2-2.5.1-windows-amd64.zip2.执行InfluxDB解压下载... 查看详情

2022年10月litedb数据库-.netcore中的使用(代码片段)

...iteDB概念1.LiteDB的语法2.LiteDB的功能3.LiteDB支持的平台一、.NetCore中使用LiteDB1.创建项目2.Nuget安装LiteDB3.创建实体类4.打开数据库5.下面是一个增删改查的例子6.LiteDB进行文件存储二、LiteDB的管理工具前言LiteDB是一个小型、快速、轻量... 查看详情

愚公系列2023年02月.netcore工具案例-使用mailkit使用smtp协议进行邮件发送(代码片段)

文章目录前言1.MailKit简介2.MailKit功能3.SMTP协议一、使用MailKit使用SMTP协议进行邮件发送1.安装MailKit程序包2.发送操作文件代码前言1.MailKit简介MailKit是最流行且最强大的.NET邮件处理框架之一,下面为大家简单介绍MailKit的使用方... 查看详情

愚公系列2023年02月.netcore工具案例-使用mailkit使用pop3协议进行邮件读取(代码片段)

前言1.MailKit简介MailKit是最流行且最强大的.NET邮件处理框架之一,下面为大家简单介绍MailKit的使用方式(IMAP为例)2.MailKit功能安全SASL身份验证支持CRAM-MD5、DIGEST-MD5、LOGIN、NTLM、OAUTHBEARER、PLAIN、SCRAM-SHA-1、SCRAM-SHA-256、... 查看详情

微软.netcore3.1年底将结束支持,请升级到.net6

微软近日宣布,将于2022年12月13日停止为.NETCore3.1提供服务更新、安全修复和技术支持。.NETCore是一个免费开源的、用于Windows、Linux和macOS操作系统的软件框架。该项目主要由微软员工通过.NET基金会开发,并在MIT许可下发... 查看详情

愚公系列2022年05月.net架构班075-分布式中间件schedulemaster的基本使用

...案二、ScheduleMaster的基本使用1.ScheduleMaster准备2.MySQL准备3.NetCore客户端准备3.1新建测试业务类3.2然后通过CMD启动程序4.Hos.ScheduleMaster.Web准备4.1配置数据库4.2进入到`Hos.ScheduleMaster.Webpublish`目录中 查看详情

#yyds干货盘点#愚公系列2023年03月.netcore工具案例-stackexchange.redis代码变量方式实现商品秒杀(代码片段)

前言下面是Redis分布式锁常用的概念说明:设置、获取、过期时间、删除。1、Setnx命令:SETNXkeyvalue说明:将key的值设为value,当且仅当key不存在。若给定的key已经存在,则SETNX不做任何动作。SETNX是『SETifNoteXists』(如果不存在,则... 查看详情