必看!.NET 7 在网络领域的四大更新

news2024/10/4 23:25:55

最新的 .NET 7 现已发布,我们想介绍一下其在网络领域所做的一些有趣的更改和添加。这篇文章我们将讨论 .NET 7 在 HTTP 空间、新 QUIC API、网络安全和 WebSockets 方面的变化。

HTTP

改进了对连接尝试失败的处理

在 .NET 6 之前的版本中,如果连接池中没有立即可用的连接,(处理程序上的设置允许的情况下,例如 HTTP /1.1 中的 MaxConnectionsPerServer,或 HTTP/2 中的EnableMultipleHttp2Connections)新的 HTTP 请求始终会发起新的连接尝试并等待响应。这样做的缺点是,建立该连接需要一段时间,而在这段时间里如果另一个连接已经可用,该请求仍将继续等待它生成的连接,从而影响延迟。在 .NET 6.0 中我们改变了这一进程,无论是新建立的连接还是与此同时准备好处理请求的另一个连接,第一个可用的连接会处理请求。这样仍会一个新连接被建立(受限制),如果发起的请求未使用这一连接,则它会被合并以供后续请求使用。

不幸的是,.NET 6.0 中的这一功能对某些用户来说是有问题的:失败的连接尝试也会使位于请求队列顶部的请求失败,这可能会在某些情况下导致意外的请求失败。此外,如果由于某些原因(例如由于服务器行为不当或网络问题)池中有一个永远未被使用的连接,与之关联的新传入的请求也将延迟并可能超时。

