Megosztás a következőn keresztül:


QUIC protokoll

A QUIC egy RFC 9000-ben szabványosított hálózati átviteli réteg protokollja. Az UDP-t használja alapul szolgáló protokollként, és eleve biztonságos, mivel a TLS 1.3-as használatát teszi kötelezővé. További információ: RFC 9001. Egy másik érdekes különbség a jól ismert átviteli protokolloktól, például a TCP-től és az UDP-től, hogy beépített stream multiplexinggel rendelkezik az átviteli rétegen. Ez lehetővé teszi, hogy több, egyidejű, egymástól független adatfolyam legyen, amelyek nem befolyásolják egymást.

Maga a QUIC nem definiál szemantikát a kicserélt adatokhoz, mivel az átviteli protokoll. Inkább alkalmazásréteg-protokollokban használják, például HTTP/3-ban vagy SMB-ben QUIC-n keresztül. Bármely egyénileg definiált protokollhoz is használható.

A protokoll számos előnyt kínál a TCP-vel és a TLS-sel szemben, íme néhány:

  • Gyorsabb kapcsolatlétrehozás, mivel nem igényel annyi oda-vissza utazást, mint a TCP és a TLS.
  • Elkerülheti a sortöréses blokkolási problémát, ha egy elveszett csomag nem blokkolja az összes többi stream adatait.

Másrészt a QUIC használata esetén potenciális hátrányokat is figyelembe kell venni. Újabb protokollként a bevezetése még mindig növekszik és korlátozott. Ezen kívül a QUIC-forgalmat egyes hálózati összetevők is blokkolhatják.

QUIC a .NET-ben

A QUIC implementáció a .NET 5-ben lett bevezetve kódtárként System.Net.Quic . A .NET 7-ig azonban a kódtár szigorúan belső volt, és csak a HTTP/3 implementációjaként szolgált. A .NET 7-tel a könyvtár nyilvánossá vált, így felfedte api-jait.

Feljegyzés

A .NET 7.0-s és 8.0-s verziójában az API-k előzetes verziójú funkciókként lettek közzétéve. A .NET 9-től kezdve ezek az API-k már nem tekinthetők előzetes verziójú funkcióknak, és most már stabilnak minősülnek.

A megvalósítás szempontjából System.Net.Quic az MsQuic-től, a QUIC protokoll natív implementációjától függ. Ennek eredményeképpen a System.Net.Quic platformtámogatás és a függőségek az MsQuictől öröklődnek, és a platformfüggőségek szakaszban dokumentálva vannak. Röviden: az MsQuic-kódtár a Windowshoz készült .NET részeként lesz szállítva. Linux esetén azonban manuálisan kell telepítenie libmsquic a megfelelő csomagkezelőn keresztül. A többi platform esetében továbbra is lehet manuálisan létrehozni az MsQuic-t, akár SChannel, akár OpenSSL rendszeren, és használhatja a következővel System.Net.Quic: . Ezek a forgatókönyvek azonban nem részei a tesztelési mátrixnak, és előre nem látható problémák léphetnek fel.

Platformfüggőségek

Az alábbi szakaszok a QUIC platformfüggőségeit ismertetik a .NET-ben.

Windows

  • Windows 11, Windows Server 2022 vagy újabb. (A korábbi Windows-verziókban hiányoznak a QUIC támogatásához szükséges titkosítási API-k.)

Windows rendszeren a msquic.dll a .NET-futtatókörnyezet részeként van elosztva, és a telepítéshez nincs szükség további lépésekre.

Linux

Feljegyzés

A .NET 7+ csak a libmsquic 2.2+-os verzióival kompatibilis.

A libmsquic csomag Linux rendszeren szükséges. Ez a csomag a Microsoft hivatalos Linux-csomagtárában van közzétéve, https://packages.microsoft.com és néhány hivatalos adattárban is elérhető, például az Alpine Packagesben – libmsquic.

Telepítés libmsquic a Microsoft hivatalos Linux-csomagtárából

A csomag telepítése előtt fel kell vennie ezt az adattárat a csomagkezelőbe. További információ: Linux Software Repository for Microsoft Products.

Figyelemfelhívás

A Microsoft-csomagtárház hozzáadása ütközhet a disztribúció tárházával, ha a disztribúció adattára .NET- és egyéb Microsoft-csomagokat biztosít. A csomagkeverések elkerülése vagy hibaelhárítása érdekében tekintse át a Linuxon hiányzó fájlokhoz kapcsolódó .NET-hibák hibaelhárítását.

Példák

Íme néhány példa a csomagkezelő telepítésére libmsquic:

  • MEGFELELŐ

    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
    

Telepítés libmsquic a terjesztési csomag adattárából

A terjesztési csomag adattárából való telepítés libmsquic is lehetséges, de ez jelenleg csak a következőhöz Alpineérhető el:

Példák

Íme néhány példa a csomagkezelő telepítésére libmsquic:

  • Alpine 3.21 és újabb verziók
apk add libmsquic
  • Alpine 3.20 és újabb
