次の方法で共有


.NET での WebSocket のサポート

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 の種類の攻撃の対象になることに注意してください。詳細については、「CRIMEBREACH」を参照してください。 このようなメッセージの DisableCompression フラグを指定して、シークレットを含むデータを送信する場合は、圧縮を無効にすることを強くお勧めします。

Keep-Alive 戦略

.NET 8 以前では、使用可能な Keep-Alive 戦略は未承諾の PONG のみです。 この戦略は、基になる TCP 接続がアイドル状態にならないようにするのに十分です。ただし、リモート ホストが応答しなくなった場合 (たとえば、リモート サーバーがクラッシュした場合)、要求されていない PONG でこのような状況を検出する唯一の方法は、TCP タイムアウトに依存することです。

.NET 9 では、既存の 設定と新しい KeepAliveInterval 設定を補完する、長く必要な KeepAliveTimeout Keep-Alive 戦略が導入されました。 .NET 9 以降では、Keep-Alive 戦略は次のように選択されています。

  1. 次の場合は Keep-Alive がオフ
    • KeepAliveIntervalTimeSpan.Zero または Timeout.InfiniteTimeSpan
  2. 次の場合は未承諾 PONG
    • KeepAliveInterval は正の有限 TimeSpan、-AND-
    • KeepAliveTimeoutTimeSpan.Zero または Timeout.InfiniteTimeSpan
  3. が 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.KeepAliveIntervalWebSocketCreationOptions.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);

タイムアウトになると、未処理の ReceiveAsyncOperationCanceledException をスローします。

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 で保留中の読み取りを維持することをお勧めします。