2

【译】.NET 8 网络改进(三) - 郑子铭

 7 months ago
source link: https://www.cnblogs.com/MingsonZheng/p/18013688
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

原文 | Máňa,Natalia Kondratyeva

翻译 | 郑子铭

简化的 SocketsHttpHandler 配置

.NET 8 添加了更方便、更流畅的方式来使用 SocketsHttpHandler 作为 HttpClientFactory 中的主处理程序 (dotnet/runtime#84075)。

您可以使用 UseSocketsHttpHandler 方法设置和配置 SocketsHttpHandler。您可以使用 IConfiguration 从配置文件设置 SocketsHttpHandler 属性,也可以从代码中配置它,或者可以结合使用这两种方法。

请注意,将 IConfiguration 应用于 SocketsHttpHandler 时,仅解析 bool、int、Enum 或 TimeSpan 类型的 SocketsHttpHandler 属性。 IConfiguration 中所有不匹配的属性都将被忽略。配置仅在注册时解析一次并且不会重新加载,因此处理程序在应用程序重新启动之前不会反映任何配置文件更改。

// sets up properties on the handler directly
services.AddHttpClient("foo")
    .UseSocketsHttpHandler((h, _) => h.UseCookies = false);

// uses a builder to combine approaches
services.AddHttpClient("bar")
    .UseSocketsHttpHandler(b =>
        b.Configure(config.GetSection($"HttpClient:bar")) // loads simple properties from config
         .Configure((h, _) => // sets up SslOptions in code
         {
            h.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
         });
    );
{
  "HttpClient": {
    "bar": {
      "AllowAutoRedirect": true,
      "UseCookies": false,
      "ConnectTimeout": "00:00:05"
    }
  }
}

OpenSSL 3 支持

当前大多数 Linux 发行版在其最新版本中都采用了 OpenSSL 3:

.NET 8 的 QUIC 支持已为此做好准备 (dotnet/runtime#81801)。

实现这一目标的第一步是确保 System.Net.Quic 下使用的 QUIC 实现 MsQuic 可以与 OpenSSL 3+ 一起使用。这项工作发生在 MsQuic 存储库 microsoft/msquic#2039 中。下一步是确保 libmsquic 包的构建和发布具有对特定发行版和版本的默认 OpenSSL 版本的相应依赖性。例如 Debian 发行版:

最后一步是确保正在测试正确版本的 MsQuic 和 OpenSSL,并且测试覆盖了所有 .NET 支持的发行版。

在 .NET 7 中发布 QUIC API(作为预览功能)后,我们收到了几个有关异常的问题:

在 .NET 8 中,System.Net.Quic 异常行为在 dotnet/runtime#82262 中进行了彻底修改,并解决了上述问题。

修订的主要目标之一是确保 System.Net.Quic 中的异常行为在整个命名空间中尽可能一致。总的来说,当前的行为可以总结如下:

  • QuicException:特定于 QUIC 协议或与其处理相关的所有错误。
    • 连接在本地或由对等方关闭。
    • 连接因不活动而闲置。
    • 流在本地或由对等方中止。
    • QuicError 中描述的其他错误
  • SocketException:用于网络问题,例如网络状况、名称解析或用户错误。
    • 地址已被使用。
    • 无法到达目标主机。
    • 指定的地址无效。
    • 无法解析主机名。
  • AuthenticationException:适用于所有 TLS 相关问题。目标是具有与 SslStream 类似的行为。
    • 证书相关错误。
    • ALPN 协商错误。
    • 握手期间用户取消。
  • ArgumentException:当提供的 QuicConnectionOptionsQuicListenerOptions 无效时。

  • OperationCanceledException:每当 CancellationToken 被触发取消时。
  • ObjectDisposeException:每当在已释放的对象上调用方法时。

请注意,上述示例并不详尽。

除了改变行为之外,QuicException 也发生了改变。其中一项更改是调整 QuicError 枚举值。现在 SocketException 涵盖的项目已被删除,并添加了用户回调错误的新值 (dotnet/runtime#87259)。新添加的 CallbackError 用于区分 QuicListenerOptions.ConnectionOptionsCallback 引发的异常与 System.Net.Quic 引发的异常 (dotnet/runtime#88614)。因此,如果用户代码抛出 ArgumentException,QuicListener.AcceptConnectionAsync 会将其包装在 QuicException 中,并将 QuicError 设置为 CallbackError,并且内部异常将包含原始用户抛出的异常。它可以这样使用:

await using var listener = await QuicListener.ListenAsync(new QuicListenerOptions
{
    // ...
    ConnectionOptionsCallback = (con, hello, token) =>
    {
        if (blockedServers.Contains(hello.ServerName))
        {
            throw new ArgumentException($"Connection attempt from forbidden server: '{hello.ServerName}'.", nameof(hello));
        }

        return ValueTask.FromResult(new QuicServerConnectionOptions
        {
            // ...
        });
    },
});
// ...
try
{
    await listener.AcceptConnectionAsync();
}
catch (QuicException ex) when (ex.QuicError == QuicError.CallbackError && ex.InnerException is ArgumentException)
{
    Console.WriteLine($"Blocked connection attempt from forbidden server: {ex.InnerException.Message}");
}

异常空间的最后一个更改是将传输错误代码添加到 QuicException 中 (dotnet/runtime#88550)。传输错误代码由 RFC 9000 传输错误代码定义,并且 MsQuic 的 System.Net.Quic 已经可以使用它们,只是没有公开公开。因此,QuicException 中添加了一个新的可为 null 的属性:TransportErrorCode。我们要感谢社区贡献者 AlexRach,他在 dotnet/runtime#88614 中实现了这一更改。

Sockets

套接字空间中最有影响力的更改是显着减少无连接 (UDP) 套接字的分配 (dotnet/runtime#30797)。使用 UDP 套接字时,分配的最大贡献者之一是在每次调用 Socket.ReceiveFrom 时分配一个新的 EndPoint 对象(并支持 IPAddress 等分配)。为了缓解这个问题,引入了一组使用 SocketAddress 的新 API (dotnet/runtime#87397)。 SocketAddress 在内部将 IP 地址保存为平台相关形式的字节数组,以便可以将其直接传递给操作系统调用。因此,在调用本机套接字函数之前不需要复制 IP 地址数据。

此外,新添加的 ReceiveFromReceiveFromAsync 重载不会实例化每次调用时都会有一个新的 IPEndPoint,而是在适当的位置改变提供的 receiveAddress 参数。所有这些一起可以用来提高 UDP 套接字代码的效率:

// Same initialization code as before, no change here.
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
byte[] message = Encoding.UTF8.GetBytes("Hello world!");
byte[] buffer = new byte[1024];
IPEndPoint endpoint = new IPEndPoint(IPAddress.Loopback, 12345);
server.Bind(endpoint);

// --------
// Original code that would allocate IPEndPoint for each ReceiveFromAsync:
Task<SocketReceiveFromResult> receiveTaskOrig = server.ReceiveFromAsync(buffer, SocketFlags.None, endpoint);
await client.SendToAsync(message, SocketFlags.None, endpoint);
SocketReceiveFromResult resultOrig = await receiveTaskOrig;

Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, result.ReceivedBytes) + " from " + result.RemoteEndPoint);
// Prints:
// Hello world! from 127.0.0.1:59769

// --------
// New variables that can be re-used for subsequent calls:
SocketAddress receivedAddress = endpoint.Serialize();
SocketAddress targetAddress = endpoint.Serialize();

// New code that will mutate provided SocketAddress for each ReceiveFromAsync:
ValueTask<int> receiveTaskNew = server.ReceiveFromAsync(buffer, SocketFlags.None, receivedAddress);
await client.SendToAsync(message, SocketFlags.None, targetAddress);
var length = await receiveTaskNew;

Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, length) + " from " + receivedAddress);
// Prints:
// Hello world! from InterNetwork:16:{233,121,127,0,0,1,0,0,0,0,0,0,0,0}

最重要的是,在 dotnet/runtime#86872 中改进了 SocketAddress 的使用。 SocketAddress 现在有几个额外的成员,使其本身更有用:

  • getter Buffer:访问整个底层地址缓冲区。
  • setter Size:能够调整上述缓冲区大小(只能调整到较小的大小)。
  • static GetMaximumAddressSize:根据地址类型获取必要的缓冲区大小。
  • 接口 IEquatable:SocketAddress 可用于区分套接字与之通信的对等点,例如作为字典中的键(这不是新功能,它只是使其可通过接口调用)。

最后,删除了一些内部制作的 IP 地址数据副本,以提高性能。

MIME 类型

添加缺失的 MIME 类型是网络空间中投票最多的问题之一 (dotnet/runtime#1489)。这是一个主要由社区驱动的更改,导致了 dotnet/runtime#85807 API 提案。由于此添加需要经过 API 审核流程,因此有必要确保添加的类型是相关的并遵循规范(IANA 媒体类型)。对于这项准备工作,我们要感谢社区贡献者 Bilal-iommarinchenko

.NET 8 中添加的另一个新 API 是新类型 IPNetwork (dotnet/runtime#79946)。该结构允许指定 RFC 4632 中定义的无类 IP 子网。例如:

  • 127.0.0.0/8 用于对应于 A 类子网的无类定义。
  • 42.42.128.0/17 用于 215 个地址的无类别子网。
  • 2a01:110:8012::/100 用于 228 个地址的 IPv6 子网。

新的 API 可以使用构造函数从 IP 地址和前缀长度进行构造,也可以通过 TryParseParse 从字符串进行解析。最重要的是,它允许使用 Contains 方法检查 IP 地址是否属于子网。示例用法如下:

// IPv4 with manual construction.
IPNetwork ipNet = new IPNetwork(new IPAddress(new byte[] { 127, 0, 0, 0 }), 8);
IPAddress ip1 = new IPAddress(new byte[] { 255, 0, 0, 1 });
IPAddress ip2 = new IPAddress(new byte[] { 127, 0, 0, 10 });
Console.WriteLine($"{ip1} {(ipNet.Contains(ip1) ? "belongs" : "doesn't belong")} to {ipNet}");
Console.WriteLine($"{ip2} {(ipNet.Contains(ip2) ? "belongs" : "doesn't belong")} to {ipNet}");
// Prints:
// 255.0.0.1 doesn't belong to 127.0.0.0/8
// 127.0.0.10 belongs to 127.0.0.0/8

// IPv6 with parsing.
IPNetwork ipNet = IPNetwork.Parse("2a01:110:8012::/96");
IPAddress ip1 = IPAddress.Parse("2a01:110:8012::1742:4244");
IPAddress ip2 = IPAddress.Parse("2a01:110:8012:1010:914e:2451:16ff:ffff");
Console.WriteLine($"{ip1} {(ipNet.Contains(ip1) ? "belongs" : "doesn't belong")} to {ipNet}");
Console.WriteLine($"{ip2} {(ipNet.Contains(ip2) ? "belongs" : "doesn't belong")} to {ipNet}");
// Prints:
// 2a01:110:8012::1742:4244 belongs to 2a01:110:8012::/96
// 2a01:110:8012:1010:914e:2451:16ff:ffff doesn't belong to 2a01:110:8012::/96

请注意,不应将此类型与自 1.0 以来 ASP.NET Core 中存在的 Microsoft.AspNetCore.HttpOverrides.IPNetwork 类混淆。我们预计 ASP.NET API 最终将迁移到新的 System.Net.IPNetwork 类型 (dotnet/aspnetcore#46157)。

最后的注释

本博文选择的主题并不是 .NET 8 中所做的所有更改的详尽列表,只是我们认为可能最有趣的主题。如果您对性能改进更感兴趣,您应该查看 Stephen 的大型性能博客文章中的网络部分。如果您有任何疑问或发现任何错误,您可以在 dotnet/runtime 存储库中与我们联系。

最后,我要感谢我的合著者:

.NET 8 Networking Improvements

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 ([email protected])


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK