當前位置: 妍妍網 > 碼農

【譯】.NET 8 網路改進(一)

2024-02-12碼農

點選上方「DotNet NB」 關註公眾號

1

/

D o t N e t N B 223

原文 | Máňa,Natalia Kondratyeva

轉譯 | 鄭子銘

隨著新的 .NET 版本的釋出,釋出有關網路空間中新的有趣變化的部落格文章已成為一種傳統。今年,我們希望引入 HTTP 空間的變化、新添加的指標、新的 HttpClientFactory API 等。

HTTP協定

指標

.NET 8 使用 .NET 6 中引入的 System.Diagnostics.Metrics API 將內建 HTTP 指標添加到 ASP.NET Core 和 HttpClient。指標 API 和新內建指標的語意都是在與 OpenTelemetry 密切合作,確保新指標符合標準,並與 Prometheus 和 Grafana 等流行工具良好配合。

System.Diagnostics.Metrics API 引入了許多 EventCounters 所缺少的新功能。新的內建指標廣泛利用了這些功能,從而透過更簡單、更優雅的儀器集實作了更廣泛的功能。舉幾個例子:

  • 直方圖使我們能夠報告持續時間,例如。請求持續時間 (

    http.client.request.duration) 或連線持續時間 (

    http.client.connection.duration)。這些是沒有 EventCounter 對應項的新指標。

  • 多維性允許我們將標簽(又名內容或標簽)附加到測量值上,這意味著我們可以報告

    諸如 server.address (標識 URI 來源)或 error.type (描述請求失敗時的

    錯誤原因)之類的資訊測量值。多維還可以實作簡化:報告開啟的 HTTP 連線數 SocketsHttpHandler 使用 3 個 EventCounters:

    http11-connections-current-total、

    http20-connections-current-total 和 http30-connections-current-total,

    而 Metrics 相當於這些counters 是一個單一的工具,

    http.client.open_connections,其中 HTTP 版本是使用

    network.protocol.version 標簽報告的。

  • 為了幫助內建標簽不足以對傳出 HTTP 請求進行分類的用例,http.client.request.duration 指標支持註入使用者定義的標簽。這稱為濃縮。

  • IMeterFactory 整合可以隔離用於發出 HTTP 指標的 Meter 例項,

    從而更輕松地編寫針對內建測量執行驗證的測試,並實作此類測試的並列執行。

  • 雖然這並不是特定於內建網路指標,但值得一提的是 System.Diagnostics.Metrics 中的集合 API 也更高級:它們是強型別的且效能更高,並允許多個同時偵聽器和偵聽器存取未聚合的測量結果。

  • 這些優勢共同帶來了更好、更豐富的指標,這些指標可以透過 Prometheus 等第三方工具更有效地收集。得益於 PromQL(Prometheus 查詢語言)的靈活性,它允許針對從 .NET 網路堆疊收集的多維指標建立復雜的查詢,使用者現在可以深入了解 HttpClient 和 SocketsHttpHandler 例項的狀態和執行狀況,其級別如下:以前是不可能的。

    不利的一面是,我們應該提到,.NET 8 中只有 System.Net.Http 和 System.Net.NameResolution 元件使用 System.Diagnostics.Metrics 進行檢測,這意味著您仍然需要使用 EventCounters 從較低層提取計數器堆疊的級別,例如 System.Net.Sockets。雖然仍然支持以前版本中存在的所有內建 EventCounters,但 .NET 團隊預計不會對 EventCounters 進行大量新投資,並且在未來版本中將使用 System.Diagnostics.Metrics 添加新的內建檢測。

    有關使用內建 HTTP 指標的更多資訊,請閱讀我們有關 .NET 中的網路指標的教程。它包括有關使用 Prometheus 和 Grafana 進行收集和報告的範例,還演示了如何豐富和測試內建 HTTP 指標。有關內建工具的完整列表,請參閱 System.Net 指標的文件。如果您對伺服器端更感興趣,請閱讀有關 ASP.NET Core 指標的文件。

    擴充套件遙測

    除了新指標之外,.NET 5 中引入的現有基於 EventSource 的遙測事件還增加了有關 HTTP 連線的更多資訊 (dotnet/runtime#88853):

    - ConnectionEstablished(byte versionMajor, byte versionMinor)
    + ConnectionEstablished(byte versionMajor, byte versionMinor, long connectionId, string scheme, string host, int port, string? remoteAddress)
    - ConnectionClosed(byte versionMajor, byte versionMinor)
    + ConnectionClosed(byte versionMajor, byte versionMinor, long connectionId)
    - RequestHeadersStart()
    + RequestHeadersStart(long connectionId)

    現在,當建立新連線時,該事件會記錄其連線 ID 及其方案、埠和對等 IP 地址。這使得可以透過 RequestHeadersStart 事件將請求和響應與連線關聯起來(當請求關聯到池連線並開始處理時發生),該事件還記錄關聯的 ConnectionId。這在使用者想要檢視為其 HTTP 請求提供服務的伺服器的 IP 地址的診斷場景中尤其有價值,這是添加項背後的主要動機 (dotnet/runtime#63159)。

    事件可以透過多種方式使用,請參閱.NET 中的網路遙測 – 事件。但為了行程內增強日誌記錄,可以使用自訂 EventListener 將請求/響應對與連線數據相關聯:

    using IPLoggingListener ipLoggingListener = new();
    using HttpClient client = new();
    // Send requests in parallel.
    await Parallel.ForAsync(0, 1000, async (i, ct) =>
    {
    // Initialize the async local so that it can be populated by "RequestHeadersStart" event handler.
    RequestInfo info = RequestInfo.Current;
    usingvar response = await client.GetAsync("https://testserver");
    Console.WriteLine($"Response {response.StatusCode} handled by connection {info.ConnectionId}. Remote IP: {info.RemoteAddress}");
    // Process response...
    });
    internalsealed classRequestInfo
    {
    privatestaticreadonly AsyncLocal<RequestInfo> _asyncLocal = new();
    publicstatic RequestInfo Current => _asyncLocal.Value ??= new();
    publicstring? RemoteAddress;
    publiclong ConnectionId;
    }
    internalsealed classIPLoggingListener : EventListener
    {
    privatestaticreadonly ConcurrentDictionary<long, string> s_connection2Endpoint = new ConcurrentDictionary<long, string>();
    // EventId corresponds to [Event(eventId)] attribute argument and the payload indices correspond to the event method argument order.
    // See: https://github.com/dotnet/runtime/blob/a6e4834d53ac591a4b3d4a213a8928ad685f7ad8/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs#L100-L101
    privateconstint ConnectionEstablished_EventId = 4;
    privateconstint ConnectionEstablished_ConnectionIdIndex = 2;
    privateconstint ConnectionEstablished_RemoteAddressIndex = 6;
    // See: https://github.com/dotnet/runtime/blob/a6e4834d53ac591a4b3d4a213a8928ad685f7ad8/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs#L106-L107
    privateconstint ConnectionClosed_EventId = 5;
    privateconstint ConnectionClosed_ConnectionIdIndex = 2;
    // See: https://github.com/dotnet/runtime/blob/a6e4834d53ac591a4b3d4a213a8928ad685f7ad8/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs#L118-L119
    privateconstint RequestHeadersStart_EventId = 7;
    privateconstint RequestHeadersStart_ConnectionIdIndex = 0;
    protectedoverridevoidOnEventSourceCreated(EventSource eventSource)
    {
    if (eventSource.Name == "System.Net.Http")
    {
    EnableEvents(eventSource, EventLevel.LogAlways);
    }
    }
    protectedoverridevoidOnEventWritten(EventWrittenEventArgs eventData)
    {
    ReadOnlyCollection<object?>? payload = eventData.Payload;
    if (payload == null) return;
    switch (eventData.EventId)
    {
    case ConnectionEstablished_EventId:
    // Remember the connection data.
    long connectionId = (long)payload[ConnectionEstablished_ConnectionIdIndex]!;
    string? remoteAddress = (string?)payload[ConnectionEstablished_RemoteAddressIndex];
    if (remoteAddress != null)
    {
    Console.WriteLine($"Connection {connectionId} established to {remoteAddress}");
    s_connection2Endpoint.TryAdd(connectionId, remoteAddress);
    }
    break;
    case ConnectionClosed_EventId:
    connectionId = (long)payload[ConnectionClosed_ConnectionIdIndex]!;
    s_connection2Endpoint.TryRemove(connectionId, out _);
    break;
    case RequestHeadersStart_EventId:
    // Populate the async local RequestInfo with data from "ConnectionEstablished" event.
    connectionId = (long)payload[RequestHeadersStart_ConnectionIdIndex]!;
    if (s_connection2Endpoint.TryGetValue(connectionId, out remoteAddress))
    {
    RequestInfo.Current.RemoteAddress = remoteAddress;
    RequestInfo.Current.ConnectionId = connectionId;
    }
    break;
    }
    }
    }










    此外,重新導向事件已擴充套件為包含重新導向 URI:

    -voidRedirect();
    +voidRedirect(string redirectUri);

    HTTP 錯誤程式碼

    HttpClient 的診斷問題之一是,在發生異常時,不容易以編程方式區分錯誤的確切根本原因。區分其中許多的唯一方法是解析來自 HttpRequestException 的異常訊息。此外,其他 HTTP 實作(例如帶有 ERROR_WINHTTP_* 錯誤程式碼的 WinHTTP)以數位程式碼或列舉的形式提供此類功能。因此.NET 8引入了類似的列舉,並在HTTP處理丟擲的異常中提供它,它們是:

  • HttpRequestException 用於請求處理,直到收到響應檔頭。

  • 讀取響應內容時丟擲 HttpIOException。

  • dotnet/runtime#76644 API 提案中描述了 HttpRequestError 列舉的設計以及如何將其插入 HTTP 異常。

    現在,HttpClient 方法的使用者可以更輕松、更可靠地處理特定的內部錯誤:

    using HttpClient httpClient = new();
    // Handling problems with the server:
    try
    {
    using HttpResponseMessage response = await httpClient.GetAsync("https://testserver", HttpCompletionOption.ResponseHeadersRead);
    using Stream responseStream = await response.Content.ReadAsStreamAsync();
    // Process responseStream ...
    }
    catch (HttpRequestException e) when (e.HttpRequestError == HttpRequestError.NameResolutionError)
    {
    Console.WriteLine($"Unknown host: {e}");
    // --> Try different hostname.
    }
    catch (HttpRequestException e) when (e.HttpRequestError == HttpRequestError.ConnectionError)
    {
    Console.WriteLine($"Server unreachable: {e}");
    // --> Try different server.
    }
    catch (HttpIOException e) when (e.HttpRequestError == HttpRequestError.InvalidResponse)
    {
    Console.WriteLine($"Mangled responses: {e}");
    // --> Block list server.
    }
    // Handling problems with HTTP version selection:
    try
    {
    using HttpResponseMessage response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://testserver")
    {
    Version = HttpVersion.Version20,
    VersionPolicy = HttpVersionPolicy.RequestVersionExact
    }, HttpCompletionOption.ResponseHeadersRead);
    using Stream responseStream = await response.Content.ReadAsStreamAsync();
    // Process responseStream ...
    }
    catch (HttpRequestException e) when (e.HttpRequestError == HttpRequestError.VersionNegotiationError)
    {
    Console.WriteLine($"HTTP version is not supported: {e}");
    // Try with different HTTP version.
    }

    HTTPS 代理支持

    此版本實作的最受歡迎的功能之一是支持 HTTPS 代理 (dotnet/runtime#31113)。現在可以使用代理透過 HTTPS 處理請求,這意味著與代理的連線是安全的。這並沒有說明來自代理的請求本身,它仍然可以是 HTTP 或 HTTPS。如果是純文本 HTTP 請求,與 HTTPS 代理的連線是安全的(透過 HTTPS),然後是從代理到目標的純文本請求。如果是 HTTPS 請求(代理隧道),開啟隧道的初始 CONNECT 請求將透過安全通道 (HTTPS) 發送到代理,然後透過隧道將 HTTPS 請求從代理發送到目的地。

    要利用該功能,只需在設定代理時使用 HTTPS 方案即可:

    using HttpClient client = new HttpClient(new SocketsHttpHandler()
    {
    Proxy = new WebProxy("https://proxy.address:12345")
    });
    using HttpResponseMessage response = await client.GetAsync("https://httpbin.org/");

    HttpClientFactory

    .NET 8 擴充套件了配置 HttpClientFactory 的方式,包括客戶端預設設定、自訂日誌記錄和簡化的 SocketsHttpHandler 配置。這些 API 在 Microsoft.Extensions.Http 包中實作,該包可在 NuGet 上獲取,並包含對 .NET Standard 2.0 的支持。因此,此功能不僅適用於 .NET 8 上的客戶,而且適用於所有版本的 .NET,包括 .NET Framework(唯一的例外是僅適用於 .NET 5+ 的 SocketsHttpHandler 相關 API)。

    為所有客戶端設定預設值

    .NET 8 添加了設定預設配置的功能,該配置將用於 HttpClientFactory (dotnet/runtime#87914) 建立的所有 HttpClient。當所有或大多數註冊客戶端包含相同的配置子集時,這非常有用。

    考慮一個定義了兩個命名客戶端的範例,並且它們的訊息處理常式鏈中都需要 MyAuthHandler。

    services.AddHttpClient("consoto", c => c.BaseAddress = newUri("https://consoto.com/"))
    .AddHttpMessageHandler<MyAuthHandler>();
    services.AddHttpClient("github", c => c.BaseAddress = newUri("https://github.com/"))
    .AddHttpMessageHandler<MyAuthHandler>();

    要提取公共部份,您現在可以使用ConfigureHttpClientDefaults方法:

    services.ConfigureHttpClientDefaults(b => b.AddHttpMessageHandler<MyAuthHandler>());
    // both clients will have MyAuthHandler added by default
    services.AddHttpClient("consoto", c => c.BaseAddress = newUri("https://consoto.com/"));
    services.AddHttpClient("github", c => c.BaseAddress = newUri("https://github.com/"));

    與 AddHttpClient 一起使用的所有 IHttpClientBuilder 擴充套件方法也可以在 ConfigureHttpClientDefaults 中使用。

    預設配置 (ConfigureHttpClientDefaults) 在客戶端特定 (AddHttpClient) 配置之前套用於所有客戶端;他們在註冊中的相對位置並不重要。配置HttpClientDefaults可以註冊多次,在這種情況下,配置將按照註冊的順序一一套用。配置的任何部份都可以在特定於客戶端的配置中覆蓋或修改,例如,您可以為 HttpClient 物件或主處理常式設定其他設定,刪除以前添加的其他處理常式等。

    請註意,從 8.0 開始, ConfigureHttpMessageHandlerBuilder 方法已被棄用。您應該改用 ConfigurePrimaryHttpMessageHandler(Action<HttpMessageHandler,IServiceProvider>) ConfigureAdditionalHttpMessageHandlers 方法來分別修改先前配置的主處理常式或附加處理常式列表。

    // by default, adds User-Agent header, uses HttpClientHandler with UseCookies=false
    // as a primary handler, and adds MyAuthHandler to all clients
    services.ConfigureHttpClientDefaults(b =>
    b.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"))
    .ConfigurePrimaryHttpMessageHandler(() => newHttpClientHandler() { UseCookies = false })
    .AddHttpMessageHandler<MyAuthHandler>());
    // HttpClient will have both User-Agent (from defaults) and BaseAddress set
    // + client will have UseCookies=false and MyAuthHandler from defaults
    services.AddHttpClient("modify-http-client", c => c.BaseAddress = newUri("https://httpbin.org/"))
    // primary handler will have both UseCookies=false (from defaults) and MaxConnectionsPerServer set
    // + client will have User-Agent and MyAuthHandler from defaults
    services.AddHttpClient("modify-primary-handler")
    .ConfigurePrimaryHandler((h, _) => ((HttpClientHandler)h).MaxConnectionsPerServer = 1);
    // MyWrappingHandler will be inserted at the top of the handlers chain
    // + client will have User-Agent, UseCookies=false and MyAuthHandler from defaults
    services.AddHttpClient("insert-handler-into-chain"))
    .ConfigureAdditionalHttpMessageHandlers((handlers, _) =>
    handlers.Insert(0, newMyWrappingHandler());
    // MyAuthHandler (initially from defaults) will be removed from the handler chain
    // + client will still have User-Agent and UseCookies=false from defaults
    services.AddHttpClient("remove-handler-from-chain"))
    .ConfigureAdditionalHttpMessageHandlers((handlers, _) =>
    handlers.Remove(handlers.Single(h => h is MyAuthHandler)));