在 .NET 7.0 中,我们实施了以下更改来解决这些问题:

  • 失败的连接尝试只能使其相关的发起请求失败,而不会导致无关的请求失败。如果在连接失败时原始请求已得到处理,则连接失败将被忽略 ( dotnet/runtime#62935 )。
  • 如果一个请求发起了一个新连接,但随后被池中的另一个连接处理,则新的待使用的连接尝试将不管 ConnectTimeout,在短时间后自动超时。通过此更改,延迟的连接将不会延迟不相关的请求 ( dotnet/runtime#71785 )。请注意,不被使用的连接尝试自动超时失败的这一进程只会在后台自己运行,用户不会看到此进程。观察它们的唯一方法是启用 telemetry。

HttpHeaders 读取线程安全

这些 HttpHeaders 集合从来都不是线程安全的。访问 header 可能会强制延迟解析它的值,从而导致对底层数据结构的修改。

在 .NET 6 之前,同时读取集合在大多数情况下恰好是线程安全的。

从 .NET 6 开始,由于内部不再需要锁定,针对 header 解析执行的锁定较少。这一变化导致许多用户错误地同时访问 header,例如,在 gRPC (dotnet/runtime#55898)、NewRelic (newrelic/newrelic-dotnet-agent#803)甚至 HttpClient 本身( dotnet/runtime #65379)。违反 .NET 6 中的线程安全可能会导致 header 值重复/格式错误或在枚举(enumeration)/header 访问期间产生各种异常。

.NET 7 使 header 行为更加直观。该 HttpHeaders 集合现在符合 Dictionary 线程安全保证:

集合可以同时支持多个读者,只要它不被修改。极少数情况下,枚举(enumeration)与书写访问权限争用,则该集合必须在整个枚举期间被锁定。要允许多个线程访问集合以同时进行读写,您必须实现自己的同步。

这是通过以下更改实现的:

  • 无效值的“验证读取”不会删除无效值 – dotnet/runtime#67833(感谢@heathbm)。
  • 同时读取是线程安全的——dotnet/runtime#68115。

检测 HTTP/2 和 HTTP/3 协议错误

HTTP/2 和 HTTP/3 协议在 RFC 7540 第 7 节和 RFC 9114 第 8.1节中定义了协议级别的错误代码,例如,HTTP/2 中的 REFUSED_STREAM (0x7) 或 HTTP/3 中的 H3_EXCESSIVE_LOAD (0x0107) 。与 HTTP 状态代码不同,这是对大多数 HttpClient 用户来说不重要的低级错误信息,但它在高级 HTTP/2 或 HTTP/3 场景中有帮助,特别是 grpc-dotnet,其中区分协议错误对于实现客户端重试至关重要。

我们定义了一个新的异常 HttpProtocolException 来在其 ErrorCode 属性中保存协议级错误代码。

HttpClient 直接调用时,HttpProtocolException 可以是内部异常HttpRequestException:

try
{
using var response = await httpClient.GetStringAsync(url);
}
catch (HttpRequestException ex) when (ex.InnerException is HttpProtocolException pex)
{
    Console.WriteLine("HTTP error code: " + pex.ErrorCode)
}

使用 HttpContent 的响应流时,它被直接抛出:

using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
using var responseStream = await response.Content.ReadAsStreamAsync();
try
{
await responseStream.ReadAsync(buffer);
}
catch (HttpProtocolException pex)
{
    Console.WriteLine("HTTP error code: " + pex.ErrorCode)
}

HTTP/3

在 HttpClient 之前的 .NET 版本已经完成了对 HTTP/3 的支持,所以我们主要集中精力在这个领域的 System.Net.Quic 底层。尽管如此,我们确实在 .NET 7 中引入了一些修复和更改。

最重要的变化是现在默认启用 HTTP/3 ( dotnet/runtime#73153 )。这并不意味着从现在开始所有 HTTP 请求都将首选 HTTP/3,但在某些情况下它们可能会升级到HTTP/3。为此,请求必须通过将HttpRequestMessage.VersionPolicy设置为RequestVersionOrHigher,从而能够版本升级。然后,如果服务器在 Alt-Svc header 中有 HTTP/3 授权,HttpClient 将使用它进行进一步的请求,请参阅 RFC 9114 第 3.1.1 节。

QUIC

QUIC 是一种新的传输层协议。它最近已在 RFC 9000 中标准化。它使用 UDP 作为底层协议,并且它本质上是安全的,因为它要求使用 TLS 1.3。与众所周知的传输协议(如 TCP 和 UDP)的另一个有趣区别是它在传输层上内置了流多路复用。这使其能够拥有多个并发的独立数据流,且这些数据流不会相互影响。

QUIC 本身没有为交换的数据定义任何语义,因为它是一种传输协议。它更适用于应用层协议,例如 HTTP/3 或 SMB over QUIC。它还可以用于任何自定义协议。

与 TLS 的 TCP 相比,该协议具有许多优势。例如,它不需要像顶部带有 TLS 的 TCP 那样多的往返行程,所以能够更快地建立连接。它能够避免队头阻塞问题,一个丢失的数据包不会阻塞所有其他流的数据。另一方面,使用 QUIC 也有缺点。由于它是一个新协议,它的采用仍在增长并且是有限的。除此之外,QUIC 流量甚至可能被某些网络组件阻止。

.NET 中的 QUIC

我们在 System.Net.Quic 库中介绍了 .NET 5 中的 QUIC 实现。然而,到目前为止,这个库是仅限内部的,并且只为自己的 HTTP/3 实现服务。随着 .NET 7 的发布,我们公开了该库并公开了它的 API。由于我们只有 HttpClient 和 Kestrel 作为此版本 API 的使用者,因此我们决定将它们保留为预览功能。它使我们能够在确定最终形式之前在下一个版本中调整 API。

从实施的角度来看,System.Net.Quic 取决于 QUIC 协议的原生实现 MsQuic。因此,System.Net.Quic 平台支持和依赖项继承自 MsQuic,并记录在 HTTP/3 平台依赖项文档中。简而言之,MsQuic 库作为 .NET for Windows 的一部分提供。对于 Linux,libmsquic 必须通过适当的包管理器手动安装。对于其他平台,仍然可以手动构建 MsQuic,无论是针对 SChannel 还是 OpenSSL,并将其与 System.Net.Quic 一起使用。

API 概述

System.Net.Quic 携带了能够使用 QUIC 协议的三个主要类:

  • QuicListener – 服务器端类,用于接受传入连接。
  • QuicConnection – QUIC 连接,对应 RFC 9000 Section 5。
  • QuicStream – QUIC 流,对应 RFC 9000 Section 2。

但是在使用这些类之前,用户代码应该检查当前系统是否支持 QUIC,因为系统可能缺失 libmsquic 或者不支持 TLS 1.3。为此, QuicListener 和 QuicConnection 都公开了一个静态属性 IsSupported:

if (QuicListener.IsSupported)
{QuicListenerOptions
// Use QuicListener
}
else
{
// Fallback/Error
}

if (QuicConnection.IsSupported)
{
// Use QuicConnection
}
else
{
// Fallback/Error
}

请注意,目前这两个属性是同步的并将显示相同的值,但将来可能会改变。所以我们建议检查一下支持服务器场景的 QuicListener.IsSupported 和用于客户端的 QuicListener.IsSupported。

QuicListener

QuicListener 属于接受客户端的传入连接的服务器端类。该侦听设备是通过静态方法 QuicListener.ListenAsync 构造和启动的。该方法接受 QuicListenerOptions 类的一个实例,其中包含启动侦听设备和接受传入连接所需的所有设置。之后,侦听设备着手通过 AcceptConnectionAsync 分发连接。此方法返回的连接始终是完全连接的,这意味着 TLS 交互已经完成,连接可以使用了。最后,要关闭侦听设备并释放所有资源,必须调用 DisposeAsync 方法。

更多关于这个类设计的细节可以在QuicListener API Proposal (dotnet/runtime#67560) 中找到。

QuicConnection

是用于服务器端和客户端 QUIC 连接的类。服务器端连接由侦听设备内部创建,并通过 QuicListener.AcceptConnectionAsync 分发连接。客户端连接必须被打开并连接到服务器。静态方法 QuicConnection 和侦听设备一起,建立并实例连接。它接受 QuicClientConnectionOptions 的实例,这是一个类似于 QuicServerConnectionOptions 的类。初次之前,此链接的工作方式与客户端和服务器之间没有区别。它可以打开向外和向内的流。它还提供与连接信息有关的属性,如 LocalEndPoint、RemoteEndPoint 或 RemoteCertificate。

当连接的工作完成后,需要关闭和处置侦听设备。QUIC 协议要求使用应用层代码立即关闭侦听设备,参见 RFC 9000 Section 10.2。为此,可以调用带有应用层代码的 CloseAsync ,如果没有,DisposeAsync 将使用 QuicConnectionOptions.DefaultCloseErrorCode 中提供的代码。无论是哪种方式,都必须在连接工作结束时调用 DisposeAsync,涌起完全释放所有相关资源。

更多关于这个类设计的细节可以在QuicConnection API Proposal (dotnet/runtime#68902) 中找到。

QuicStream

QuicStream 是 QUIC 协议中用于发送和接收数据的实际类型。它起源于普通的流 Stream。可以和普通的流一样使用,但它也提供了一些特定于 QUIC 协议的特性。首先,QUIC 流可以是单向的,也可以是双向的,参见 RFC 9000 Section 2.1。双向流能够在两端发送和接收数据,而单向流只能从发起端输入数据,从接受端读取。每端都可以限制每种类型的并发流的数量,参见QuicConnectionOptions.MaxInboundBidirectionalStreams 与 QuicConnectionOptions.MaxInboundUnidirectionalStreams。

QUIC 流的另一个特点是能够在流的工作过程中显式地关闭写入端,参见CompleteWrites or WriteAsync?ocid=AID3052907)-system-boolean-system-threading-cancellationtoken)) 重载 CompleteWrites 参数。关闭写入端可以让接受端明确不会再有更多的数据输入了,但另一端仍然可以继续发送数据流(在双向流的情况下)。这在 HTTP 请求/响应交换等场景中非常有用,客户端发送请求并关闭写入端,服务器就知道这是请求发送的所有内容了。在此之后,服务器仍然能够发送响应,但客户端却不会发送更多的数据流了。而对于错误的情况,流的写入或读取端都可以进行中止,请参阅 Abort。下表总结了每种流类型的每种方法的表现(注意客户端和服务器都可以开放和接受流):

在以上这些方法中,quickstream 提供了两个专门的属性,用于在流的读写端关闭时获得通知:它们是 readclosed 和 WritesClosed。两者都返回一个任务,该任务完成后相应的边被关闭。无论任务是成功还是中止,其中都将包含相应的字段。当用户代码需要知道流端的关闭情况而不需要调用 ReadAsync 或 WriteAsync 时,这些属性非常就派上作用了。

最后,当流的工作完成时,它需要使用 DisposeAsync 方法。这一方法将确保读取/写入端(取决于流类型)都关闭。如果流直到结束还没有被正确读取,dispose 将发出一个等效的 Abort(QuicAbortDirection.Read) 命令。但是,如果流写入端还没有关闭,写入端会像使用 CompleteWrites 方法一样被关闭。之所以存在这种差异,是为了确保普通流的使用能够按照预期的方式进行,并进入相应的路径。

更多关于这个类设计的细节可以在 the QuicStream API Proposal (dotnet/runtime#69675) 中找到。

未来

由于 System.Net.Quic 是新公开的,并且用法有限,因此我们将不胜感激有关 API 形状的任何错误报告或见解。由于 API 处于预览模式,我们仍然有机会根据收到的反馈针对 .NET 8 调整它们。可以在 dotnet/runtime 存储库中提交问题。

 

安全

Negotiate API

Windows Authentication 是一个包含多种技术的术语,用于企业中针对中央机构(通常是域控制器)对用户和应用程序进行身份验证。它支持诸如单点登录电子邮件服务或 Intranet 应用程序之类的场景。用于身份验证的基础技术是 Kerberos、NTLM 和包含的 Negotiate 协议,其中为特定身份验证方案选择最合适的技术。

在 .NET 7 之前,Windows 身份验证在高级 API 中公开。例如 HttpClient (Negotiate 和 NTLM 身份验证方案),SmtpClient (GSSAPI 和 NTLM 身份验证方案),NegotiateStream、ASP.NET Core 和 SQL Server 客户端库。虽然这涵盖了最终用户的大多数场景,但它对库作者来说是有限的。其他库,如 Npgsql PostgreSQL client、 MailKit、 Apache Kudu client 需要诉诸各种技巧,为并非基于 HTTP 或其他可用的高级构建块构建的低级协议实施相同的身份验证方案。

.NET 7 引入了新的 API,提供低级构建块来执行上述协议的身份验证交换,请参阅dotnet/runtime#69920。与 .NET 中的所有其他 API  一样, 它在构建时考虑了跨平台互操作性。在 Linux、macOS、iOS 和其他类似平台上,它使用 GSSAPI 系统库。在 Windows 上,它依赖于 SSPI 库。对于系统实现不可用的平台,例如 Android 和 tvOS,存在有限的仅客户端实现。

如何使用 API

为了理解身份验证 API 的工作原理,让我们从一个示例开始,了解身份验证会话在 SMTP 等高级协议中的外观。该示例取自 Microsoft protocol documentation,该文档对其进行了更详细的解释。

S: 220 server.contoso.com Authenticated Receive Connector
C: EHLO client.contoso.com
S: 250-server-contoso.com Hello [203.0.113.1]
S: 250-AUTH GSSAPI NTLM
S: 250 OK
C: AUTH GSSAPI <token1>
S: 334 <token2>
C: <token3>
S: 235 2.7.0 Authentication successful

身份验证从客户端生成质询令牌开始。然后服务器产生响应。客户端处理响应,并向服务器发送新的质询。这种质询/响应交换可以发生多次。当任何一方拒绝认证或双方都接受认证时,它结束。令牌的格式由 Windows 身份验证协议定义,而封装是高级协议规范的一部分。在此示例中,SMTP 协议预先添加334代码以告知客户端服务器产生了身份验证响应,该235代码表示身份验证成功。

大部分新 API 都以新 NegotiateAuthentication 类为中心。它用于实例化客户端或服务器端身份验证的上下文。有多种选项可以指定建立经过身份验证的会话的要求,例如要求加密或确定要使用的特定协议 (Negotiate, Kerberos 或 NTLM) 指定参数后,身份验证将通过在客户端和服务器之间交换身份验证质询/响应来进行。GetOutgoingBlob 方法用于此目的。它可以处理字节跨度或 base64 编码的字符串。

以下代码将同时执行客户端和服务器,对同一台机器上的当前用户进行部分身份验证:

using System.Net;
using System.Net.Security;

var serverAuthentication = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions { });
var clientAuthentication = new NegotiateAuthentication(
    new NegotiateAuthenticationClientOptions
    {
        Package = "Negotiate",
        Credential = CredentialCache.DefaultNetworkCredentials,
        TargetName = "HTTP/localhost",
        RequiredProtectionLevel = ProtectionLevel.Sign
    });

string? serverBlob = null;
while (!clientAuthentication.IsAuthenticated)
{
    // Client produces the authentication challenge, or response to server's challenge
    string? clientBlob = clientAuthentication.GetOutgoingBlob(serverBlob, out var clientStatusCode);
    if (clientStatusCode == NegotiateAuthenticationStatusCode.ContinueNeeded)
    {
        // Send the client blob to the server; this would normally happen over a network
        Console.WriteLine($"C: {clientBlob}");
        serverBlob = serverAuthentication.GetOutgoingBlob(clientBlob, out var serverStatusCode);
        if (serverStatusCode != NegotiateAuthenticationStatusCode.Completed &&
            serverStatusCode != NegotiateAuthenticationStatusCode.ContinueNeeded)
        {
            Console.WriteLine($"Server authentication failed with status code {serverStatusCode}");
            break;
        }
        Console.WriteLine($"S: {serverBlob}");
    }
    else
    {
        Console.WriteLine(
            clientStatusCode == NegotiateAuthenticationStatusCode.Completed ?
            "Successfully authenticated" :
            $"Authentication failed with status code {clientStatusCode}");
        break;
    }
}

一旦建立了经过身份验证的会话,NegotiateAuthentication 实例就可以用于对传出消息进行签名/加密并验证/解密传入消息。这是通过 Wrap 和 Unwrap 方法完成的。

证书验证选项

当客户端收到服务器的证书时,反之亦然,如果请求客户端证书,证书将通过 X509Chain。验证始终进行,即使 RemoteCertificateValidationCallback 提供了验证,并且在验证期间可能会下载其他证书。由于无法控制此行为,因此提出了几个问题。其中包括完全阻止证书下载、设置超时或提供自定义存储以从中获取证书的要求。为了缓解这整组问题,我们决定在 SslClientAuthenticationOptions 和 SslServerAuthenticationOptions 上引入一个新属性 CertificateChainPolicy。此属性的目标是在 AuthenticateAsClientAsync / AuthenticateAsServerAsync 操作期间生成链时覆盖 SslStream 的默认行为。在正常情况下,X509ChainPolicy 在后台自动构建。但是,如果指定了这个新属性,它将优先并被使用,从而使用户能够完全控制证书验证过程。

更多信息可以在 API 提案 (dotnet/runtime#71191) 中找到。

TLS 简介

建立新的 TLS 连接是相当昂贵的操作,因为它需要多个步骤和多次往返。在经常重新创建与同一服务器的连接的情况下,握手所消耗的时间将加起来。TLS 提供的被称为会话恢复的功能可以缓解这种情况,请参阅 RFC 5246 Section 7.3 和 RFC 8446 Section 2.2。简而言之,在握手期间,客户端可以发送先前建立的 TLS 会话的标识,如果服务器同意,则根据先前连接的缓存数据重新建立安全上下文。尽管不同 TLS 版本的机制不同,但最终目标是相同的,即在重新建立与先前连接的服务器的连接时节省往返时间和一些 CPU 时间。此功能由 Windows 上的 SChannel 自动提供,但在 Linux 上使用 OpenSSL 需要进行几处更改才能启用此功能:

  • 服务器端(无状态)dotnet/runtime#57079 和 dotnet/runtime#63030
  • 客户端 – dotnet/runtime#64369
  • 缓存大小控制 – dotnet/runtime#69065

如果不需要缓存 TLS 上下文,可以使用环境变量“DOTNET_SYSTEM_NET_SECURITY_DISABLETLSRESUME”或通过 AppContext.SetSwitch “System.Net.Security.TlsCacheSize” 在进程范围内关闭它。

OCSP Stapling

Online Certificate Status Protocol (OCSP) Stapling 是服务器提供已签名和时间戳证明(OCSP 响应)的机制,证明发送的证书尚未被吊销,参见 RFC 6961。因此,客户端无需联系 OCSP 服务器本身,从而减少了建立连接所需的请求数量以及施加在 OCSP 服务器上的负载。由于 OCSP 响应需要由证书颁发机构 (CA) 签名,因此提供证书的服务器无法伪造它。在此版本之前,我们没有利用此 TLS 功能,有关详细信息请参阅 dotnet/runtime#33377。

跨平台的一致性

我们知道,.NET 提供的某些功能仅在某些平台上可用。但是每个版本我们都试图进一步缩小差距。在 .NET 7 中,我们对网络安全空间进行了一些更改,以改善差异:

  • 在适用于 TLS 1.3 的 Linux 上支持握手后认证 – dotnet/runtime#64268
  • 远程证书现已在 Windows 上设置 SslClientAuthenticationOptions.LocalCertificateSelectionCallback–dotnet/runtime#65134
  • 支持在 OSX 和 Linux 上的 TLS 握手中发送受信任 CA 的名称 – dotnet/runtime#65195

WebSockets

WebSocket 握手响应详细信息

在 .NET 7 之前,WebSocket 的开场握手(对升级请求的 HTTP 响应)的服务器响应部分隐藏在 ClientWebSocket 实现中, 并且所有握手错误都将显示为 WebSocketException,在异常信息旁边没有太多详细信息。但是,有关 HTTP 响应标头和状态代码的信息在失败和成功方案中都可能很重要。如果发生故障,HTTP 状态代码可以帮助区分可重试和不可重试的错误(例如,服务器根本不支持 WebSockets,或者只是暂时性网络错误)。标头还可能包含有关如何处理这种情况的其他信息。即使在 WebSocket 握手成功的情况下,标头也很有用,例如,它们可以包含与会话绑定的令牌、与子协议版本相关的信息,或者服务器可能很快关闭的信息。

.NET 7 将 CollectHttpResponseDetails 设置添加到  ClientWebSocketOptions 中,该设置允许在 ConnectAsync 调用期间收集 ClientWebSocket 实例中的升级响应详细信息。你稍后可以使用 ClientWebSocket 实例的 HttpStatusCode 和 HttpResponseHeaders 属性访问数据,即使在 ConnectAsync 引发异常的情况下也是如此。请注意,在特殊情况下,信息可能不可用,即如果服务器从未响应请求。

另请注意,如果连接成功,并且在使用 HttpResponseHeaders 数据后,可以通过将 ClientWebSocket.HttpResponseHeaders 属性设置为 null 来减少 ClientWebSocket 的内存占用量。

var ws = new ClientWebSocket();
ws.Options.CollectHttpResponseDetails = true;
try
{
    await ws.ConnectAsync(uri, cancellationToken);
    // success scenario
    ProcessSuccess(ws.HttpResponseHeaders);
    ws.HttpResponseHeaders = null; // clean up (if needed)
}
catch (WebSocketException)
{
    // failure scenario
    if (ws.HttpStatusCode != 0)
    {
        ProcessFailure(ws.HttpStatusCode, ws.HttpResponseHeaders);
    }
}

提供外部 HTTP 客户端

在默认情况下,ClientWebSocket 使用缓存的静态 HttpMessageInvoker 实例来执行 HTTP 升级请求。但是,有一些 ClientWebSocketOptions 会阻止缓存调用程序,例如 Proxy、ClientCertificates 或 Cookie。具有这些参数的 HttpMessageInvoker 实例不安全,每次调用 ConnectAsync 时都需要创建。这会导致许多不必要的分配,并使 HttpMessageInvoker 连接池无法重用。

.NET 7 允许您使用 ConnectAsync(Uri, HttpMessageInvoker, CancellationToken) 重载将现有的 HttpMessageInvoker (例如 HttpClient) 实例传递给 ConnectAsync 调用。在这种情况下,将使用提供的实例执行 HTTP 升级请求。

var httpClient = new HttpClient();

var ws = new ClientWebSocket();
await ws.ConnectAsync(uri, httpClient, cancellationToken);

请注意,如果传递了自定义 HTTP 调用程序,则不得设置以下任何 ClientWebSocketOptions,而应在 HTTP 调用程序上设置:

  • ClientCertificates
  • Cookies
  • Credentials
  • Proxy
  • RemoteCertificateValidationCallback
  • UseDefaultCredentials

以下是在 HttpMessageInvoker 实例上设置所有这些选项的方法:

var handler = new HttpClientHandler();
handler.CookieContainer = cookies;
handler.UseCookies = cookies != null;
handler.ServerCertificateCustomValidationCallback = remoteCertificateValidationCallback;
handler.Credentials = useDefaultCredentials ?
    CredentialCache.DefaultCredentials :
    credentials;
if (proxy == null)
{
    handler.UseProxy = false;
}
else
{
    handler.Proxy = proxy;
}
if (clientCertificates?.Count > 0)
{
    handler.ClientCertificates.AddRange(clientCertificates);
}
var invoker = new HttpMessageInvoker(handler);

var ws = new ClientWebSocket();
await ws.ConnectAsync(uri, invoker, cancellationToken);

WebSockets over HTTP/2

.NET 7 还增加了通过 HTTP/2 使用 WebSocket 协议的功能,如 RFC 8441 中所述。这样,WebSocket 连接是通过 HTTP / 2 连接上的单个流建立的。这允许同时在多个 WebSocket 连接和 HTTP 请求之间共享单个 TCP 连接,从而更有效地使用网络。

要通过 HTTP/2 启用 WebSockets,您可以将 ClientWebSocketOptions.HttpVersion 选项设置为HttpVersion.Version20。您还可以通过设置  ClientWebSocketOptions.HttpVersionPolicy 属性来启用所使用的 HTTP 版本的升级/降级。这些选项的行为方式与HttpRequestMessage.Version 和HttpRequestMessage.VersionPolicy 的行为方式相同。

例如,以下代码将探测 HTTP/2 WebSocket,如果无法建立 WebSocket 连接,它将回退到 HTTP/1.1:

var ws = new ClientWebSocket();
ws.Options.HttpVersion = HttpVersion.Version20;
ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
await ws.ConnectAsync(uri, httpClient, cancellationToken);

HttpVersion.Version11 和 HttpVersionPolicy.RequestVersionOrHigher 的组合将导致与上述相同的行为,而 HttpVersionPolicy.RequestVersionExact 将不允许升级/降级 HTTP 版本。

默认情况下,设置了 HttpVersion = HttpVersion.Version11 和 HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,这意味着只会使用 HTTP/1.1。

通过单个 HTTP/2 连接多路复用 WebSocket 连接和 HTTP 请求的能力是此功能的关键部分。为了使它按预期工作,您需要在调用 ConnectAsync 时从代码中传递并重用相同的 HttpMessageInvoker 实例(例如 HttpClient),即使用 ConnectAsync(Uri, HttpMessageInvoker, CancellationToken) 重载。这将重用 HttpMessageInvoker 实例中的连接池进行多路复用。

最后,感谢您成为 .NET 一员,如果您遇到问题或有任何反馈,都可以在文章下面留言,我们将竭力为您解决!

点我了解更多 .NET 7 性能改进~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/136573.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云计算运营—04 FusionSphere OpenStack 6.5方案介绍

FusionSphere OpenStack 6.5方案介绍 OpenStack 系统架构 OpenStack是什么 OpenStack是目前最流行的开源云操作系统&#xff1a; 资源抽象 OpenStack将各类硬件资源&#xff0c;通过虚拟化与软件定义的方式&#xff0c;抽象成资源池 资源分配与负载调度 OpenStack根据管理员…

Ardupilot EKF3核心算法《状态量的协方差矩阵推导》

目录 文章目录 目录摘要1.协方差矩阵推导2.关于 F的计算2.1 计算F的前四维关于四元数的状态方程2.2 计算F 的5-10维关于速度和位置的状态方程3.其他协方差的传播3.1 关于角增量偏差的协方差传播3.2 关于速度增量偏差的协方差传播3.3 关于地理坐标系地磁磁场矢量的协方差传播3.4…

【Vue基本指令】一.什么是Vue;二.Vue开发的方式;三.Vue的基本指令(重点)

目录 一.什么是Vue 1.前端技术的发展&#xff08;html、CSS、JavaScript&#xff09; &#xff08;1&#xff09;JQuery&#xff1a;是对JavaScript进行了封装&#xff0c;使得操作DOM、事件处理、动画处理、Ajax交互变得非常简洁、方便。是JavaScript的库。 &#xff08;&a…

《云原生》一文搞懂RocketMQ队列概述

目录 序 概念简述 一、客户端概念 1. Topic-主题 2.ConsumerGroup&#xff08;消费者组&#xff09; 概念一览图 二、消息传输模型 三、实践应用 1.配置文件 2.生产者 3.消费者 配置一览图 最后的话 序 接上一篇对rabbitMQ队列进行了梳理 《一文搞懂rabbitMQ消息…

shell技术

退出状态码 Shell 中运行的命令会使用0-255之间的整数值&#xff0c;作为退出状态码,并以此来告知shell该命令执行的状态。 通常情况下&#xff0c;约定0代表命令成功结束&#xff0c;非0代表程序非正常退出。 假如没有指定返回值&#xff0c;那么会用脚本的最后一个命令的执…

华为路由器配置笔记

路由器(Router),是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号,路由器工作在网络层,用来跨网段通信,路由器具有判断网络地址和选择IP路径的功能,它能在多网络互联环境中,建立灵活的连接,可用完全不同的数据分组和…

巧用数据分析表达式,让数据指标创建更简单

实现数据业务一体化的指标分析 从零售系统进化史get 数据统计的需求变更 零售系统需要的数据统计需求 V1.0 只需要获取当日累计的销售额&#xff0c;于是店老板就用 Excel或者纸质的表格创建了一个表&#xff0c;表中包含销售的日期时间&#xff0c;销售的产品&#xff0c;销…

c语言的变量和指针,怎么理解?

学会应用指针是C语言程序员的分水岭&#xff0c;也是C程序员级别的试金石。 变量可以分为基础变量、数组变量、指针变量&#xff0c;其中数组变量非常特殊&#xff0c;可以进一步分为基础数组变量和指针数组变量&#xff0c;所以暂时不考虑数组变量。假设我们在32位计算机上工…

【云原生】k8s之pod基础(下)

内容预知 1.pod的镜像拉取策略 1.1 镜像拉取说明 1.2 镜像拉取的策略 1.3 镜像拉取策略的设置操作 &#xff08;1&#xff09;Never策略的使用 &#xff08;2&#xff09;IfNotPresent策略在本地无镜像的情况下使用 &#xff08;3&#xff09; IfNotPresent策略在本地有…

客观认识植物乳杆菌 (L. plantarum) 及其健康益处

人体消化系统包含大约几百到几千种不同的细菌种类&#xff0c;其丰度构成因人而异。 其中少数益生菌乳杆菌属&#xff0c;即嗜酸乳杆菌、植物乳杆菌、短乳杆菌、乳酸乳杆菌、干酪乳杆菌、保加利亚乳杆菌、发酵乳杆菌、鼠李糖乳杆菌特异性产生细胞外蛋白、胞外多糖、细菌素和脂磷…

信息安全治理-信息安全状态示例

声明 本文是学习github5.com 网站的报告而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 信息安全治理-信息安全状态示例 组织可以生成一个信息安全状态&#xff0c;并将其作为信息安全的沟通工具披露给利益相关者。 组织宜选择和决定信息安全状态的格…

Curve 分布式存储在 KubeSphere 中的实践

Curve 介绍 Curve 是网易开发的现代存储系统&#xff0c;目前支持文件存储 (CurveFS) 和块存储 (CurveBS)。现在它作为一个沙盒项目托管在 CNCF。 Curve 是一个高性能、轻量级操作、本地云的开源分布式存储系统。Curve 可以应用于 : 1) 主流云本地基础设施平台 OpenStack 和…

【Bigdata】【Java】用IDEA创建一个Maven项目时,一直卡在Generating project in Batch mode步骤

Project Scenario&#xff08;项目场景&#xff09;&#xff1a; I want to create a Maven project with IDEA to practice writing UDF functions and upload it to hdfs, so I need to initialize the maven project. &#xff08;本人想用IDEA创建一个Maven项目来练习UDF函…

Netty初探

序&#xff1a; 为什么打算写Netty 相关的博客呢&#xff1f; Netty如今已经是应用非常广泛了&#xff0c; 很多框架底层都能看到他的影子&#xff0c;如Dubbo , Spring Gateway &#xff0c; RocketMQ、Elasticsearch、HBase 等比较出名的框架&#xff0c;在性能&#xff0c;…

使用div+css实现表格布局

DIVCSS是WEB设计标准&#xff0c;它是一种网页的布局方法。与传统中通过表格&#xff08;table&#xff09;布局定位的方式不同&#xff0c;它可以实现网页页面内容与表现相分离。提起DIVCSS组合&#xff0c;还要从XHTML说起。XHTML是一种在HTML&#xff08;标准通用标记语言的…

【MySQL】【systemd】mysqld_pre_systemd 及 mysqld@.service 的 bugs

mysqld_pre_systemd 及 mysqld.service 的 bugs问题原理mysqld_pre_systemd 的 bugsmysqld.service 的 bugs测试案例重现不指定 datadir 和 log-error 的 bugs开启 SELinux &#xff0c;指定不同于默认值的自定义数据目录和错误日志位置进行测试修正方法方法一&#xff1a;向 m…

【Word】MathType 运行时错误‘53’:文件未找到:MathPage.WLL

问题描述 1. 环境&#xff1a; MathType7.4Microsoft Office 365Windows 11 2. 问题 情景1. Microsoft Word 启动时显示 Please reload Word to load MathType addin properly 情景2. 安装MathType后在 Microsoft Word 中使用复制粘贴时报错 运行时错误‘53’ 情景3. 在 M…

JavaScript 对象-三种创建对象的方式,遍历获取到对象。

JavaScript 对象-三种创建对象的方式&#xff0c;遍历获取到对象。 目录JavaScript 对象-三种创建对象的方式&#xff0c;遍历获取到对象。1. 对象1.1 什么是对象&#xff1f;1.2 为什么需要对象2. 创建对象的三种方式2.1 利用字面量创建对象2.2 利用new Object创建对象2.3 利用…

【数组】leetcode209.有序数组的平方(C/C++/Java/Js)

leetcode209.长度最小的子数组1 题目2 思路-滑动窗口3 代码3.1 C版本3.2 C版本3.3 Java版本3.4 JavaScript版本4 总结1 题目 题源链接 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl1, …, nu…

系列教程之《高铁上的GO》-第一篇

作者&#xff1a;坚果&#xff0c;OpenHarmony布道师&#xff0c;OpenHarmony校源行开源大使&#xff0c;CSDN博客专家&#xff0c;电子发烧友鸿蒙MVP&#xff0c;51CTO博客专家博主&#xff0c;阿里云博客专家。 本文主要讲解Go是什么&#xff0c;Go如何安装&#xff0c;开发G…