WebSocket プロトコルを使用すると、クライアントとリモート ホスト間の双方向通信が可能になります。
System.Net.WebSockets.ClientWebSocket は、開始ハンドシェイクを介して WebSocket 接続を確立する機能を公開し、ConnectAsync
メソッドによって作成および送信されます。
HTTP/1.1 と HTTP/2 WebSocket の違い
HTTP/1.1 経由の WebSocket は単一の TCP 接続を使用するため、接続全体のヘッダーによって管理されます。詳細については、RFC 6455 参照してください。 HTTP/1.1 経由で WebSocket を確立する方法の次の例を考えてみましょう。
Uri uri = new("ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx");
using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, default);
var bytes = new byte[1024];
var result = await ws.ReceiveAsync(bytes, default);
string res = Encoding.UTF8.GetString(bytes, 0, result.Count);
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closed", default);
HTTP/2 では、多重化の性質上、別のアプローチを使用する必要があります。 WebSocket はストリームごとに確立されます。詳細については、RFC 8441 参照してください。 HTTP/2 では、通常の HTTP ストリームと共に複数の Web ソケット ストリームに 1 つの接続を使用し、HTTP/2 のネットワークのより効率的な使用を WebSocket に拡張することができます。 既存のプールされた接続の再利用を許可する ConnectAsync(Uri, HttpMessageInvoker, CancellationToken) を受け入れる、HttpMessageInvoker の特別なオーバーロードがあります。
using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);
HTTP バージョンとポリシーを設定する
既定では、ClientWebSocket
は HTTP/1.1 を使用して開始ハンドシェイクを送信し、ダウングレードを許可します。 .NET 7 では、HTTP/2 経由の Web ソケットを使用できます。
ConnectAsync
を呼び出す前に変更できます。
using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();
ws.Options.HttpVersion = HttpVersion.Version20;
ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);
互換性のないオプション
ClientWebSocket
には、接続が確立される前にユーザーが設定できるプロパティ System.Net.WebSockets.ClientWebSocketOptions があります。 ただし、HttpMessageInvoker
が指定されている場合は、これらのプロパティも含まれます。 あいまいさを回避するには、HttpMessageInvoker
にプロパティを設定し、ClientWebSocketOptions
に既定値を設定する必要があります。 それ以外の場合、ClientWebSocketOptions
が変更されると、ConnectAsync
のオーバーロードは ArgumentException をスローします。
using HttpClientHandler handler = new()
{
CookieContainer = cookies;
UseCookies = cookies != null;
ServerCertificateCustomValidationCallback = remoteCertificateValidationCallback;
Credentials = useDefaultCredentials
? CredentialCache.DefaultCredentials
: credentials;
};
if (proxy is null)
{
handler.UseProxy = false;
}
else
{
handler.Proxy = proxy;
}
if (clientCertificates?.Count > 0)
{
handler.ClientCertificates.AddRange(clientCertificates);
}
HttpMessageInvoker invoker = new(handler);
using ClientWebSocket cws = new();
await cws.ConnectAsync(uri, invoker, cancellationToken);
圧縮
WebSocket プロトコルは、メッセージごとのデフレート を、RFC 7692で定義されているようにサポートします。 それは System.Net.WebSockets.ClientWebSocketOptions.DangerousDeflateOptionsによって制御される。 存在する場合、オプションはハンドシェイク フェーズ中にサーバーに送信されます。 サーバーがメッセージ単位のデフレートをサポートし、オプションを受け入れる場合、すべてのメッセージに対して既定で圧縮が有効になっている ClientWebSocket
インスタンスが作成されます。
using ClientWebSocket ws = new()
{
Options =
{
DangerousDeflateOptions = new WebSocketDeflateOptions()
{
ClientMaxWindowBits = 10,
ServerMaxWindowBits = 10
}
}
};
重要
圧縮を使用する前に、これを有効にすると、アプリケーションが CRIME/BREACH の種類の攻撃の対象になることに注意してください。詳細については、「CRIME と BREACH」を参照してください。 このようなメッセージの DisableCompression
フラグを指定して、シークレットを含むデータを送信する場合は、圧縮を無効にすることを強くお勧めします。
Keep-Alive 戦略
.NET 8 以前では、使用可能な Keep-Alive 戦略は未承諾の PONG のみです。 この戦略は、基になる TCP 接続がアイドル状態にならないようにするのに十分です。ただし、リモート ホストが応答しなくなった場合 (たとえば、リモート サーバーがクラッシュした場合)、要求されていない PONG でこのような状況を検出する唯一の方法は、TCP タイムアウトに依存することです。
.NET 9 では、既存の 設定と新しい KeepAliveInterval
設定を補完する、長く必要な KeepAliveTimeout
Keep-Alive 戦略が導入されました。 .NET 9 以降では、Keep-Alive 戦略は次のように選択されています。
- 次の場合は Keep-Alive がオフ
-
KeepAliveInterval
がTimeSpan.Zero
またはTimeout.InfiniteTimeSpan
-
- 次の場合は未承諾 PONG
-
KeepAliveInterval
は正の有限TimeSpan
、-AND- -
KeepAliveTimeout
がTimeSpan.Zero
またはTimeout.InfiniteTimeSpan
-
-
が PING/PONGの場合
-
KeepAliveInterval
は正の有限TimeSpan
、-AND- -
KeepAliveTimeout
は正の有限TimeSpan
-
既定の KeepAliveTimeout
値は Timeout.InfiniteTimeSpan
されるため、既定の Keep-Alive 動作は .NET バージョン間で一貫しています。
ClientWebSocket
を使用する場合、既定の ClientWebSocketOptions.KeepAliveInterval 値は WebSocket.DefaultKeepAliveInterval (通常は 30 秒) です。 つまり、ClientWebSocket
にはデフォルトで Keep-Alive がONに設定されており、デフォルトの戦略として通知されていないPONGが使用されます。
PING/PONG 戦略に切り替える場合は、ClientWebSocketOptions.KeepAliveTimeout をオーバーライドするだけで十分です。
var ws = new ClientWebSocket();
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(20);
await ws.ConnectAsync(uri, cts.Token);
基本 WebSocket
の場合、Keep-Alive は既定で OFF です。 PING/PONG 戦略を使用する場合は、WebSocketCreationOptions.KeepAliveInterval と WebSocketCreationOptions.KeepAliveTimeout の両方を設定する必要があります。
var options = new WebSocketCreationOptions()
{
KeepAliveInterval = WebSocket.DefaultKeepAliveInterval,
KeepAliveTimeout = TimeSpan.FromSeconds(20)
};
var ws = WebSocket.CreateFromStream(stream, options);
未承認 PONG 戦略が使用されている場合、PONG フレームは一方向のハートビートとして使用されます。 リモート エンドポイントが通信しているかどうかに関係なく、KeepAliveInterval
間隔で定期的に送信されます。
PING/PONG 戦略がアクティブな場合、KeepAliveInterval
されてから 時間が経過した後に PING フレームが送信されます。 各 PING フレームには、予想される PONG 応答とペアリングする整数トークンが含まれています。
KeepAliveTimeout
経過後に PONG 応答が到着しなかった場合、リモート エンドポイントは応答しないと見なされ、WebSocket 接続は自動的に中止されます。
var ws = new ClientWebSocket();
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
await ws.ConnectAsync(uri, cts.Token);
// NOTE: There must be an outstanding read at all times to ensure
// incoming PONGs are processed
var result = await _webSocket.ReceiveAsync(buffer, cts.Token);
タイムアウトになると、未処理の ReceiveAsync
は OperationCanceledException
をスローします。
System.OperationCanceledException: Aborted
---> System.AggregateException: One or more errors occurred. (The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.) (Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request..)
---> System.Net.WebSockets.WebSocketException (0x80004005): The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.
at System.Net.WebSockets.ManagedWebSocket.KeepAlivePingHeartBeat()
...
読み続けて PONG を処理
手記
現在、WebSocket
は、保留中の ReceiveAsync
がある間にのみ受信フレームを処理します。
重要
Keep-Alive タイムアウトを使用する場合、PONG 応答が迅速に処理されることが重要です。 リモートエンドポイントが動作していて適切にPONG応答を送信している場合でも、WebSocket
が受信フレームを処理していない際には、Keep-Aliveメカニズムが「偽陽性」アボートを発生させる可能性があります。 この問題は、タイムアウトが経過する前に PONG フレームがトランスポート ストリームから取得されない場合に発生する可能性があります。
良好な接続を破棄しないように、ユーザーは、Keep-Alive タイムアウトが構成されているすべての WebSocket で保留中の読み取りを維持することをお勧めします。
.NET