【譯】.NET 8 網絡改進(三)

原文 | 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:

.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

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 地址和前綴長度進行構造,也可以通過 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])

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章