QUIC-protokoll
QUIC är ett protokoll för nätverkstransportskikt som är standardiserat i RFC 9000. Den använder UDP som ett underliggande protokoll och är i sig säkert eftersom det kräver TLS 1.3-användning. Mer information finns i RFC 9001. En annan intressant skillnad jämfört med välkända transportprotokoll som TCP och UDP är att det har strömmat multipling inbyggt på transportskiktet. Detta gör det möjligt att ha flera, samtidiga, oberoende dataströmmar som inte påverkar varandra.
QUIC definierar inte någon semantik för utväxlade data eftersom det är ett transportprotokoll. Det används snarare i protokoll på programnivå, till exempel i HTTP/3 eller i SMB över QUIC. Det kan också användas för alla anpassade definierade protokoll.
Protokollet erbjuder många fördelar jämfört med TCP med TLS, här är några:
- Snabbare anslutningsetablering eftersom det inte kräver lika många tur- och returresor som TCP med TLS ovanpå.
- Undvikande av head-of-line blockeringsproblem där ett förlorat paket inte blockerar data från alla andra strömmar.
Å andra sidan finns det potentiella nackdelar att tänka på när du använder QUIC. Som ett nyare protokoll växer och begränsas antagandet fortfarande. Dessutom kan QUIC-trafik till och med blockeras av vissa nätverkskomponenter.
QUIC i .NET
QUIC-implementeringen introducerades i .NET 5 som System.Net.Quic
bibliotek. Fram till .NET 7 var biblioteket dock strikt internt och fungerade endast som en implementering av HTTP/3. Med .NET 7 blev biblioteket offentligt och exponerade därmed sina API:er.
Kommentar
I .NET 7.0 och 8.0 publicerades API:erna som förhandsversionsfunktioner. Från och med .NET 9 betraktas dessa API:er inte längre som förhandsversionsfunktioner och anses nu vara stabila.
Ur implementeringsperspektivet System.Net.Quic
beror på MsQuic, den interna implementeringen av QUIC-protokollet. Därför System.Net.Quic
ärvs plattformsstöd och beroenden från MsQuic och dokumenteras i avsnittet Plattformsberoenden . Kort och kort levereras MsQuic-biblioteket som en del av .NET för Windows. Men för Linux måste du installera libmsquic
manuellt via en lämplig pakethanterare. För de andra plattformarna är det fortfarande möjligt att skapa MsQuic manuellt, oavsett om det är mot SChannel eller OpenSSL, och använda det med System.Net.Quic
. Dessa scenarier är dock inte en del av vår testmatris och oförutsedda problem kan uppstå.
Plattformsberoenden
I följande avsnitt beskrivs plattformsberoenden för QUIC i .NET.
Windows
- Windows 11, Windows Server 2022 eller senare. (Tidigare Windows-versioner saknar de kryptografiska API:er som krävs för att stödja QUIC.)
I Windows distribueras msquic.dll som en del av .NET-körningen och inga andra steg krävs för att installera den.
Linux
Kommentar
.NET 7+ är endast kompatibelt med 2,2+ versioner av libmsquic.
Paketet libmsquic
krävs i Linux. Det här paketet publiceras på Microsofts officiella Linux-paketlagringsplats https://packages.microsoft.com och finns även i vissa officiella lagringsplatser, till exempel Alpine Packages - libmsquic.
libmsquic
Installera från Microsofts officiella Linux-paketlagringsplats
Du måste lägga till den här lagringsplatsen i pakethanteraren innan du installerar paketet. Mer information finns i Linux Software Repository for Microsoft Products (Linux Software Repository for Microsoft Products).
Varning
Att lägga till Microsoft-paketlagringsplatsen kan vara i konflikt med distributionens lagringsplats när distributionens lagringsplats tillhandahåller .NET och andra Microsoft-paket. Om du vill undvika eller felsöka paketblandningar läser du Felsöka .NET-fel relaterade till filer som saknas i Linux.
Exempel
Här följer några exempel på hur du använder en pakethanterare för att installera libmsquic
:
PASSANDE
sudo apt-get install libmsquic
APK
sudo apk add libmsquic
DNF
sudo dnf install libmsquic
zypper
sudo zypper install libmsquic
MUMS
sudo yum install libmsquic
libmsquic
Installera från distributionspaketlagringsplatsen
Det går också att libmsquic
installera från distributionspaketlagringsplatsen, men för närvarande är detta endast tillgängligt för Alpine
.
Exempel
Här följer några exempel på hur du använder en pakethanterare för att installera libmsquic
:
- Alpine 3.21 och senare
apk add libmsquic
- Alpine 3.20 och äldre
# Get libmsquic from community repository edge branch.
apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ libmsquic
Beroenden för libmsquic
Alla följande beroenden anges i libmsquic
paketmanifestet och installeras automatiskt av pakethanteraren:
OpenSSL 3+ eller 1.1 – beror på standardversionen av OpenSSL för distributionsversionen, till exempel OpenSSL 3 för Ubuntu 22 och OpenSSL 1.1 för Ubuntu 20.
libnuma1
macOS
QUIC stöds nu delvis på macOS via en homebrew-pakethanterare som inte är standard med vissa begränsningar. Du kan installera libmsquic
på macOS med hjälp av Homebrew med följande kommando:
brew install libmsquic
Om du vill köra ett .NET-program som använder libmsquic
måste du ange miljövariabeln innan du kör den. Detta säkerställer att programmet kan hitta libmsquic
biblioteket under dynamisk inläsning under körning. Du kan göra detta genom att lägga till följande kommando före huvudkommandot:
DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib dotnet run
eller
DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib ./binaryname
Du kan också ange miljövariabeln med:
export DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib
och kör sedan huvudkommandot:
./binaryname
API-översikt
System.Net.Quic innehåller tre huvudklasser som möjliggör användning av QUIC-protokoll:
- QuicListener – klassen på serversidan för att acceptera inkommande anslutningar.
- QuicConnection - QUIC-anslutning, motsvarande RFC 9000 avsnitt 5.
- QuicStream - QUIC-ström, motsvarande RFC 9000 avsnitt 2.
Men innan du använder dessa klasser bör koden kontrollera om QUIC stöds för närvarande, vilket libmsquic
kanske saknas, eller om TLS 1.3 kanske inte stöds. För detta exponerar både QuicListener
och QuicConnection
en statisk egenskap IsSupported
:
if (QuicListener.IsSupported)
{
// Use QuicListener
}
else
{
// Fallback/Error
}
if (QuicConnection.IsSupported)
{
// Use QuicConnection
}
else
{
// Fallback/Error
}
Dessa egenskaper rapporterar samma värde, men det kan ändras i framtiden. Vi rekommenderar att du söker efter IsSupported serverscenarier och IsSupported klientscenarier.
QuicListener
QuicListener representerar en klass på serversidan som accepterar inkommande anslutningar från klienterna. Lyssnaren konstrueras och startas med en statisk metod ListenAsync(QuicListenerOptions, CancellationToken). Metoden accepterar en instans av QuicListenerOptions klassen med alla inställningar som krävs för att starta lyssnaren och acceptera inkommande anslutningar. Efter det är lyssnaren redo att dela ut anslutningar via AcceptConnectionAsync(CancellationToken). Anslutningar som returneras med den här metoden är alltid helt anslutna, vilket innebär att TLS-handskakningen är klar och anslutningen är redo att användas. Slutligen, för att sluta lyssna och släppa alla resurser, DisposeAsync() måste anropas.
Tänk på följande QuicListener
exempelkod:
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();
Mer information om hur har QuicListener
utformats finns i API-förslaget.
QuicConnection
QuicConnection är en klass som används för både QUIC-anslutningar på server- och klientsidan. Anslutningar på serversidan skapas internt av lyssnaren och delas ut via AcceptConnectionAsync(CancellationToken). Anslutningar på klientsidan måste öppnas och anslutas till servern. Precis som med lyssnaren finns det en statisk metod ConnectAsync(QuicClientConnectionOptions, CancellationToken) som instansierar och ansluter anslutningen. Den accepterar en instans av QuicClientConnectionOptions, en motsvarande klass till QuicServerConnectionOptions. Därefter skiljer sig inte arbetet med anslutningen mellan klient och server. Den kan öppna utgående strömmar och acceptera inkommande strömmar. Det ger också egenskaper med information om anslutningen, till exempel LocalEndPoint, RemoteEndPointeller RemoteCertificate.
När arbetet med anslutningen är klart måste det stängas och tas bort. QUIC-protokollmandat med hjälp av en programlagerkod för omedelbar stängning, se RFC 9000 Avsnitt 10.2. För detta CloseAsync(Int64, CancellationToken) , med programlagerkod kan anropas eller om inte, DisposeAsync() kommer att använda koden som anges i DefaultCloseErrorCode. Hur som helst DisposeAsync() måste anropas i slutet av arbetet med anslutningen för att helt frigöra alla associerade resurser.
Tänk på följande QuicConnection
exempelkod:
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();
Mer information om hur har QuicConnection
utformats finns i API-förslaget.
QuicStream
QuicStream är den faktiska typen som används för att skicka och ta emot data i QUIC-protokollet. Det härleds från vanliga Stream och kan användas som sådana, men det erbjuder också flera funktioner som är specifika för QUIC-protokoll. För det första kan en QUIC-ström antingen vara enkelriktad eller dubbelriktad, se RFC 9000 Avsnitt 2.1. En dubbelriktad dataström kan skicka och ta emot data på båda sidor, medan enkelriktad dataström endast kan skriva från den inledande sidan och läsa på den accepterande dataströmmen. Varje peer kan begränsa hur många samtidiga strömmar av varje typ som är villiga att acceptera, se MaxInboundBidirectionalStreams och MaxInboundUnidirectionalStreams.
En annan visshet i QUIC-dataströmmen är möjligheten att uttryckligen stänga skrivsidan mitt i arbetet med strömmen, se CompleteWrites() eller WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken) överbelasta med completeWrites
argument. Stängning av skrivsidan låter peer veta att inga fler data kommer att tas emot, men peer fortfarande kan fortsätta att skicka (i händelse av en dubbelriktad dataström). Detta är användbart i scenarier som HTTP-begäran/svarsutbyte när klienten skickar begäran och stänger skrivsidan för att låta servern veta att detta är slutet på begärandeinnehållet. Servern kan fortfarande skicka svaret efter det, men vet att inga fler data kommer från klienten. Och för felaktiga fall kan antingen skrivnings- eller lässidan av dataströmmen avbrytas, se Abort(QuicAbortDirection, Int64).
Kommentar
Om du öppnar en dataström reserveras den bara utan att några data skickas. Den här metoden är utformad för att optimera nätverksanvändningen genom att undvika överföring av nästan tomma bildrutor. Eftersom peer-filen inte meddelas förrän faktiska data skickas förblir dataströmmen inaktiv från peer-peer-perspektivet. Om du inte skickar data kommer peer-datorn inte att känna igen strömmen, vilket kan orsaka AcceptInboundStreamAsync()
att den hänger sig när den väntar på en meningsfull ström. För att säkerställa korrekt kommunikation måste du skicka data när dataströmmen har öppnats.
Beteendet för de enskilda metoderna för varje dataströmstyp sammanfattas i följande tabell (observera att både klienten och servern kan öppna och acceptera strömmar):
Metod | Peer-öppningsström | Peer-accepterande ström |
---|---|---|
CanRead |
dubbelriktad: true enkelriktad: false |
true |
CanWrite |
true |
dubbelriktad: true enkelriktad: false |
ReadAsync |
dubbelriktad: läser data enkelriktad: InvalidOperationException |
läser data |
WriteAsync |
skickar data => peer-läsning returnerar data | dubbelriktad: skickar data => peer-läsning returnerar data enkelriktad: InvalidOperationException |
CompleteWrites |
stänger skrivsidan => peer-läsning returnerar 0 | dubbelriktad: stänger skrivsidan => peer-läsning returnerar 0 enkelriktad: no-op |
Abort(QuicAbortDirection.Read) |
dubbelriktad: STOP_SENDING => peer-skrivkast QuicException(QuicError.OperationAborted) enkelriktad: no-op |
STOP_SENDING => peer-skrivkastQuicException(QuicError.OperationAborted) |
Abort(QuicAbortDirection.Write) |
RESET_STREAM => peer read throwsQuicException(QuicError.OperationAborted) |
dubbelriktad: RESET_STREAM => peer-läskast QuicException(QuicError.OperationAborted) enkelriktad: no-op |
Utöver dessa metoder QuicStream
erbjuder två specialiserade egenskaper för att få aviseringar när antingen läsnings- eller skrivsidan av strömmen har stängts: ReadsClosed och WritesClosed. Båda returnerar en Task
som slutförs med motsvarande sida som stängs, oavsett om den lyckas eller avbryts, i vilket fall Task
kommer att innehålla lämpligt undantag. De här egenskaperna är användbara när användarkoden behöver veta om att strömsidan stängs utan att utfärda anrop till ReadAsync
eller WriteAsync
.
När arbetet med dataströmmen är klart måste det slutligen tas bort med DisposeAsync(). Slängningen ser till att både läs- och/eller skrivsidan - beroende på strömtyp - är stängd. Om dataströmmen inte har lästs korrekt förrän i slutet utfärdas en motsvarighet till Abort(QuicAbortDirection.Read)
. Men om strömskrivningssidan inte har stängts stängs den korrekt som den skulle vara med CompleteWrites
. Anledningen till den här skillnaden är att se till att scenarier som arbetar med ett vanligt Stream
beteende fungerar som förväntat och leder till en lyckad sökväg. Ta följande som exempel:
// 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);
Exempelanvändningen för QuicStream
i klientscenariot:
// 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.
Och exempelanvändningen i QuicStream
serverscenariot:
// 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.
Mer information om hur har QuicStream
utformats finns i API-förslaget.