Protocolo QUIC
O QUIC é um protocolo de camada de transporte de rede padronizado em RFC 9000. Ele usa UDP como um protocolo subjacente e é inerentemente seguro, pois exige o uso de TLS 1.3. Para saber mais, confira RFC 9001. Outra diferença interessante entre ele e os protocolos de transporte conhecidos, como TCP e UDP, é que ele conta com multiplexação de fluxo integrada na camada de transporte. Isso permite ter diversos fluxos de dados simultâneos e independentes que não afetam uns aos outros.
O próprio QUIC não define nenhuma semântica para os dados trocados, pois é um protocolo de transporte. Ele é bastante usado em protocolos de camada de aplicativo, por exemplo, em HTTP/3 ou SMB sobre QUIC. Também pode ser usado para qualquer protocolo definido de maneira personalizada.
O protocolo oferece muitas vantagens com relação ao TCP com TLS, como as seguintes:
- Estabelecimento de conexão mais rápido, pois não requer tantas viagens de ida e volta quanto TCP com TLS.
- Evita o problema de bloqueio HOL (bloqueio da fila de recebimento), em que um pacote perdido não bloqueia os dados de todos os outros fluxos.
Por outro lado, há possíveis desvantagens a serem consideradas ao usar QUIC. Por ser um protocolo mais recente, sua adoção ainda não é abrangente e está em ampliação. Além disso, o tráfego QUIC pode até mesmo ser bloqueado por alguns componentes de rede.
QUIC no .NET
A implementação do QUIC foi introduzida no .NET 5 como a biblioteca System.Net.Quic
. No entanto, até o .NET 7.0, a biblioteca era estritamente interna e servia somente como uma implementação de HTTP/3. Com o .NET 7, ela se tornou pública, expondo as APIs.
Observação
No .NET 7.0, as APIs são publicadas como versões prévias de recursos.
Do ponto de vista da implementação, o System.Net.Quic
depende do MsQuic, a implementação nativa do protocolo QUIC. Como resultado, o suporte e as dependências da plataforma System.Net.Quic
são herdados da MsQuic e documentados na seção Dependências da plataforma. Resumindo, a biblioteca MsQuic é fornecida como parte do .NET para Windows. Porém, no caso do Linux, é preciso instalar libmsquic
manualmente por meio de um gerenciador de pacotes apropriado. Para as outras plataformas, ainda é possível compilar a MsQuic manualmente, seja no SChannel ou no OpenSSL, e usá-la com System.Net.Quic
. Porém, esses cenários não fazem parte da matriz de testes e imprevistos podem ocorrer.
Dependências de plataforma
As seções a seguir descrevem as dependências da plataforma para QUIC no .NET.
Windows
- Windows 11, Windows Server 2022 ou posterior. (Versões anteriores do Windows não têm as APIs criptográficas necessárias para dar suporte ao QUIC.)
No Windows, msquic.dll é distribuído como parte do runtime do .NET e nenhuma outra etapa é necessária para instalá-lo.
Linux
Observação
O .NET 7+ só é compatível com versões 2.2+ do libmsquic.
O pacote libmsquic
é necessário no Linux. Esse pacote é publicado no repositório oficial de pacotes Linux da Microsoft, https://packages.microsoft.com. Você deve adicionar esse repositório ao gerenciador de pacotes antes de instalar o pacote. Para obter mais informações, consulte Repositório de Software do Linux para Produtos da Microsoft.
Cuidado
Adicionar o repositório de pacotes da Microsoft pode entrar em conflito com o repositório da distribuição quando o repositório da distribuição fornece pacotes .NET e outros pacotes da Microsoft. Para evitar ou solucionar problemas de combinações de pacotes, examine Solucionar problemas de erros do .NET relacionados a arquivos ausentes no Linux.
Exemplos
Aqui estão alguns exemplos de como usar um gerenciador de pacotes para instalar 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
Dependências do libmsquic
Todas as seguintes dependências são declaradas no manifesto do pacote libmsquic
e instaladas automaticamente pelo gerenciador de pacotes:
OpenSSL 3 e posterior ou 1.1: depende da versão padrão do OpenSSL para a versão de distribuição, por exemplo, OpenSSL 3 para Ubuntu 22 e OpenSSL 1.1 para Ubuntu 20.
libnuma 1
macOS
No momento, não há suporte para o QUIC no macOS, mas ele pode estar disponível em uma versão futura.
Visão geral da API
O System.Net.Quic oferece três grandes classes que possibilitam o uso do protocolo QUIC:
- QuicListener – classe do servidor para aceitar conexões de entrada.
- QuicConnection – conexão QUIC, correspondente à seção 5 do RFC 9000.
- QuicStream – fluxo QUIC, correspondente à seção 2 do RFC 9000.
No entanto, antes de usar essas classes, o código deve verificar se o QUIC é compatível no momento, pois libmsquic
pode estar ausente ou o TLS 1.3 pode não ser compatível. Para isso, tanto QuicListener
quanto QuicConnection
expõem uma propriedade estática IsSupported
:
if (QuicListener.IsSupported)
{
// Use QuicListener
}
else
{
// Fallback/Error
}
if (QuicConnection.IsSupported)
{
// Use QuicConnection
}
else
{
// Fallback/Error
}
Essas propriedades relatarão o mesmo valor, mas isso pode mudar no futuro. Recomenda-se verificar IsSupported para os cenários de servidor e IsSupported para os de cliente.
QuicListener
QuicListener representa uma classe do servidor que aceita conexões de entrada dos clientes. O ouvinte é criado e iniciado com um método estático ListenAsync(QuicListenerOptions, CancellationToken). O método aceita uma instância da classe QuicListenerOptions com todas as configurações necessárias para iniciar o ouvinte e aceitar conexões de entrada. Depois disso, o ouvinte está pronto para realizar conexões via AcceptConnectionAsync(CancellationToken). As conexões retornadas por este método são sempre totalmente estabelecidas, o que significa que o handshake TLS está finalizado e a conexão está pronta para ser utilizada. Finalmente, para interromper a escuta e liberar todos os recursos, chame DisposeAsync().
Considere o seguinte código de exemplo 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();
Para saber como o QuicListener
foi projetado, confira a proposta da API.
QuicConnection
QuicConnection é uma classe usada para conexões QUIC do servidor e do cliente. As conexões do servidor são criadas internamente pelo ouvinte e realizadas via AcceptConnectionAsync(CancellationToken). As conexões do cliente devem ser abertas e estabelecidas com o servidor. Assim como o ouvinte, há um método estático ConnectAsync(QuicClientConnectionOptions, CancellationToken) que instancia e estabelece a conexão. Ele aceita uma instância de QuicClientConnectionOptions, uma classe análoga a QuicServerConnectionOptions. Depois disso, o trabalho com a conexão não difere entre cliente e servidor. Ele pode abrir fluxos de saída e aceitar os de entrada. Também fornece propriedades com informações sobre a conexão, como LocalEndPoint, RemoteEndPoint ou RemoteCertificate.
Quando o trabalho com a conexão é concluído, ele deve ser encerrado e descartado. O protocolo QUIC exige o uso de um código de camada de aplicativo para o encerramento imediato. Para saber mais, confira a seção 10.2 do RFC 9000. Para isso, é possível chamar CloseAsync(Int64, CancellationToken) com o código da camada de aplicativo. Caso contrário, o DisposeAsync() usará o código fornecido em DefaultCloseErrorCode. De qualquer forma, o DisposeAsync() deve ser chamado ao final do trabalho com a conexão para liberar totalmente todos os recursos associados.
Considere o seguinte código de exemplo 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();
ou saiba como o QuicConnection
foi projetado. Para isso, confira a proposta da API.
QuicStream
QuicStream é o tipo real usado para enviar e receber dados no protocolo QUIC. Ele deriva do Stream comum e pode ser usado como tal, mas também oferece diversos recursos específicos do protocolo QUIC. Em primeiro lugar, um fluxo QUIC pode ser unidirecional ou bidirecional. Para saber mais, confira a seção 2.1 do RFC 9000. Um fluxo bidirecional é capaz de enviar e receber dados em ambos os lados, enquanto o fluxo unidirecional só pode gravar no lado inicial e ler no lado receptor. Cada par pode limitar quantos fluxos simultâneos de cada tipo está disposto a aceitar. Para saber mais, confira MaxInboundBidirectionalStreams e MaxInboundUnidirectionalStreams.
Outra particularidade do fluxo QUIC é a capacidade de encerrar explicitamente o lado da gravação no meio do trabalho com o fluxo. Para saber mais, confira a sobrecarga de CompleteWrites() ou WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken) com o argumento completeWrites
. O encerramento do lado da gravação permite que o par saiba que não chegarão mais dados, mas ele ainda pode continuar realizando envios (no caso de um fluxo bidirecional). Isso é útil em cenários como trocas de solicitação/resposta HTTP quando o cliente envia a solicitação e encerra o lado de gravação para informar ao servidor que este é o fim do conteúdo da solicitação. O servidor ainda pode enviar a resposta depois disso, mas sabe que nenhum outro dado chegará do cliente. Nos casos errôneos, o lado de gravação ou de leitura do fluxo pode ser anulado. Para saber mais, confira Abort(QuicAbortDirection, Int64). O comportamento dos métodos individuais para cada tipo de fluxo é resumido na tabela a seguir (observe que tanto o cliente quanto o servidor podem abrir e aceitar fluxos):
Método | Fluxo de abertura de par | Fluxo de aceitação de par |
---|---|---|
CanRead |
bidirecional: true unidirecional: false |
true |
CanWrite |
true |
bidirecional: true unidirecional: false |
ReadAsync |
bidirecional: lê os dados unidirecional: InvalidOperationException |
lê os dados |
WriteAsync |
envia os dados = a leitura do par > retorna os dados | bidirecional: envia os dados = a leitura do par > retorna os dados unidirecional: InvalidOperationException |
CompleteWrites |
encerra o lado da gravação = a leitura do par > retorna 0 | bidirecional: encerra o lado da gravação = a leitura do par > retorna 0 unidirecional: sem operação |
Abort(QuicAbortDirection.Read) |
bidirecional: STOP_SENDING = a gravação do par > gera QuicException(QuicError.OperationAborted) unidirecional: sem operação |
STOP_SENDING = a gravação do par > gera QuicException(QuicError.OperationAborted) |
Abort(QuicAbortDirection.Write) |
RESET_STREAM = a leitura do par > gera QuicException(QuicError.OperationAborted) |
bidirecional: RESET_STREAM = a leitura do par > gera QuicException(QuicError.OperationAborted) unidirecional: sem operação |
Além desses métodos, o QuicStream
oferece duas propriedades especializadas para que você seja notificado sempre que o lado de leitura ou de gravação do fluxo for encerrado: ReadsClosed e WritesClosed. Ambas retornam um Task
que é concluído com o encerramento do lado correspondente, seja a operação bem-sucedida ou uma anulação, caso em que o Task
conterá a exceção apropriada. Essas propriedades são úteis quando o código do usuário precisa saber sobre o encerramento do fluxo sem a emissão de uma chamada para ReadAsync
ou WriteAsync
.
Finalmente, quando o trabalho com o fluxo é encerrado, ele precisa ser descartado com DisposeAsync(). O descarte garante que o lado de leitura e/ou de gravação, dependendo do tipo de fluxo, seja encerrado. Se o fluxo não tiver sido lido corretamente até o final, o descarte emitirá um equivalente de Abort(QuicAbortDirection.Read)
. No entanto, se o lado de gravação do fluxo não tiver sido encerrado, isso acontecerá normalmente, como no caso de CompleteWrites
. O motivo dessa diferença é garantir que os cenários que trabalham com um Stream
comum se comportem conforme o esperado e levem a um caminho de sucesso. Considere o exemplo a seguir:
// 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);
O exemplo de uso de QuicStream
no cenário do cliente:
// Consider connection from the connection example, open a bidirectional stream.
await using var stream = await connection.OpenStreamAsync(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.
E o exemplo de uso de QuicStream
no cenário do servidor:
// Consider connection from the connection example, accept a stream.
await using var stream = await connection.AcceptStreamAsync(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.
Para saber como o QuicStream
foi projetado, confira a proposta de API.
Confira também
Comentários
https://aka.ms/ContentUserFeedback.
Em breve: Ao longo de 2024, eliminaremos os problemas do GitHub como o mecanismo de comentários para conteúdo e o substituiremos por um novo sistema de comentários. Para obter mais informações, consulteEnviar e exibir comentários de