# Get libmsquic from community repository edge branch.
apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ libmsquic
A libmsquic függőségei

A csomagjegyzék az libmsquic alábbi függőségeket tartalmazza, és a csomagkezelő automatikusan telepíti:

  • OpenSSL 3+ vagy 1.1 – a terjesztési verzió alapértelmezett OpenSSL-verziójától függ, például az OpenSSL 3 az Ubuntu 22-hez és az OpenSSL 1.1 az Ubuntu 20-hoz.

  • libnuma1

macOS

A QUIC jelenleg részben támogatott macOS rendszeren egy nem szabványos Homebrew csomagkezelőn keresztül, bizonyos korlátozásokkal. A következő paranccsal telepítheti libmsquic macOS rendszerre a Homebrew használatával:

brew install libmsquic

A használt libmsquic.NET-alkalmazás futtatásához be kell állítania a környezeti változót a futtatás előtt. Ez biztosítja, hogy az alkalmazás megtalálja a kódtárat a libmsquic futtatókörnyezet dinamikus betöltése során. Ehhez adja hozzá a következő parancsot a fő parancs elé:

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

vagy

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

Másik lehetőségként beállíthatja a környezeti változót a következőkkel:

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

majd futtassa a fő parancsot:

./binaryname

API – Áttekintés

System.Net.Quic három fő osztályt hoz létre, amelyek lehetővé teszik a QUIC protokoll használatát:

Az osztályok használata előtt azonban a kódnak ellenőriznie kell, hogy a QUIC jelenleg támogatott-e, mert libmsquic előfordulhat, hogy hiányzik, vagy a TLS 1.3 nem támogatott. Ehhez statikus tulajdonságot IsSupportedis QuicListener elérhetővé kell QuicConnection tennie:

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

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

Ezek a tulajdonságok ugyanazt az értéket jelentik, de ez a jövőben változhat. Javasoljuk, hogy ellenőrizze IsSupported a kiszolgálói forgatókönyveket és IsSupported az ügyfeleket.

QuicListener

QuicListener egy kiszolgálóoldali osztályt jelöl, amely fogadja az ügyfelek bejövő kapcsolatait. A figyelő statikus módszerrel ListenAsync(QuicListenerOptions, CancellationToken)van létrehozva és elindítva. A metódus elfogadja az osztály egy példányát QuicListenerOptions a figyelő elindításához és a bejövő kapcsolatok elfogadásához szükséges összes beállítással. Ezután a figyelő készen áll a kapcsolatok átadására a .AcceptConnectionAsync(CancellationToken) A metódus által visszaadott kapcsolatok mindig teljes mértékben csatlakoztatva vannak, ami azt jelenti, hogy a TLS kézfogása befejeződött, és a kapcsolat készen áll a használatra. Végül az összes erőforrás DisposeAsync() figyelésének és kiadásának leállításához meg kell hívni.

Vegye figyelembe a következő QuicListener példakódot:

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

A tervezés módjával kapcsolatos további információkért QuicListener tekintse meg az API-javaslatot.

QuicConnection

QuicConnection kiszolgálói és ügyféloldali QUIC-kapcsolatokhoz egyaránt használt osztály. A kiszolgálóoldali kapcsolatokat a figyelő belsőleg hozza létre, és a figyelőn keresztül AcceptConnectionAsync(CancellationToken)adja ki. Az ügyféloldali kapcsolatokat meg kell nyitni, és csatlakozni kell a kiszolgálóhoz. A figyelőhöz hasonlóan létezik egy statikus módszer ConnectAsync(QuicClientConnectionOptions, CancellationToken) , amely példányosítja és összekapcsolja a kapcsolatot. Elfogadja a hasonló osztály egy példányátQuicClientConnectionOptionsQuicServerConnectionOptions. Ezután a kapcsolattal végzett munka nem különbözik az ügyfél és a kiszolgáló között. Képes megnyitni a kimenő streameket, és fogadni a bejövőket. Emellett tulajdonságokat is biztosít a kapcsolattal kapcsolatos információkkal, például LocalEndPoint, RemoteEndPointvagy RemoteCertificate.

Ha a kapcsolattal végzett munka befejeződik, azt le kell zárni és el kell dobni. A QUIC protokoll egy alkalmazásréteg kódjának azonnali bezárását írja elő, lásd az RFC 9000 10.2. szakaszát. CloseAsync(Int64, CancellationToken) Ehhez az alkalmazásréteg kódja meghívható, vagy ha nem, DisposeAsync() akkor a megadott DefaultCloseErrorCodekódot fogja használni. Akárhogy is, DisposeAsync() a kapcsolattal végzett munka végén meg kell hívni az összes társított erőforrás teljes felszabadításához.

Vegye figyelembe a következő QuicConnection példakódot:

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

A tervezés módjával kapcsolatos további információkért QuicConnection tekintse meg az API-javaslatot.

QuicStream

