必看!.NET 7 在網路領域的四大更新

微軟技術棧發表於2023-01-03

最新的 .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。下表總結了每種流型別的每種方法的表現(注意客戶端和伺服器都可以開放和接受流):

image.png

在以上這些方法中,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.3RFC 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 效能改進~

相關文章