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:
- QuicListener - kiszolgálóoldali osztály a bejövő kapcsolatok elfogadásához.
- QuicConnection - QUIC-kapcsolat, amely megfelel az RFC 9000 5.
- QuicStream - QUIC-stream, amely megfelel az RFC 9000 2.
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 IsSupported
is 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.