QuicStream az adatok QUIC protokollban való küldéséhez és fogadásához használt tényleges típus. A hagyományostól Stream származik, és így használható, de számos olyan funkciót is kínál, amelyek a QUIC protokollra vonatkoznak. Először is a QUIC-streamek lehetnek egyirányúak vagy kétirányúak, lásd: RFC 9000 2.1. szakasz. A kétirányú stream mindkét oldalon képes adatokat küldeni és fogadni, míg az egyirányú stream csak a kezdeményező oldalról tud írni és olvasni az elfogadó oldalon. Minden társ korlátozhatja, hogy az egyes típusok egyidejű streamje hány hajlandó elfogadni, látni MaxInboundBidirectionalStreams és MaxInboundUnidirectionalStreams.

A QUIC-stream egy másik különlegessége, hogy explicit módon bezárhatja az írási oldalt a streammel végzett munka közepén, láthatja CompleteWrites() vagy WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken) túlterhelheti az argumentumokat completeWrites . Az írási oldal bezárása tudatja a társsal, hogy nem érkezik több adat, de a társ továbbra is küldhet (kétirányú stream esetén). Ez olyan helyzetekben hasznos, mint a HTTP-kérések/válaszok cseréje, amikor az ügyfél elküldi a kérést, és bezárja az írási oldalt, hogy a kiszolgáló tudja, hogy ez a kérés tartalmának vége. A kiszolgáló ezután is képes elküldeni a választ, de tudja, hogy az ügyféltől nem érkeznek további adatok. Hibás esetekben a stream írási vagy olvasási oldala megszakítható, lásd Abort(QuicAbortDirection, Int64).

Feljegyzés

A stream megnyitása csak az adatok elküldése nélkül foglalja le. Ez a megközelítés úgy lett kialakítva, hogy optimalizálja a hálózathasználatot a szinte üres keretek átvitelének elkerülésével. Mivel a rendszer nem értesíti a társt a tényleges adatok elküldéséig, a stream inaktív marad a társ szempontjából. Ha nem küld adatokat, a társ nem fogja felismerni a streamet, ami okozhatja AcceptInboundStreamAsync() , hogy lefagy, miközben egy tartalmas streamre vár. A megfelelő kommunikáció érdekében a stream megnyitása után adatokat kell küldenie.

Az egyes streamtípusok egyes metódusainak viselkedését az alábbi táblázat foglalja össze (vegye figyelembe, hogy mind az ügyfél, mind a kiszolgáló megnyithatja és elfogadhatja a streameket):

Metódus Társ megnyitása stream Társátvevő stream
CanRead kétirányú: true
egyirányú: false
true
CanWrite true kétirányú: true
egyirányú: false
ReadAsync kétirányú: adatokat olvas
egyirányú: InvalidOperationException
adatolvasás
WriteAsync adatküldés => a társolvasás az adatokat adja vissza kétirányú: adatokat küld => a társolvasás az adatokat adja vissza
egyirányú: InvalidOperationException
CompleteWrites írási oldal bezárása => társolvasás eredménye 0 kétirányú: bezárja az írási oldalt => a társ olvasása 0-t ad vissza
egyirányú: no-op
Abort(QuicAbortDirection.Read) kétirányú: STOP_SENDING => társírási dobások QuicException(QuicError.OperationAborted)
egyirányú: no-op
STOP_SENDING => társírási dobásokQuicException(QuicError.OperationAborted)
Abort(QuicAbortDirection.Write) RESET_STREAM => társolvasási dobásokQuicException(QuicError.OperationAborted) kétirányú: RESET_STREAM => társolvasási dobások QuicException(QuicError.OperationAborted)
egyirányú: no-op

Ezen módszerek mellett két speciális tulajdonságot kínál, QuicStream amelyek értesítést kapnak, ha a stream olvasási vagy írási oldalát bezárták: ReadsClosed és WritesClosed. Mindkettő visszaadja Task a megfelelő oldalt lezáró, sikeres vagy megszakított állapotot, amely esetben a Task megfelelő kivételt fogja tartalmazni. Ezek a tulajdonságok akkor hasznosak, ha a felhasználói kódnak tudnia kell arról, hogy a streamoldal bezárul anélkül, hogy hívást kellene kezdeményeznieReadAsync.WriteAsync

Végül, amikor a streampel végzett munka befejezve, azt el kell végezni a következővel DisposeAsync(): . A törlés gondoskodik arról, hogy az olvasási és/vagy írási oldal – a stream típusától függően – zárva legyen. Ha a stream nem lett megfelelően beolvasva a végéig, a megsemmisítés a következőnek megfelelő Abort(QuicAbortDirection.Read)értéket ad ki. Ha azonban a stream írási oldalát még nem zárták be, akkor az elegánsan le lesz zárva, mint a CompleteWrites. Ennek a különbségnek az az oka, hogy a szokásos Stream módon működő forgatókönyvek a várt módon viselkednek, és sikeres úthoz vezetnek. Vegyük a következő példát:

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

Az ügyfélforgatókönyv mintahasználata 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.

És a kiszolgálói forgatókönyv mintahasználata 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.

A tervezés módjával kapcsolatos további információkért QuicStream tekintse meg az API-javaslatot.

Lásd még