原文 | 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"
}
}
}
QUIC
OpenSSL 3 支持
当前大多数 Linux 发行版在其最新版本中都采用了 OpenSSL 3:
- Debian 12+:Bookworm OpenSSL
- Ubuntu 22+:Jammy OpenSSL
- Fedora 37+:Fedora OpenSSL
- OpenSUSE:Tumbleweed OpenSSL
- AlmaLinux 9+:AlmaLinux 9 软件包存储库
.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(作为预览功能)后,我们收到了几个有关异常的问题:
- dotnet/runtime#78751:当找不到主机时,QuicConnection.ConnectAsync 会引发 SocketException
- dotnet/runtime#78096:QuicListener AcceptConnectionAsync 和 OperationCanceledException
- dotnet/runtime#75115:QuicListener.AcceptConnectionAsync 重新抛出异常
在 .NET 8 中,System.Net.Quic 异常行为在 dotnet/runtime#82262 中进行了彻底修改,并解决了上述问题。
修订的主要目标之一是确保 System.Net.Quic 中的异常行为在整个命名空间中尽可能一致。总的来说,当前的行为可以总结如下:
- QuicException:特定于 QUIC 协议或与其处理相关的所有错误。
- 连接在本地或由对等方关闭。
- 连接因不活动而闲置。
- 流在本地或由对等方中止。
- QuicError 中描述的其他错误
- SocketException:用于网络问题,例如网络状况、名称解析或用户错误。
- 地址已被使用。
- 无法到达目标主机。
- 指定的地址无效。
- 无法解析主机名。
- AuthenticationException:适用于所有 TLS 相关问题。目标是具有与 SslStream 类似的行为。
- 证书相关错误。
- ALPN 协商错误。
- 握手期间用户取消。
- ArgumentException:当提供的 QuicConnectionOptions 或 QuicListenerOptions 无效时。
- 提供的流限制不在 0-65535 范围内。
- 省略强制属性,例如:DefaultCloseErrorCode 或 DefaultStreamErrorCode。
- 未指定 ClientAuthenticationOptions 或 ServerAuthenticationOptions。
- 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 地址数据。
此外,新添加的 ReceiveFrom 和 ReceiveFromAsync 重载不会实例化每次调用时都会有一个新的 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-io 和 mmarinchenko。
IP网络
.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 地址和前缀长度进行构造,也可以通过 TryParse 或 Parse 从字符串进行解析。最重要的是,它允许使用 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 存储库中与我们联系。
最后,我要感谢我的合著者:
- @antonfirsov 是 Metrics 的作者。
- @CarnaViire 编写了 HttpClientFactory。
原文链接
.NET 8 Networking Improvements
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com)