Share via


QUIC 通訊協定

QUIC 是 RFC 9000 中標準化的網路傳輸層通訊協定。 其使用 UDP 作為基礎通訊協定,而且本身就很安全,原因是會要求使用 TLS 1.3。 如需詳細資訊,請參閱 RFC 9001。 TCP 與 UDP 等已知傳輸通訊協定的其他特別差異是,其會在傳輸層上內建資料流多工。 這可讓多個並行獨立資料流不會相互影響。

QUIC 本身不會定義交換資料的任何語意,因為其即是傳輸通訊協定。 其更適用於應用程式層通訊協定,例如 HTTP/3QUIC 上的 SMB。 另也可用於任何自訂定義的通訊協定。

通訊協定提供許多搭配使用 TCP 與 TLS 的優點,以下是幾項:

  • 加快連線建立,因為與在其上搭配使用 TCP 與 TLS 不同,該通訊協定不需要多次來回行程。
  • 在遺失一個封包時不會封鎖所有其他資料留的資料時,避免發生隊頭阻塞問題。

另一個方面,您也要考慮使用 QUIC 時的潛在缺點。 作為較新的通訊協定,其採用仍在發展中且有限制。 除此之外,某些網路元件甚至可能封鎖 QUIC 流量。

.NET 中的 QUIC

在 .NET 5 中已導入 QUIC 實作作為 System.Net.Quic 程式庫。 不過,在 .NET 7.0 之前,程式庫在內部有嚴格限制且僅作為 HTTP/3 的實作。 使用 .NET 7 時,程式庫會設為公開,因此會公開其 API。

注意

在 .NET 7.0 中,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 發行。 您必須先將此存放庫新增至套件管理員,才能安裝套件。 如需詳細資訊,請參閱適用於 Microsoft 產品的 Linux 軟體存放庫

警告

當您的散發套件存放庫提供 .NET 和其他 Microsoft 套件時,新增 Microsoft 套件存放庫可能會與散發套件的存放庫衝突。 若要避免或疑難排解套件混用,請檢閱針對 Linux 上遺失檔案的相關 .NET 錯誤進行疑難排解

範例

以下是使用套件管理員安裝 libmsquic 的一些範例:

  • APT

    sudo apt-get libmsquic 
    
  • APK

    sudo apk add libmsquic
    
  • DNF

    sudo dnf install libmsquic
    
  • zypper

    sudo zypper install libmsquic
    
  • YUM

    sudo yum install libmsquic
    
libmsquic 的相依性

下列所有相依性都會在 libmsquic 套件資訊清單中陳述,並由套件管理員自動安裝:

  • OpenSSL 3+ 或 1.1 - 取決於散發套件版本的預設 OpenSSL 版本,例如 OpenSSL 3 用於 Ubuntu 22 和 OpenSSL 1.1 用於 Ubuntu 20

  • libnuma 1

macOS

macOS 目前不支援 QUIC,但未來版本可能支援。

API 概觀

System.Net.Quic 會帶入三個主要類別,允許使用 QUIC 通訊協定:

但在使用這些類別之前,您的程式碼應檢查目前是否支援 QUIC,因為可能遺失 libmsquic 或可能不支援 TLS 1.3。 為此,QuicListenerQuicConnection 會公開靜態屬性 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
    {
        // List of supported application protocols, must be the same or subset of QuicListenerOptions.ApplicationProtocols.
        ApplicationProtocols = new List<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
{
    // Listening endpoint, port 0 means any port.
    ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0),
    // List of all supported application protocols by this listener.
    ApplicationProtocols = new List<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 的相似類別)。 之後,使用連線在用戶端和伺服器之間並無不同。 可開啟傳出資料流並接受傳入資料流。 也提供包含連線資訊的屬性,例如 LocalEndPointRemoteEndPointRemoteCertificate

當完成使用連線時,必須關閉並進行處置。 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 List<SslApplicationProtocol>() { "protocol-name" }
    }
};

// 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.
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 節。 雙向資料流能夠同時兩個端和用戶端上傳送和接收資料,而單向資料流僅可從起始端寫入並在接受的端上讀取。 每個同儕節點可限制願意接受每個類型的並行資料流數目,請參閱 MaxInboundBidirectionalStreamsMaxInboundUnidirectionalStreams

QUIC 資料流的其他特性是能夠明確在使用資料流中途時關閉寫入端,請參閱含 completeWrites 引數的 CompleteWrites()WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken) 加載。 關閉寫入端可通知同儕節點不會再傳送任何資料,但同儕節點仍可繼續傳送 (在雙向資料流的案例中)。 在用戶端傳送耀球並關閉寫入端以通知伺服器這是要求內容的結尾時的 HTTP 要求/回應交換案例中,這會非常實用。 伺服器仍能夠在之後傳送回應,但知道不會在從用戶端傳送任何資料。 針對錯誤案例,您可中止資料流的寫入和讀取端,請參閱 Abort(QuicAbortDirection, Int64)。 下表摘要說明每個資料流類型的個別方法行為 (請注意,用戶端和伺服器可開啟和接受資料流):

方法 開啟資料流的同儕節點 接受資料流的同儕節點
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 提供兩個特殊化屬性,在關閉資料流的讀取或寫入端時收到通知:ReadsClosedWritesClosed。 這兩者都會傳回 Task 並完成關閉其對應端,無論為成功或中止,在兩個案例中 Task 都會包含適當的例外狀況。 當使用者程式碼需要知道關閉資料流端時是否未對 ReadAsyncWriteAsync 發出呼叫時,這些屬性會非常實用。

最後,當使用資料流完成時,必須使用 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, endStream: 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 提案

另請參閱