Sdílet prostřednictvím


Protokol QUIC

QUIC je protokol síťové přenosové vrstvy standardizovaný v RFC 9000. Používá protokol UDP jako základní protokol a je ze své podstaty zabezpečený, protože vyžaduje použití protokolu TLS 1.3. Další informace naleznete v dokumentu RFC 9001. Dalším zajímavým rozdílem od dobře známých přenosových protokolů, jako je TCP a UDP, je to, že má stream multiplexing integrovaný v přenosové vrstvě. To umožňuje mít více souběžných nezávislých datových proudů, které vzájemně neovlivňují.

QuIC sám nedefinuje žádnou sémantiku pro vyměňovaná data, protože se jedná o přenosový protokol. Používá se spíše v protokolech aplikační vrstvy, například v HTTP/3 nebo SMB přes QUIC. Lze ho také použít pro jakýkoli vlastní definovaný protokol.

Protokol nabízí mnoho výhod oproti protokolu TCP s protokolem TLS, tady je několik:

  • Rychlejší vytvoření připojení, protože nevyžaduje tolik cest zaokrouhlení, jako je TCP s protokolem TLS.
  • Vyhýbejte se problémům s blokováním hlavy, kdy jeden ztracený paket neblokuje data všech ostatních datových proudů.

Na druhou stranu existují potenciální nevýhody, které je potřeba vzít v úvahu při použití QUIC. Jako novější protokol je jeho přijetí stále rostoucí a omezené. Kromě toho může provoz QUIC blokovat i některé síťové komponenty.

QUIC v .NET

Implementace QUIC byla zavedena v .NET 5 jako System.Net.Quic knihovna. Až do .NET 7 však knihovna byla přísně interní a sloužila pouze jako implementace HTTP/3. S .NET 7 byla knihovna zpřístupněna veřejnosti, čímž se zpřístupnilo jeho rozhraní API.

Poznámka:

V .NET 7.0 a 8.0 se rozhraní API publikovala jako funkce preview. Počínaje rozhraním .NET 9 se tato rozhraní API už nepovažují za funkce ve verzi Preview a považují se za stabilní.

Z hlediska System.Net.Quic implementace závisí na MsQuic, nativní implementaci protokolu QUIC. V důsledku toho System.Net.Quic se podpora platformy a závislosti dědí z MsQuic a dokumentují se v části závislosti platformy. Stručně řečeno, knihovna MsQuic se dodává jako součást .NET pro Windows. V případě Linuxu ale musíte ručně nainstalovat libmsquic přes odpovídajícího správce balíčků. Pro ostatní platformy je stále možné sestavit MsQuic ručně, ať už proti SChannel nebo OpenSSL, a použít ho s System.Net.Quic. Tyto scénáře ale nejsou součástí naší testovací matice a neočekávaných problémů mohou nastat.

Závislosti platformy

Následující části popisují závislosti platformy pro QUIC v .NET.

Windows

  • Windows 11, Windows Server 2022 nebo novější. (Ve starších verzích Windows chybí kryptografická rozhraní API požadovaná pro podporu QUIC.)

Ve Windows se msquic.dll distribuuje jako součást modulu runtime .NET a k jeho instalaci nejsou potřeba žádné další kroky.

Linux

Poznámka:

.NET 7+ je kompatibilní pouze s verzemi 2.2+ knihovny libmsquic.

Balíček libmsquic se vyžaduje v Linuxu. Tento balíček je publikovaný v oficiálním úložišti https://packages.microsoft.com balíčků Linux od Microsoftu a je k dispozici také v některých oficiálních úložištích, jako je Alpine Packages – libmsquic.

libmsquic Instalace z oficiálního úložiště balíčků Pro Linux od Microsoftu

Před instalací balíčku musíte toto úložiště přidat do správce balíčků. Další informace naleznete v tématu Linux Software Repository for Microsoft Products.

Upozornění

Přidání úložiště balíčků Microsoftu může kolidovat s úložištěm vaší distribuce, když úložiště vaší distribuce poskytuje .NET a další balíčky Microsoftu. Pokud se chcete vyhnout kombinaci balíčků nebo řešit potíže, přečtěte si téma Řešení chyb .NET souvisejících s chybějícími soubory v Linuxu.

Příklady

Tady je několik příkladů použití správce balíčků k instalaci libmsquic:

  • VÝSTIŽNÝ

    sudo apt-get install libmsquic 
    
  • APK

    sudo apk add libmsquic
    
  • DNF

    sudo dnf install libmsquic
    
  • zypper

    sudo zypper install libmsquic
    
  • MŇAM

    sudo yum install libmsquic
    

libmsquic Instalace z úložiště distribučního balíčku

libmsquic Instalace z úložiště distribučních balíčků je také možná, ale v současné době je k dispozici pouze pro Alpine.

Příklady

Tady je několik příkladů použití správce balíčků k instalaci libmsquic:

  • Alpine 3.21 a novější
