Protokol QUIC
QUIC adalah protokol lapisan transportasi jaringan yang distandarkan dalam RFC 9000. Ini menggunakan UDP sebagai protokol yang mendasar dan secara inheren aman karena mengamanatkan penggunaan TLS 1.3. Untuk informasi selengkapnya, lihat RFC 9001. Perbedaan menarik lainnya dari protokol transportasi terkenal seperti TCP dan UDP adalah bahwa ia memiliki streaming multipleks bawaan pada lapisan transportasi. Ini memungkinkan memiliki beberapa aliran data independen bersamaan yang tidak memengaruhi satu sama lain.
QUIC sendiri tidak menentukan semantik apa pun untuk data yang ditukar karena ini adalah protokol transportasi. Ini lebih digunakan dalam protokol lapisan aplikasi, misalnya di HTTP/3 atau di SMB melalui QUIC. Ini juga dapat digunakan untuk protokol yang ditentukan khusus.
Protokol ini menawarkan banyak keuntungan daripada TCP dengan TLS, berikut adalah beberapa:
- Pembentukan koneksi yang lebih cepat karena tidak memerlukan perjalanan pulang pergi sebanyak TCP dengan TLS di atasnya.
- Menghindari masalah pemblokiran head-of-line di mana satu paket yang hilang tidak memblokir data dari semua aliran lainnya.
Di sisi lain, ada potensi kerugian yang perlu dipertimbangkan saat menggunakan QUIC. Sebagai protokol yang lebih baru, adopsinya masih tumbuh dan terbatas. Terlepas dari itu, lalu lintas QUIC bahkan dapat diblokir oleh beberapa komponen jaringan.
QUIC dalam .NET
Implementasi QUIC diperkenalkan di .NET 5 sebagai System.Net.Quic
pustaka. Namun, hingga .NET 7 pustaka benar-benar internal dan hanya berfungsi sebagai implementasi HTTP/3. Dengan .NET 7, pustaka dipublikasikan sehingga mengekspos API-nya.
Catatan
Di .NET 7.0 dan 8.0, API diterbitkan sebagai fitur pratinjau. Dimulai dengan .NET 9, API ini tidak lagi dianggap sebagai fitur pratinjau dan sekarang dianggap stabil.
Dari perspektif implementasi, System.Net.Quic
tergantung pada MsQuic, implementasi asli protokol QUIC. Akibatnya, System.Net.Quic
dukungan platform dan dependensi diwarisi dari MsQuic dan didokumenkan di bagian dependensi Platform. Singkatnya, pustaka MsQuic dikirim sebagai bagian dari .NET untuk Windows. Tetapi untuk Linux, Anda harus menginstal libmsquic
secara manual melalui manajer paket yang sesuai. Untuk platform lain, masih mungkin untuk membangun MsQuic secara manual, baik terhadap SChannel atau OpenSSL, dan menggunakannya dengan System.Net.Quic
. Namun, skenario ini bukan bagian dari matriks pengujian kami dan masalah yang tidak terduga mungkin terjadi.
Dependensi platform
Bagian berikut menjelaskan dependensi platform untuk QUIC di .NET.
Windows
- Windows 11, Windows Server 2022, atau yang lebih baru. (Versi Windows sebelumnya tidak memiliki API kriptografi yang diperlukan untuk mendukung QUIC.)
Di Windows, msquic.dll didistribusikan sebagai bagian dari runtime .NET, dan tidak ada langkah lain yang diperlukan untuk menginstalnya.
Linux
Catatan
.NET 7+ hanya kompatibel dengan 2.2+ versi libmsquic.
Paket libmsquic
diperlukan di Linux. Paket ini diterbitkan di repositori paket Linux resmi Microsoft, https://packages.microsoft.com dan juga tersedia di beberapa repositori resmi, seperti Paket Alpine - libmsquic.
libmsquic
Menginstal dari repositori paket Linux resmi Microsoft
Anda harus menambahkan repositori ini ke manajer paket Anda sebelum menginstal paket. Untuk informasi selengkapnya, lihat Repositori Perangkat Lunak Linux untuk Produk Microsoft.
Perhatian
Menambahkan repositori paket Microsoft mungkin bertentangan dengan repositori distribusi Anda saat repositori distribusi Anda menyediakan .NET dan paket Microsoft lainnya. Untuk menghindari atau memecahkan masalah campuran paket, tinjau Memecahkan masalah kesalahan .NET yang terkait dengan file yang hilang di Linux.
Contoh
Berikut adalah beberapa contoh penggunaan manajer paket untuk menginstal libmsquic
:
APT
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
libmsquic
Menginstal dari Repositori Paket Distribusi
libmsquic
Menginstal dari repositori paket distribusi juga dimungkinkan, tetapi saat ini ini hanya tersedia untuk Alpine
.
Contoh
Berikut adalah beberapa contoh penggunaan manajer paket untuk menginstal libmsquic
:
- Alpine 3.21 dan yang lebih baru
apk add libmsquic
- Alpine 3.20 dan yang lebih lama
# Get libmsquic from community repository edge branch.
apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ libmsquic
Dependensi libmsquic
Semua dependensi berikut dinyatakan dalam libmsquic
manifes paket dan secara otomatis diinstal oleh manajer paket:
OpenSSL 3+ atau 1.1 - tergantung pada versi OpenSSL default untuk versi distribusi, misalnya, OpenSSL 3 untuk Ubuntu 22 dan OpenSSL 1.1 untuk Ubuntu 20.
libnuma1
macOS
QUIC sekarang didukung sebagian pada macOS melalui manajer paket Homebrew non-standar dengan beberapa batasan. Anda dapat menginstal libmsquic
di macOS menggunakan Homebrew dengan perintah berikut:
brew install libmsquic
Untuk menjalankan aplikasi .NET yang menggunakan libmsquic
, Anda perlu mengatur variabel lingkungan sebelum menjalankannya. Ini memastikan aplikasi dapat menemukan libmsquic
pustaka selama pemuatan dinamis runtime. Anda dapat melakukan ini dengan menambahkan perintah berikut sebelum perintah utama Anda:
DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib dotnet run
or
DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib ./binaryname
Atau, Anda dapat mengatur variabel lingkungan dengan:
export DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH:$(brew --prefix)/lib
lalu jalankan perintah utama Anda:
./binaryname
Ringkasan API
System.Net.Quic membawa tiga kelas utama yang memungkinkan penggunaan protokol QUIC:
- QuicListener - kelas sisi server untuk menerima koneksi masuk.
- QuicConnection - Koneksi QUIC, sesuai dengan RFC 9000 Bagian 5.
- QuicStream - Aliran QUIC, sesuai dengan RFC 9000 Bagian 2.
Tetapi sebelum menggunakan kelas-kelas ini, kode Anda harus memeriksa apakah QUIC saat ini didukung, seperti libmsquic
yang mungkin hilang, atau TLS 1.3 mungkin tidak didukung. Untuk itu, baik QuicListener
dan QuicConnection
mengekspos properti IsSupported
statis :
if (QuicListener.IsSupported)
{
// Use QuicListener
}
else
{
// Fallback/Error
}
if (QuicConnection.IsSupported)
{
// Use QuicConnection
}
else
{
// Fallback/Error
}
Properti ini akan melaporkan nilai yang sama, tetapi mungkin berubah di masa mendatang. Disarankan untuk memeriksa IsSupported skenario server dan IsSupported untuk skenario klien.
QuicListener
QuicListener mewakili kelas sisi server yang menerima koneksi masuk dari klien. Pendengar dibangun dan dimulai dengan metode ListenAsync(QuicListenerOptions, CancellationToken)statis . Metode ini menerima instans QuicListenerOptions kelas dengan semua pengaturan yang diperlukan untuk memulai pendengar dan menerima koneksi masuk. Setelah itu, pendengar siap untuk membagikan koneksi melalui AcceptConnectionAsync(CancellationToken). Koneksi yang dikembalikan oleh metode ini selalu terhubung sepenuhnya, yang berarti bahwa jabat tangan TLS selesai dan koneksi siap digunakan. Akhirnya, untuk berhenti mendengarkan dan melepaskan semua sumber daya, DisposeAsync() harus dipanggil.
Pertimbangkan contoh kode berikut 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
{
// 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();
Untuk informasi selengkapnya tentang bagaimana QuicListener
desainnya, lihat proposal API.
QuicConnection
QuicConnection adalah kelas yang digunakan untuk koneksi QUIC sisi server dan klien. Koneksi sisi server dibuat secara internal oleh pendengar dan diserahkan melalui AcceptConnectionAsync(CancellationToken). Koneksi sisi klien harus dibuka dan tersambung ke server. Seperti halnya pendengar, ada metode ConnectAsync(QuicClientConnectionOptions, CancellationToken) statis yang membuat instans dan menghubungkan koneksi. Ini menerima instans QuicClientConnectionOptions, kelas analog ke QuicServerConnectionOptions. Setelah itu, pekerjaan dengan koneksi tidak berbeda antara klien dan server. Ini dapat membuka aliran keluar dan menerima yang masuk. Ini juga menyediakan properti dengan informasi tentang koneksi, seperti LocalEndPoint, RemoteEndPoint, atau RemoteCertificate.
Ketika pekerjaan dengan koneksi selesai, itu perlu ditutup dan dibuang. Protokol QUIC mengamanatkan menggunakan kode lapisan aplikasi untuk penutupan segera, lihat RFC 9000 Bagian 10.2. Untuk itu, CloseAsync(Int64, CancellationToken) dengan kode lapisan aplikasi dapat dipanggil atau jika tidak, DisposeAsync() akan menggunakan kode yang disediakan dalam DefaultCloseErrorCode. Bagaimanapun, DisposeAsync() harus dipanggil di akhir pekerjaan dengan koneksi untuk sepenuhnya merilis semua sumber daya terkait.
Pertimbangkan contoh kode berikut 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 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();
untuk informasi selengkapnya tentang bagaimana QuicConnection
dirancang, lihat proposal API.
QuicStream
QuicStream adalah jenis aktual yang digunakan untuk mengirim dan menerima data dalam protokol QUIC. Ini berasal dari biasa Stream dan dapat digunakan seperti itu, tetapi juga menawarkan beberapa fitur yang khusus untuk protokol QUIC. Pertama, aliran QUIC dapat bersifat searah atau dua arah, lihat RFC 9000 Bagian 2.1. Aliran dua arah dapat mengirim dan menerima data di kedua sisi, sedangkan aliran searah hanya dapat menulis dari sisi yang memulai dan membaca pada yang menerima. Setiap rekan dapat membatasi berapa banyak aliran bersamaan dari setiap jenis yang bersedia menerima, melihat MaxInboundBidirectionalStreams , dan MaxInboundUnidirectionalStreams.
Kekhususan lain dari aliran QUIC adalah kemampuan untuk secara eksplisit menutup sisi penulisan di tengah pekerjaan dengan aliran, melihat CompleteWrites() atau WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken) kelebihan beban dengan completeWrites
argumen. Penutupan sisi penulisan memungkinkan serekan tahu bahwa tidak ada lagi data yang akan tiba, namun peer masih dapat terus mengirim (jika terjadi aliran dua arah). Ini berguna dalam skenario seperti pertukaran permintaan/respons HTTP ketika klien mengirim permintaan dan menutup sisi penulisan untuk memberi tahu server bahwa ini adalah akhir dari konten permintaan. Server masih dapat mengirim respons setelah itu, tetapi tahu bahwa tidak ada lagi data yang akan tiba dari klien. Dan untuk kasus yang salah, baik menulis atau membaca sisi aliran dapat dibatalkan, lihat Abort(QuicAbortDirection, Int64).
Catatan
Membuka aliran hanya mencadangkannya tanpa mengirim data apa pun. Pendekatan ini dirancang untuk mengoptimalkan penggunaan jaringan dengan menghindari transmisi bingkai yang hampir kosong. Karena serekan tidak diberi tahu sampai data aktual dikirim, aliran tetap tidak aktif dari perspektif serekan. Jika Anda tidak mengirim data, peer tidak akan mengenali aliran, yang dapat menyebabkan AcceptInboundStreamAsync()
macet karena menunggu aliran yang bermakna. Untuk memastikan komunikasi yang tepat, Anda perlu mengirim data setelah membuka aliran.
Perilaku metode individual untuk setiap jenis aliran dirangkum dalam tabel berikut (perhatikan bahwa klien dan server dapat membuka dan menerima aliran):
Metode | Aliran pembukaan serekan | Peer menerima aliran |
---|---|---|
CanRead |
dua arah: true unidirectional: false |
true |
CanWrite |
true |
dua arah: true unidirectional: false |
ReadAsync |
dua arah: membaca data unidirectional: InvalidOperationException |
membaca data |
WriteAsync |
mengirim data => peer read mengembalikan data | dua arah: mengirim data => peer read mengembalikan data unidirectional: InvalidOperationException |
CompleteWrites |
menutup sisi penulisan => peer read mengembalikan 0 | dua arah: menutup sisi penulisan => pembacaan serekan mengembalikan 0 unidirectional: no-op |
Abort(QuicAbortDirection.Read) |
dua arah: STOP_SENDING => peer write throws QuicException(QuicError.OperationAborted) unidirectional: no-op |
STOP_SENDING => peer write throwsQuicException(QuicError.OperationAborted) |
Abort(QuicAbortDirection.Write) |
RESET_STREAM => peer read throwsQuicException(QuicError.OperationAborted) |
dua arah: RESET_STREAM => peer read throws QuicException(QuicError.OperationAborted) unidirectional: no-op |
Selain metode ini, QuicStream
menawarkan dua properti khusus untuk mendapatkan pemberitahuan setiap kali sisi membaca atau menulis aliran telah ditutup: ReadsClosed dan WritesClosed. Keduanya mengembalikan Task
yang lengkap dengan sisi yang sesuai ditutup, baik berhasil atau dibatalkan, dalam hal ini Task
akan berisi pengecualian yang sesuai. Properti ini berguna ketika kode pengguna perlu mengetahui tentang sisi aliran ditutup tanpa mengeluarkan panggilan ke ReadAsync
atau WriteAsync
.
Akhirnya, ketika pekerjaan dengan aliran selesai, itu perlu dibuang dengan DisposeAsync(). Pembuangan akan memastikan bahwa sisi baca dan/atau tulis - tergantung pada jenis aliran - ditutup. Jika aliran belum dibaca dengan benar sampai akhir, buang akan mengeluarkan yang setara dengan Abort(QuicAbortDirection.Read)
. Namun, jika sisi penulisan aliran belum ditutup, itu akan ditutup dengan anggun seperti yang akan terjadi dengan CompleteWrites
. Alasan perbedaan ini adalah untuk memastikan bahwa skenario yang bekerja dengan perilaku biasa Stream
seperti yang diharapkan dan mengarah ke jalur yang berhasil. Pertimbangkan contoh berikut:
// 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);
Penggunaan sampel QuicStream
dalam skenario klien:
// 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.
Dan penggunaan QuicStream
sampel dalam skenario server:
// 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.
Untuk informasi selengkapnya tentang bagaimana QuicStream
desainnya, lihat proposal API.