QUIC プロトコル
QUIC は、RFC 9000 で標準化されたネットワーク トランスポート層プロトコルです。 基になるプロトコルとして UDP が使用されており、TLS 1.3 の使用が義務付けられているため、本質的に安全です。 詳細については、RFC 9001 を参照してください。 TCP や UDP などの既知のトランスポート プロトコルとのもう 1 つの興味深い違いは、トランスポート層にストリーム多重化が組み込まれている点です。 これにより、相互に影響を与えない複数の同時実行の独立したデータ ストリームを使用できます。
QUIC 自体は、トランスポート プロトコルであるため、交換されたデータのセマンティクスを定義しません。 これはむしろ、アプリケーション層プロトコル (HTTP/3 や SMB over QUIC など) で使用されます。 また、任意のカスタム定義プロトコルにも使用できます。
このプロトコルには、TCP と TLS よりも多くの利点があります。そのいくつかを以下に示します。
- TCP と TLS ほど多くのラウンド トリップを必要としないため、接続の確立がより高速になる。
- 1 つの失われたパケットが他のすべてのストリームのデータをブロックしない、ヘッドオブライン ブロッキングの問題を回避できる。
一方、QUIC を使用する場合に考慮すべき潜在的な欠点があります。 新しいプロトコルとして、引き続き採用され、制限されます。 それとは別に、QUIC トラフィックは一部のネットワーク コンポーネントによってブロックされる可能性もあります。
.NET での QUIC
QUIC の実装は、System.Net.Quic
ライブラリとして .NET 5 で導入されました。 しかし、.NET 7 まではライブラリは厳密に内部的なものであり、HTTP/3 の実装としてのみ機能していました。 .NET 7 では、ライブラリが公開され、それにより API が公開されました。
Note
.NET 7.0 および 8.0 では、API はプレビュー機能として公開されました。 .NET 9 以降、これらの API はプレビュー機能とは見なされなくなり、安定的に動作すると考えられるようになりました。
実装の観点から、System.Net.Quic
は、QUIC プロトコルのネイティブ実装である MsQuic に依存します。 その結果、System.Net.Quic
プラットフォームのサポートと依存関係は MsQuic から継承されます。これについては「プラットフォームの依存関係」セクションに記載されています。 つまり、MsQuic ライブラリは Windows 用 .NET の一部として出荷されます。 しかし、Linux の場合、libmsquic
は適切なパッケージ マネージャーを使用して手動でインストールする必要があります。 他のプラットフォームでは、対象が SChannel であるか OpenSSL であるかに関係なく、引き続き MsQuic を手動でビルドし、System.Net.Quic
で使用することができます。 しかし、これらのシナリオはテスト マトリックスの一部ではなく、予期しない問題が発生する可能性があります。
プラットフォームの依存関係
以下のセクションでは、.NET における QUIC のプラットフォームの依存関係について説明します。
Windows
- Windows 11、Windows Server 2022、またはそれ以降。 (以前のバージョンの Windows には、QUIC をサポートするために必要な暗号化 API がありません。)
Windows では、msquic.dll は .NET ランタイムの一部として配布され、インストールするために他の手順は必要ありません。
Linux
Note
.NET 7 以降は libmsquic の 2.2 以降のバージョンとのみ互換性があります。
Linux では、libmsquic
パッケージが必要です。 このパッケージは、 https://packages.microsoft.com Microsoft の公式 Linux パッケージ リポジトリで公開されており、 Alpine Packages - libmsquic などの一部の公式リポジトリでも入手できます。
Microsoft の公式 Linux パッケージ リポジトリからの libmsquic
のインストール
パッケージをインストールする前に、このリポジトリをパッケージ マネージャーに追加する必要があります。 詳しくは、「Microsoft 製品用 Linux ソフトウェア リポジトリ」をご覧ください。
注意事項
ディストリビューションのリポジトリで .NET やその他の Microsoft パッケージが提供されている場合、Microsoft パッケージ リポジトリを追加すると、ディストリビューションのリポジトリと競合する可能性があります。 パッケージの混同を回避またはトラブルシューティングするには、「Linux 上で見つからないファイルに関連する .NET エラーのトラブルシューティング」を参照してください。
例
パッケージ マネージャーを使って 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
のインストール
配布パッケージ リポジトリから libmsquic
をインストールすることもできますが、現在、これは Alpine
でのみ使用できます。
例
パッケージ マネージャーを使って libmsquic
をインストールする例を次に示します。
- Alpine 3.21 以降
apk add libmsquic
- Alpine 3.20 以前
# Get libmsquic from community repository edge branch.
apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ libmsquic
libmsquic の依存関係
次のすべての依存関係は libmsquic
パッケージ マニフェストに記載されており、パッケージ マネージャーによって自動的にインストールされます。
OpenSSL 3 以降または 1.1 - ディストリビューション バージョンの既定の OpenSSL バージョンによって異なります (たとえば、Ubuntu 22 の場合は OpenSSL 3、Ubuntu 20 の場合は OpenSSL 1.1)。
libnuma 1
macOS
現在、QUIC は macOS ではサポートされていませんが、今後のリリースで利用可能になる可能性があります。
API の概要
System.Net.Quic では、QUIC プロトコルの使用を可能にする次の 3 つの主要なクラスが提供されます。
- QuicListener - 受信接続を受け入れるためのサーバー側クラス。
- QuicConnection - RFC 9000 セクション 5 に対応する QUIC 接続。
- QuicStream - RFC 9000 セクション 2 に対応する QUIC ストリーム。
しかし、これらのクラスを使用する前に、libmsquic
が欠落しているか、TLS 1.3 がサポートされていない可能性があるため、コードで QUIC が現在サポートされているかどうかを確認する必要があります。 このため、QuicListener
と QuicConnection
の両方で静的プロパティ IsSupported
が公開されます。
if (QuicListener.IsSupported)
{
// Use QuicListener
}
else
{
// Fallback/Error
}
if (QuicConnection.IsSupported)
{
// Use QuicConnection
}
else
{
// Fallback/Error
}
これらのプロパティでは同じ値が報告されますが、今後変更される可能性があります。 サーバー シナリオの IsSupported とクライアント シナリオの IsSupported を確認することをお勧めします。
QuicListener
QuicListener は、クライアントからの受信接続を受け入れるサーバー側クラスを表します。 リスナーは静的メソッド ListenAsync(QuicListenerOptions, CancellationToken) で構築され、開始されます。 このメソッドでは、リスナーを開始して受信接続を受け入れるのに必要なすべての設定で、QuicListenerOptions クラスのインスタンスを受け入れます。 その後、リスナーは AcceptConnectionAsync(CancellationToken) 経由で接続を渡す準備が整います。 このメソッドによって返される接続は常に完全に接続されます。つまり、TLS ハンドシェイクが完了し、接続を使用する準備が整います。 最後に、リッスンを停止し、すべてのリソースを解放するには、DisposeAsync() を呼び出す必要があります。
次の 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();
QuicListener
の設計方法について詳しくは、API の提案に関するページを参照してください。
QuicConnection
QuicConnection は、サーバー側とクライアント側の QUIC 接続の両方に使用されるクラスです。 サーバー側の接続はリスナーによって内部的に作成され、AcceptConnectionAsync(CancellationToken) 経由で渡されます。 クライアント側の接続を開き、サーバーに接続する必要があります。 リスナーと同様に、接続をインスタンス化して接続する静的メソッド ConnectAsync(QuicClientConnectionOptions, CancellationToken) があります。 QuicServerConnectionOptions に似たクラスである QuicClientConnectionOptions のインスタンスが受け入れられます。 その後の接続の処理はクライアントとサーバーで違いはありません。 送信ストリームを開き、受信ストリームを受け入れることができます。 また、LocalEndPoint、RemoteEndPoint、RemoteCertificate など、接続に関する情報がプロパティに提供されます。
接続の処理が完了したら、接続を閉じて破棄する必要があります。 QUIC プロトコルでは、アプリケーション層コードを使用して直ちに閉じることが義務付けられています。RFC 9000 セクション 10.2 を参照してください。 そのため、アプリケーション層コードを使用した CloseAsync(Int64, CancellationToken) を呼び出すことができます。呼び出さない場合は、DisposeAsync()DefaultCloseErrorCode で提供されるコードが使用されます。 いずれの場合も、関連付けられているすべてのリソースを完全に解放するには、接続の作業の最後に DisposeAsync() を呼び出す必要があります。
次の 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();
QuicConnection
の設計方法について詳しくは、API 提案を確認してください。
QuicStream
QuicStream は、QUIC プロトコルでデータを送受信するために使用される実際の種類です。 通常の Stream から派生し、そのように使用できますが、QUIC プロトコルに固有のいくつかの機能も提供されます。 まず、QUIC ストリームを一方向にも双方向にもすることができます。RFC 9000 セクション 2.1 を参照してください。 双方向ストリームは両側でデータを送受信できますが、一方向ストリームは開始側からのみ書き込み、受け入れ側で読み取ることができます。 各ピアでは、受け入れる各種類の同時ストリームの数を制限できます。「MaxInboundBidirectionalStreams」および「MaxInboundUnidirectionalStreams」を参照してください。
QUIC ストリームのもう 1 つの特殊性は、ストリームの処理の途中で書き込み側を明示的に閉じる機能です。CompleteWrites() に関するページ、または completeWrites
引数を使用するオーバーロードに関する「WriteAsync(ReadOnlyMemory<Byte>, Boolean, CancellationToken)」を参照してください。 書き込み側を閉じると、ピアではそれ以上データが到着しないことがわかりますが、ピアで (双方向ストリームの場合は) 送信を続行できます。 これは、クライアントが要求を送信し、書き込み側を閉じて、これが要求コンテンツの最後であることをサーバーに知らせるときの HTTP 要求/応答交換などのシナリオで役立ちます。 サーバーではその後も応答を送信できますが、クライアントからそれ以上データが到着しないことがわかっています。 また、誤ったケースの場合、ストリームの書き込み側または読み取り側が中止される可能性があります。Abort(QuicAbortDirection, Int64) に関するページを参照してください。
Note
ストリームを開くと、データを送信せずに予約されるだけです。 このアプローチは、ほぼ空のフレームの送信を回避することで、ネットワークの使用を最適化するように設計されています。 ピアは実際のデータが送信されるまで通知されないため、ストリームはピアの観点から非アクティブなままになります。 データを送信しない場合、ピアはストリームを認識しないため、意味のあるストリームを待機するときに AcceptInboundStreamAsync()
がハングする原因になる可能性があります。 適切なコミュニケーションを確保するには、ストリームを開いた後にデータを送信する必要があります。
ストリームの種類ごとの個々のメソッドの動作を次の表にまとめます (クライアントとサーバーの両方でストリームを開いて受け入れられることに注意してください)。
認証方法 | ストリームを開くピア | ストリームを受け入れるピア |
---|---|---|
CanRead |
双方向: true 一方向: false |
true |
CanWrite |
true |
双方向: true 一方向: false |
ReadAsync |
双方向: データを読み取る 一方向: InvalidOperationException |
データを読み取る |
WriteAsync |
データを送信する => ピア読み取りでデータを返す | 双方向: データを送信する => ピア読み取りでデータを返す 一方向: InvalidOperationException |
CompleteWrites |
書き込み側を閉じる => ピア読み取りで 0 を返す | 双方向: 書き込み側を閉じる => ピア読み取りで 0 を返す 一方向: 操作なし |
Abort(QuicAbortDirection.Read) |
双方向: STOP_SENDING => ピア書き込みで QuicException(QuicError.OperationAborted) をスローする一方向: 操作なし |
STOP_SENDING => ピア書き込みで QuicException(QuicError.OperationAborted) をスローする |
Abort(QuicAbortDirection.Write) |
RESET_STREAM => ピア読み取りで QuicException(QuicError.OperationAborted) をスローする |
双方向: RESET_STREAM => ピア読み取りで QuicException(QuicError.OperationAborted) をスローする一方向: 操作なし |
これらのメソッドに加え、QuicStream
では、ストリームの読み取り側または書き込み側のいずれかが閉じられるたびに通知を受け取る 2 つの特殊なプロパティ (ReadsClosed と WritesClosed) が提供されます。 どちらの場合も、対応する側が閉じられて完了した Task
が返されます (成功か中止かに関係なく)。その場合、Task
には適切な例外が含まれます。 これらのプロパティは、ReadAsync
や WriteAsync
が呼び出されずにストリーム側が閉じられることをユーザー コードで認識する必要がある場合に便利です。
最後に、ストリームの処理が完了したら、DisposeAsync() で破棄する必要があります。 破棄することで、ストリームの種類に応じて、読み取り側と書き込み側の両方が確実に閉じられます。 ストリームが最後まで適切に読み取られない場合は、破棄することで Abort(QuicAbortDirection.Read)
と同等のことが行われます。 ただし、ストリームの書き込み側が閉じられていない場合は、CompleteWrites
の場合と同様に適切に閉じられます。 この違いの理由は、確実に通常の Stream
を操作するシナリオが期待どおりに動作し、成功につながるようにするためです。 次の例を確認してください。
// 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);
クライアント シナリオでの 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.
サーバー シナリオでの 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.
QuicStream
の設計方法の詳細については、API の提案に関するページを参照してください。
関連項目
.NET