apk add libmsquic
  • Alpine 3.20 a starší
# Get libmsquic from community repository edge branch.
apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ libmsquic
Závislosti libmsquic

V manifestu libmsquic balíčku jsou uvedeny všechny následující závislosti a správce balíčků je automaticky nainstaluje:

  • OpenSSL 3 nebo 1.1 – závisí na výchozí verzi OpenSSL pro distribuční verzi, například OpenSSL 3 pro Ubuntu 22 a OpenSSL 1.1 pro Ubuntu 20.

  • libnuma1

macOS

QuIC je teď v systému macOS částečně podporovaný prostřednictvím nestandardního správce balíčků Homebrew s určitými omezeními. V systému macOS můžete nainstalovat libmsquic homebrew pomocí následujícího příkazu:

brew install libmsquic

Pokud chcete spustit aplikaci .NET, která používá libmsquic, musíte před spuštěním nastavit proměnnou prostředí. Tím zajistíte, že aplikace najde knihovnu během dynamického libmsquic načítání za běhu. Můžete to udělat tak, že před hlavní příkaz přidáte následující příkaz:

DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib dotnet run

nebo

DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib ./binaryname

Proměnnou prostředí můžete také nastavit pomocí následujících možností:

export DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib

a pak spusťte hlavní příkaz:

./binaryname

Přehled rozhraní API

System.Net.Quic přináší tři hlavní třídy, které umožňují použití protokolu QUIC:

Než ale začnete tyto třídy používat, měl by váš kód zkontrolovat, jestli je quIC aktuálně podporovaný, protože libmsquic možná chybí, nebo se nemusí podporovat protokol TLS 1.3. V takovém případě QuicListener a QuicConnection zveřejnění statické vlastnosti IsSupported:

if (QuicListener.IsSupported)
{
    // Use QuicListener
}
else
{
    // Fallback/Error
}

if (QuicConnection.IsSupported)
{
    // Use QuicConnection
}
else
{
    // Fallback/Error
}

Tyto vlastnosti budou hlásit stejnou hodnotu, ale v budoucnu se to může změnit. Doporučujeme zkontrolovat IsSupported scénáře serveru a IsSupported klienty.

QuicListener

QuicListener představuje třídu na straně serveru, která přijímá příchozí připojení z klientů. Naslouchací proces je vytvořen a spuštěn se statickou metodou ListenAsync(QuicListenerOptions, CancellationToken). Metoda přijímá instanci třídy se všemi nastaveními potřebnými ke spuštění naslouchacího QuicListenerOptions procesu a přijímání příchozích připojení. Potom je naslouchací proces připraven k předání připojení přes AcceptConnectionAsync(CancellationToken). Připojení vrácená touto metodou jsou vždy plně připojená, což znamená, že je dokončeno handshake protokolu TLS a připojení je připravené k použití. Nakonec je potřeba volat, aby se přestalo naslouchat a vydávat všechny prostředky DisposeAsync() .

Podívejte se na následující QuicListener ukázkový kód:

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();

Další informace o tom, jak QuicListener byla navržena, najdete v návrhu rozhraní API.

QuicConnection

QuicConnection je třída používaná pro připojení QUIC na straně serveru i klienta. Připojení na straně serveru jsou vytvořena interně naslouchacím procesem a předány prostřednictvím AcceptConnectionAsync(CancellationToken). Připojení na straně klienta musí být otevřena a připojena k serveru. Stejně jako u naslouchacího procesu existuje statická metoda ConnectAsync(QuicClientConnectionOptions, CancellationToken) , která vytvoří instanci a připojí připojení. Přijímá instanci QuicClientConnectionOptions, analogické třídy QuicServerConnectionOptions. Potom se práce s připojením mezi klientem a serverem neliší. Může otevírat odchozí datové proudy a přijímat příchozí datové proudy. Poskytuje také vlastnosti s informacemi o připojení, jako LocalEndPointje , RemoteEndPointnebo RemoteCertificate.

Po dokončení práce s připojením je potřeba ho zavřít a odstranit. Protokol QUIC vyžaduje použití kódu aplikační vrstvy k okamžitému uzavření, viz část 10.2 DOKUMENTU RFC 9000. V takovém případě CloseAsync(Int64, CancellationToken) lze kód aplikační vrstvy volat nebo pokud ne, DisposeAsync() použije kód uvedený v DefaultCloseErrorCode. V obou směrech musí být volána na konci práce s připojením, DisposeAsync() aby se plně uvolnily všechny přidružené prostředky.

Podívejte se na následující QuicConnection ukázkový kód:

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();

Další informace o tom, jak QuicConnection byla navržena, najdete v návrhu rozhraní API.

QuicStream

