QUIC 通訊協定
QUIC 是 RFC 9000 中標準化的網路傳輸層通訊協定。 其使用 UDP 作為基礎通訊協定,而且本身就很安全,原因是會要求使用 TLS 1.3。 如需詳細資訊,請參閱 RFC 9001。 TCP 與 UDP 等已知傳輸通訊協定的其他特別差異是,其會在傳輸層上內建資料流多工。 這可讓多個並行獨立資料流不會相互影響。
QUIC 本身不會定義交換資料的任何語意,因為其即是傳輸通訊協定。 其更適用於應用程式層通訊協定,例如 HTTP/3 或 QUIC 上的 SMB。 另也可用於任何自訂定義的通訊協定。
通訊協定提供許多搭配使用 TCP 與 TLS 的優點,以下是幾項:
- 加快連線建立,因為與在其上搭配使用 TCP 與 TLS 不同,該通訊協定不需要多次來回行程。
- 在遺失一個封包時不會封鎖所有其他資料留的資料時,避免發生隊頭阻塞問題。
另一個方面,您也要考慮使用 QUIC 時的潛在缺點。 作為較新的通訊協定,其採用仍在發展中且有限制。 除此之外,某些網路元件甚至可能封鎖 QUIC 流量。
.NET 中的 QUIC
在 .NET 5 中已導入 QUIC 實作作為 System.Net.Quic
程式庫。 然而,直到 .NET 7 之前,這個程式庫僅作為 HTTP/3 的實作而存在,且僅限於內部使用。 使用 .NET 7 時,程式庫會設為公開,因此會公開其 API。
注意
在 .NET 7.0 和 8.0 中,這些 API 以預覽功能的形式發布。 從 .NET 9 開始,這些 API 不再視為預覽功能,而是穩定功能。
從實作的觀點來看,System.Net.Quic
取決於 MsQuic (QUIC 通訊協定的原生實作)。 因此,System.Net.Quic
平台支援和相依性會繼承自 MsQuic,並記載於 [平台相依性] 區段中。 簡言之,MsQuic 程式庫隨附於適用於 Windows 的 .NET。 但針對 Linux,您必須透過適當的套件管理員手動安裝 libmsquic
。 針對其他平台,無論針對 SChannel 或 OpenSSL,您仍可手動建置 MsQuic 並將其與 System.Net.Quic
搭配使用。 不過,這些案例不屬於我們測試的環境,而且可能會發生非預期問題。
平台相依性
下列各節描述 .NET 中 QUIC 的平台相依性。
Windows
- Windows 11、Windows Server 2022 或更新版本。 (舊版 Windows 沒有支援 QUIC 所需的密碼編譯 API。)
在 Windows 上,msquic.dll 會作為 .NET 執行階段的一部分發佈,因此不需要進行其他步驟即可安裝。
Linux
注意
.NET 7 只與 2.2+ 版的 libmsquic 相容。
Linux 上需要 libmsquic
套件。 此套件發行於Microsoft官方Linux套件存放庫中, https://packages.microsoft.com 而且也會在某些官方存放庫中提供,例如 Alpine Packages - libmsquic。
libmsquic
從Microsoft的官方Linux套件存放庫安裝
您必須先將此存放庫新增至套件管理員,才能安裝套件。 如需詳細資訊,請參閱適用於 Microsoft 產品的 Linux 軟體存放庫。
警告
當您的散發套件存放庫提供 .NET 和其他Microsoft套件時,新增Microsoft套件存放庫可能與散發套件的存放庫衝突。 若要避免或疑難排解套件混用,請檢閱針對 Linux 上遺失檔案的相關 .NET 錯誤進行疑難排解。
範例
以下是使用套件管理員安裝 libmsquic
的一些範例:
APT
sudo apt-get install libmsquic
APK
sudo apk add libmsquic
DNF
sudo dnf install libmsquic
zypper
sudo zypper install libmsquic
YUM
sudo yum install libmsquic
libmsquic
從散發套件存放庫安裝
libmsquic
您也可以從散發套件存放庫安裝,但目前僅適用於 Alpine
。
範例
以下是使用套件管理員安裝 libmsquic
的一些範例:
- Alpine 3.21 和更新版本
apk add libmsquic
- Alpine 3.20 和更舊版本
# Get libmsquic from community repository edge branch.
apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ libmsquic
libmsquic 的相依性
下列所有相依性都會在 libmsquic
套件資訊清單中陳述,並由套件管理員自動安裝:
OpenSSL 3+ 或 1.1 - 取決於散發套件版本的預設 OpenSSL 版本,例如 OpenSSL 3 用於 Ubuntu 22 和 OpenSSL 1.1 用於 Ubuntu 20。
libnuma1
macOS
MACOS 上現在部分支援 QUIC,但有一些限制的非標準 Homebrew 套件管理員。 您可以使用 libmsquic
Homebrew 搭配下列命令在 macOS 上安裝 :
brew install libmsquic
若要執行使用 libmsquic
的 .NET 應用程式,您必須先設定環境變數,再執行它。 這可確保應用程式可以在運行時間動態載入期間找到連結 libmsquic
庫。 您可以在主要命令之前新增下列命令,以執行這項操作:
DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib dotnet run
或
DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib ./binaryname
或者,您可以使用下列項目來設定環境變數:
export DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib
然後執行您的主要命令:
./binaryname
API 概觀
System.Net.Quic 會帶入三個主要類別,允許使用 QUIC 通訊協定:
- QuicListener - 伺服器端類別,用於接受連入連線。
- QuicConnection - QUIC 連線,對應至 RFC 9000 第 5 節。
- QuicStream - QUIC 資料流,對應至 RFC 9000 第 2 節。
但在使用這些類別之前,您的程式碼應檢查目前是否支援 QUIC,因為可能遺失 libmsquic
或可能不支援 TLS 1.3。 為此,QuicListener
和 QuicConnection
會公開靜態屬性 IsSupported
:
if (QuicListener.IsSupported)
{
// Use QuicListener
}
else
{
// Fallback/Error
}
if (QuicConnection.IsSupported)
{
// Use QuicConnection
}
else
{
// Fallback/Error
}
這些屬性將會報告相同的值,但未來可能會變更。 建議針對伺服器案例檢查 IsSupported,針對用戶端案例檢查 IsSupported。
QuicListener
QuicListener 表示伺服器端類別,其會接受來自用戶端的連入連線。 接聽程式會以靜態方法 ListenAsync(QuicListenerOptions, CancellationToken) 進行建構並啟動。 方法會接受 QuicListenerOptions 類別的執行個體,其中包啟動接聽程式和接受連入連線所需的所有設定。 之後,接聽程式已準備好透過 AcceptConnectionAsync(CancellationToken) 發出連線。 此方法傳回的連線一律為完全連線,這可表示 TLS 交握已完成且連性已準備好可供使用。 最後,若要停止接聽和釋放所有資源,則必須呼叫 DisposeAsync()。
請考慮下列 QuicListener
範例程式碼:
using System.Net.Quic;
// First, check if QUIC is supported.
if (!QuicListener.IsSupported)
{
Console.WriteLine("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.");
return;
}
// Share configuration for each incoming connection.
// This represents the minimal configuration necessary.
var serverConnectionOptions = new QuicServerConnectionOptions
{
// Used to abort stream if it's not properly closed by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultStreamErrorCode = 0x0A, // Protocol-dependent error code.
// Used to close the connection if it's not done by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultCloseErrorCode = 0x0B, // Protocol-dependent error code.
// Same options as for server side SslStream.
ServerAuthenticationOptions = new SslServerAuthenticationOptions
{
// Specify the application protocols that the server supports. This list must be a subset of the protocols specified in QuicListenerOptions.ApplicationProtocols.
ApplicationProtocols = [new SslApplicationProtocol("protocol-name")],
// Server certificate, it can also be provided via ServerCertificateContext or ServerCertificateSelectionCallback.
ServerCertificate = serverCertificate
}
};
// Initialize, configure the listener and start listening.
var listener = await QuicListener.ListenAsync(new QuicListenerOptions
{
// Define the endpoint on which the server will listen for incoming connections. The port number 0 can be replaced with any valid port number as needed.
ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0),
// List of all supported application protocols by this listener.
ApplicationProtocols = [new SslApplicationProtocol("protocol-name")],
// Callback to provide options for the incoming connections, it gets called once per each connection.
ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(serverConnectionOptions)
});
// Accept and process the connections.
while (isRunning)
{
// Accept will propagate any exceptions that occurred during the connection establishment,
// including exceptions thrown from ConnectionOptionsCallback, caused by invalid QuicServerConnectionOptions or TLS handshake failures.
var connection = await listener.AcceptConnectionAsync();
// Process the connection...
}
// When finished, dispose the listener.
await listener.DisposeAsync();
有關 QuicListener
設計的更多資訊,請參閱 API 提案。
QuicConnection
QuicConnection 是用於伺服器端和用戶端 QUIC 連線的類別。 接聽程式會在內部建立伺服器端連線並透過 AcceptConnectionAsync(CancellationToken) 發出連線。 用戶端連線必須開啟並連線至伺服器。 如同接聽程式,有一個靜態方法 ConnectAsync(QuicClientConnectionOptions, CancellationToken) 可具現化和進行連線。 該靜態方法接受 QuicClientConnectionOptions 執行個體 (QuicServerConnectionOptions 的相似類別)。 之後,使用連線在用戶端和伺服器之間並無不同。 可開啟傳出資料流並接受傳入資料流。 也提供包含連線資訊的屬性,例如 LocalEndPoint、RemoteEndPoint 或 RemoteCertificate。
當完成使用連線時,必須關閉並進行處置。 QUIC 通訊協定會授權使用應用程式層程式碼立即關閉,請參閱 RFC 9000 第 10.2 節。 為此,您可使用應用程式層程式碼呼叫 CloseAsync(Int64, CancellationToken),否則 DisposeAsync() 將會使用 DefaultCloseErrorCode 中提供的程式碼。 不論何種方式,必須在結束使用連線時呼叫 DisposeAsync(),才能完整釋放所有相關的資源。
請考慮下列 QuicConnection
範例程式碼:
using System.Net.Quic;
// First, check if QUIC is supported.
if (!QuicConnection.IsSupported)
{
Console.WriteLine("QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.");
return;
}
// This represents the minimal configuration necessary to open a connection.
var clientConnectionOptions = new QuicClientConnectionOptions
{
// End point of the server to connect to.
RemoteEndPoint = listener.LocalEndPoint,
// Used to abort stream if it's not properly closed by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultStreamErrorCode = 0x0A, // Protocol-dependent error code.
// Used to close the connection if it's not done by the user.
// See https://www.rfc-editor.org/rfc/rfc9000#section-20.2
DefaultCloseErrorCode = 0x0B, // Protocol-dependent error code.
// Optionally set limits for inbound streams.
MaxInboundUnidirectionalStreams = 10,
MaxInboundBidirectionalStreams = 100,
// Same options as for client side SslStream.
ClientAuthenticationOptions = new SslClientAuthenticationOptions
{
// List of supported application protocols.
ApplicationProtocols = [new SslApplicationProtocol("protocol-name")],
// The name of the server the client is trying to connect to. Used for server certificate validation.
TargetHost = ""
}
};
// Initialize, configure and connect to the server.
var connection = await QuicConnection.ConnectAsync(clientConnectionOptions);
Console.WriteLine($"Connected {connection.LocalEndPoint} --> {connection.RemoteEndPoint}");
// Open a bidirectional (can both read and write) outbound stream.
// Opening a stream reserves it but does not notify the peer or send any data. If you don't send data, the peer
// won't be informed about the stream, which can cause AcceptInboundStreamAsync() to hang. To avoid this, ensure
// you send data on the stream to properly initiate communication.
var outgoingStream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
// Work with the outgoing stream ...
// To accept any stream on a client connection, at least one of MaxInboundBidirectionalStreams or MaxInboundUnidirectionalStreams of QuicConnectionOptions must be set.
while (isRunning)
{
// Accept an inbound stream.
var incomingStream = await connection.AcceptInboundStreamAsync();
// Work with the incoming stream ...
}
// Close the connection with the custom code.
await connection.CloseAsync(0x0C);
// Dispose the connection.
await connection.DisposeAsync();
有關 QuicConnection
設計的更多資訊,請參閱 API 提案。
QuicStream
QuicStream 是用來在 QUIC 通訊協定中傳送和接收資料的實際類型。 其衍生自一般 Stream 並可如此使用,同時也提供數個 QUIC 通訊協定特有的功能。 首先,QUIC 資料流可為單向或雙向,請參閱 RFC 9000 第 2.1 節。 雙向資料流能夠同時兩個端和用戶端上傳送和接收資料,而單向資料流僅可從起始端寫入並在接受的端上讀取。 每個同儕節點可限制願意接受每個類型的並行資料流數目,請參閱 MaxInboundBidirectionalStreams 和 MaxInboundUnidirectionalStreams。
QUIC 資料流的其他特性是能夠明確在使用資料流中途時關閉寫入端,請參閱含 completeWrites
引數的 CompleteWrites() 或 WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken) 加載。 關閉寫入端可通知同儕節點不會再傳送任何資料,但同儕節點仍可繼續傳送 (在雙向資料流的案例中)。 在用戶端傳送耀球並關閉寫入端以通知伺服器這是要求內容的結尾時的 HTTP 要求/回應交換案例中,這會非常實用。 伺服器仍能夠在之後傳送回應,但知道不會在從用戶端傳送任何資料。 針對錯誤案例,您可中止資料流的寫入和讀取端,請參閱 Abort(QuicAbortDirection, Int64)。
注意
打開串流只會保留它,而不會發送任何資料。 這種方法的設計目的是要避免傳輸幾乎空白的影格,以最佳化網路使用。 由於在實際資料傳送之前,對等端不會收到通知,因此從對等端的角度來看,該串流仍保持非使用中狀態。 如果您不傳送資料,對等端將無法識別該串流,這可能會導致 AcceptInboundStreamAsync()
暫停,因為它在等待有意義的串流。 為確保通訊正常,您需要在打開串流後傳送資料。
下表摘要說明每個資料流類型的個別方法行為 (請注意,用戶端和伺服器可開啟和接受資料流):
方法 | 開啟資料流的同儕節點 | 接受資料流的同儕節點 |
---|---|---|
CanRead |
雙向:true 單向: false |
true |
CanWrite |
true |
雙向:true 單向: false |
ReadAsync |
雙向:讀取資料 單向: InvalidOperationException |
讀取資料 |
WriteAsync |
傳送資料 = > 同儕節點讀取會傳回資料 | 雙向:傳送資料 => 同儕節點讀取會傳回資料 單向: InvalidOperationException |
CompleteWrites |
關閉寫入端 = > 同儕節點讀取會傳回 0 | 雙向:關閉寫入端 => 同儕節點讀取會傳回 0 單向:no-op |
Abort(QuicAbortDirection.Read) |
雙向:STOP_SENDING => 同儕節點寫入會擲回 QuicException(QuicError.OperationAborted) 單向:no-op |
STOP_SENDING => 同儕節點寫入會擲回 QuicException(QuicError.OperationAborted) |
Abort(QuicAbortDirection.Write) |
RESET_STREAM => 同儕節點讀取會擲回 QuicException(QuicError.OperationAborted) |
雙向:RESET_STREAM => 同儕節點讀取會擲回 QuicException(QuicError.OperationAborted) 單向:no-op |
在這些方式上,QuicStream
提供兩個特殊化屬性,在關閉資料流的讀取或寫入端時收到通知:ReadsClosed 和 WritesClosed。 這兩者都會傳回 Task
並完成關閉其對應端,無論為成功或中止,在兩個案例中 Task
都會包含適當的例外狀況。 當使用者程式碼需要知道關閉資料流端時是否未對 ReadAsync
或 WriteAsync
發出呼叫時,這些屬性會非常實用。
最後,當使用資料流完成時,必須使用 DisposeAsync() 進行處置。 處置將確保關閉讀取和/或寫入端 (取決於資料流類型)。 如果未正確讀取至資料流的結尾,處置將發出 Abort(QuicAbortDirection.Read)
的對應項。 然而,如果未關閉資料流寫入端,則會正常關閉,如同使用 CompleteWrites
一樣。 此差異的原因是確保使用一般 Stream
行為的案例會如期運作且進而成功。 請考慮下列範例:
// Work done with all different types of streams.
async Task WorkWithStreamAsync(Stream stream)
{
// This will dispose the stream at the end of the scope.
await using (stream)
{
// Simple echo, read data and send them back.
byte[] buffer = new byte[1024];
int count = 0;
// The loop stops when read returns 0 bytes as is common for all streams.
while ((count = await stream.ReadAsync(buffer)) > 0)
{
await stream.WriteAsync(buffer.AsMemory(0, count));
}
}
}
// Open a QuicStream and pass to the common method.
var quicStream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
await WorkWithStreamAsync(quicStream);
在用戶端案例中 QuicStream
的範例使用方式:
// Consider connection from the connection example, open a bidirectional stream.
await using var stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional, cancellationToken);
// Send some data.
await stream.WriteAsync(data, cancellationToken);
await stream.WriteAsync(data, cancellationToken);
// End the writing-side together with the last data.
await stream.WriteAsync(data, completeWrites: true, cancellationToken);
// Or separately.
stream.CompleteWrites();
// Read data until the end of stream.
while (await stream.ReadAsync(buffer, cancellationToken) > 0)
{
// Handle buffer data...
}
// DisposeAsync called by await using at the top.
在伺服器案例中 QuicStream
的範例使用方式:
// Consider connection from the connection example, accept a stream.
await using var stream = await connection.AcceptInboundStreamAsync(cancellationToken);
if (stream.Type != QuicStreamType.Bidirectional)
{
Console.WriteLine($"Expected bidirectional stream, got {stream.Type}");
return;
}
// Read the data.
while (stream.ReadAsync(buffer, cancellationToken) > 0)
{
// Handle buffer data...
// Client completed the writes, the loop might be exited now without another ReadAsync.
if (stream.ReadsCompleted.IsCompleted)
{
break;
}
}
// Listen for Abort(QuicAbortDirection.Read) from the client.
var writesClosedTask = WritesClosedAsync(stream);
async ValueTask WritesClosedAsync(QuicStream stream)
{
try
{
await stream.WritesClosed;
}
catch (Exception ex)
{
// Handle peer aborting our writing side ...
}
}
// DisposeAsync called by await using at the top.
有關 QuicStream
設計的更多資訊,請參閱 API 提案。