QuicStream je skutečný typ, který se používá k odesílání a přijímání dat v protokolu QUIC. Je odvozena od běžného Stream a může být použita jako taková, ale nabízí také několik funkcí, které jsou specifické pro protokol QUIC. Nejprve může být datový proud QUIC jednosměrný nebo obousměrný, viz část 2.1 DOKUMENTU RFC 9000. Obousměrný datový proud je schopen odesílat a přijímat data na obou stranách, zatímco jednosměrný datový proud může zapisovat pouze ze strany iniciace a číst na přijímající. Každý partnerský vztah může omezit počet souběžných datových proudů každého typu, který je ochotný přijmout, zobrazit MaxInboundBidirectionalStreams a MaxInboundUnidirectionalStreams.

Další specifika datového proudu QUIC je schopnost explicitně zavřít stranu zápisu uprostřed práce s datovým proudem, zobrazit CompleteWrites() nebo WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken) přetížit argumentem completeWrites . Zavření strany zápisu informuje peera, že dorazí žádná další data, ale partnerský vztah může pokračovat v odesílání (v případě obousměrného datového proudu). To je užitečné ve scénářích, jako je výměna požadavků HTTP nebo odpovědí, když klient odešle požadavek a zavře stranu zápisu, aby server věděl, že se jedná o konec obsahu požadavku. Server po tom stále dokáže odeslat odpověď, ale ví, že z klienta nebudou docházet žádná další data. A v případě chybných případů je možné přerušit psaní nebo čtení datového proudu, viz Abort(QuicAbortDirection, Int64).

Poznámka:

Otevření datového proudu si ho rezervuje jenom bez odeslání dat. Tento přístup je navržený tak, aby optimalizoval využití sítě tím, že se vyhnul přenosu téměř prázdných snímků. Vzhledem k tomu, že partnerský vztah není upozorněn, dokud se neodesílají skutečná data, zůstává stream neaktivní z pohledu partnerského uzlu. Pokud data neodesíláte, partnerský vztah stream nerozpozná, což může způsobit AcceptInboundStreamAsync() zablokování při čekání na smysluplný datový proud. Pokud chcete zajistit správnou komunikaci, musíte po otevření datového proudu odesílat data.

Chování jednotlivých metod jednotlivých typů datových proudů je shrnuto v následující tabulce (všimněte si, že klient i server mohou otevírat a přijímat streamy):

metoda Stream otevření partnerského uzlu Peer accepting stream
CanRead obousměrný: true
jednosměrné: false
true
CanWrite true obousměrný: true
jednosměrné: false
ReadAsync obousměrný: čte data
jednosměrné: InvalidOperationException
čte data.
WriteAsync odesílá data => peer read vrátí data. obousměrně: odesílá data => peer read vrátí data.
jednosměrné: InvalidOperationException
CompleteWrites zavře na straně zápisu => peer read vrátí hodnotu 0. obousměrný: Zavře se na straně zápisu => peer read vrátí hodnotu 0.
jednosměrný: no-op
Abort(QuicAbortDirection.Read) obousměrný: STOP_SENDING => vyvolá zápis partnerského uzlu QuicException(QuicError.OperationAborted)
jednosměrný: no-op
STOP_SENDING => vyvolá zápis partnerského uzluQuicException(QuicError.OperationAborted)
Abort(QuicAbortDirection.Write) RESET_STREAM => vyvolání čtení partnerského uzluQuicException(QuicError.OperationAborted) obousměrný: RESET_STREAM => vyvolání partnerského čtení QuicException(QuicError.OperationAborted)
jednosměrný: no-op

Nad těmito metodami nabízí dvě specializované vlastnosti, QuicStream které se zobrazí oznámení při každém zavření čtení nebo zápisu datového proudu: ReadsClosed a WritesClosed. Oba vrátí dokončenou Task odpovídající stranu, která se zavře, ať už je úspěšná nebo přerušená, v takovém případě Task bude obsahovat příslušnou výjimku. Tyto vlastnosti jsou užitečné, když uživatelský kód potřebuje vědět o zavření strany datového proudu, aniž by bylo nutné volat ReadAsync nebo WriteAsync.

A konečně, když je práce s datovým proudem hotová, musí být odstraněna s DisposeAsync(). Dispose zajistí, že je uzavřena strana čtení i zápisu v závislosti na typu datového proudu. Pokud stream nebyl správně přečten až do konce, dispose vydá ekvivalent .Abort(QuicAbortDirection.Read) Pokud však není uzavřena strana zápisu streamu, bude elegantně uzavřena, jak by to bylo s CompleteWrites. Důvodem tohoto rozdílu je zajistit, aby se scénáře pracující s běžným Stream chováním chovaly podle očekávání a vedly k úspěšné cestě. Představte si následující příklad:

// 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);

Ukázkové QuicStream použití ve scénáři klienta:

// 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.

A ukázkové QuicStream použití ve scénáři serveru:

// 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.

Další informace o tom, jak QuicStream byla navržena, najdete v návrhu rozhraní API.

Viz také