ASP.NET Core での Websocket のサポート

作成者: Tom Dykstra および Andrew Stanton-Nurse

この記事では、ASP.NET Core で Websocket の使用を開始する方法について説明します。 WebSocket (RFC 6455) は、TCP 接続を使用した双方向の永続的通信チャネルを有効にするプロトコルです。 このプロトコルは、チャット、ダッシュボード、ゲーム アプリなど、高速かつリアルタイムのコミュニケーションを活用するアプリで使用されます。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。 実行方法

SignalR

ASP.NET Core SignalR は、アプリへのリアルタイム Web 機能の追加を簡単にするライブラリです。 可能なかぎり、WebSocket が使用されます。

ほとんどのアプリケーションでは、生の WebSocket よりも SignalR が推奨されます。 SignalR には、WebSocket を使用できない環境の場合にトランスポートのフォールバックが用意されています。 基本的なリモート プロシージャ呼び出しアプリ モデルも用意されています。 また、ほとんどのシナリオで、SignalR には生の WebSocket を使用した場合と比較してパフォーマンス上の大きなデメリットがありません。

一部のアプリでは、.NET の gRPC に Websocket の代替手段が用意されています。

前提条件

  • ASP.NET Core をサポートする任意の OS:
    • Windows 7 / Windows Server 2008 以降
    • Linux
    • macOS
  • アプリが IIS を含む Windows 上で実行されている場合:
    • Windows 8 / Windows Server 2012 以降
    • IIS 8 / IIS 8 Express
    • WebSockets を有効にする必要があります。 「IIS/IIS Express のサポート」セクションを参照してください。
  • アプリが HTTP.sys で実行されている場合:
    • Windows 8 / Windows Server 2012 以降
  • サポートされているブラウザーについては、 https://caniuse.com/#feat=websockets を参照してください。

ミドルウェアの構成

Startup クラスの Configure メソッドに、Websocket ミドルウェアを追加します。

app.UseWebSockets();

注意

コントローラーで WebSocket 要求を受け入れる場合は、app.UseWebSockets への呼び出しが app.UseEndpoints の前に行われる必要があります。

次の設定を構成できます。

  • KeepAliveInterval: プロキシの接続の維持を保証する、クライアントに "ping" フレームを送信する頻度。 既定値は 2 分です。

次の設定を構成できます。

  • KeepAliveInterval: プロキシの接続の維持を保証する、クライアントに "ping" フレームを送信する頻度。 既定値は 2 分です。
  • AllowedOrigins - WebSocket 要求で許可される配信元ヘッダー値の一覧。 既定では、すべての配信元が許可されています。 詳細については、下記の "WebSocket の配信元の制限" を参照してください。
var webSocketOptions = new WebSocketOptions() 
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

app.UseWebSockets(webSocketOptions);

WebSocket の要求の受け入れ

以降の要求ライフサイクルのどこかで (たとえば、以降の Configure メソッドまたはアクション メソッド)、それが WebSocket 要求であるかを確認し、WebSocket 要求を受け入れます。

次の例は、以降の Configure メソッドから抜粋したものです。

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
            {
                await Echo(context, webSocket);
            }
        }
        else
        {
            context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
        }
    }
    else
    {
        await next();
    }

});

WebSocket 要求はどの URL からも受け取る場合がありますが、このサンプル コードでは /ws の要求のみを受け取ります。

コントローラー メソッドでも同様の方法を使用できます。

public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using WebSocket webSocket = await 
                               HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(HttpContext, webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
    }

WebSocket を使用するとき、接続中、ミドルウェア パイプラインの実行を維持する 必要があります。 ミドルウェア パイプラインの修了後に WebSocket メッセージを送信するか、受信する場合、次のような例外を受け取ることがあります。

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

バックグラウンド サービスを利用してデータを WebSocket に書き込む場合、ミドルウェア パイプラインの実行を維持します。 これは TaskCompletionSource<TResult> を使用して行います。 TaskCompletionSource をバックグラウンド サービスに渡し、WebSocket が終わったとき、それに TrySetResult を呼び出させます。 次の例に示すように、要求中に Task プロパティの await を行います。

app.Use(async (context, next) =>
{
    using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
    {
        var socketFinishedTcs = new TaskCompletionSource<object>();

        BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

        await socketFinishedTcs.Task;
    }
});

WebSocket の終了例外は、アクション メソッドから早く戻りすぎた場合にも発生する可能性があります。 アクション メソッドでソケットを受け入れる場合は、そのソケットを使用するコードが完了するまで待ち、アクション メソッドから戻ってください。

重大なスレッドの問題を引き起こす可能性があるので、ソケットの完了を待つために Task.WaitTask.Result、または同様のブロック呼び出しを使用しないでください。 常に await を使用します。

圧縮

警告

暗号化された接続での圧縮を有効にすると、アプリが CRIME 攻撃や BREACH 攻撃を受ける可能性があります。 機密情報を送信するときは、圧縮を有効にしたり、WebSocket.SendAsync を呼び出すときに WebSocketMessageFlags.DisableCompression を使用したりしないでください。 これは WebSocket の両方の側に当てはまります。 ブラウザーの Websocket API には、送信ごとに圧縮を無効にするための構成がないことに注意してください。

Websocket でのメッセージの圧縮が必要な場合は、受け入れるコードで次のように圧縮を許可することを指定する必要があります。

using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext() { DangerousEnableCompression = true }))
{
}

WebSocketAcceptContext.ServerMaxWindowBits および WebSocketAcceptContext.DisableServerContextTakeover は、圧縮の動作を制御する高度なオプションです。

最初に接続を確立するときに、クライアントとサーバーの間で圧縮がネゴシエートされます。 ネゴシエーションの詳細については、「Compression Extensions for WebSocket (WebSocket の圧縮拡張機能)」(RFC) を参照してください。

注意

サーバーまたはクライアントによって圧縮のネゴシエーションが受け入れられない場合でも、接続は確立されます。 ただし、その接続ではメッセージの送受信時に圧縮が使用されません。

メッセージの送受信

AcceptWebSocketAsync メソッドは、TCP 接続を WebSocket 接続にアップグレードし、WebSocket オブジェクトを提供します。 メッセージの送受信に、WebSocket オブジェクトを使用します。

前に示した、WebSocket 要求を受け入れるコードが、WebSocket オブジェクトを Echo メソッドに渡します。 このコードは、メッセージを受信し、同じメッセージをすぐに送信します。 クライアントが接続を閉じるまで、メッセージがループで送受信されます。

private async Task Echo(HttpContext context, WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

このループを開始する前に、WebSocket 接続を受け入れた場合、ミドルウェア パイプラインは終了します。 ソケットを閉じると、パイプラインはアンワインドされます。 つまり、WebSocket を受け入れると、要求はパイプラインでの先への移動を中止します。 ループを終了し、ソケットを閉じた場合、要求はパイプラインのバックアップを続けます。

クライアントの切断の処理

接続の損失によってクライアントが切断されても、サーバーに自動的に通知されるわけではありません。 サーバーが切断メッセージを受信するのは、クライアントがそれを送信した場合のみです。インターネット接続が失われた場合、これを実行することはできません。 これが発生した場合に何らかのアクションを実行したい場合は、特定の時間枠内でクライアントからの受信を待つタイムアウトを設定します。

クライアントが常にメッセージを送信するとは限らず、その接続がアイドル状態になっただけでタイムアウトしたくない場合は、X 秒ごとに ping メッセージを送信するタイマーをクライアントに使用させます。 サーバー上では、前のものから 2*X 秒以内にメッセージが到着しなかった場合に、接続を終了してクライアントが切断されたことをレポートします。 予想される 2 倍の期間を待機することで、ping メッセージを遅らせる可能性のあるネットワークの遅延のために余分な時間を残します。

WebSocket の配信元の制限

CORS で提供される保護は、WebSocket には適用されません。 ブラウザーでは以下を実行 しません

  • CORS の事前要求を実行する。
  • WebSocket 要求を行うときに Access-Control ヘッダーに指定された制限を考慮する。

ただし、WebSocket 要求を発行するときにはブラウザーから Origin ヘッダーが送信されます。 予期した配信元からの WebSocket のみが許可されるように、アプリケーションでこれらのヘッダーが検証されるように構成する必要があります。

"https://server.com" でサーバーを、"https://client.com" でクライアントをホスティングしている場合は、検証のために "https://client.com" を WebSocket の AllowedOrigins 一覧に追加します。

var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

注意

Origin ヘッダーは、クライアントによって制御され、Referer のように偽装することができます。 これらのヘッダーを認証メカニズムとして使用 しないでください

IIS/IIS Express のサポート

IIS/IIS Express 8 以降を含む、Windows Server 2012 以降および Windows 8 以降では、WebSocket プロトコルをサポートします。

注意

IIS Express を使用する場合、WebSockets は常に有効になります。

IIS での WebSockets の有効化

Windows Server 2012 以降で WebSocket プロトコルのサポートを有効にするには

注意

これらの手順は、IIS Express を使用する場合は必要ありません

  1. [管理] メニューから 役割と機能の追加 ウィザードを使用するか、サーバー マネージャー にあるリンクを使用します。
  2. [役割ベースまたは機能ベースのインストール] を選択します。 [次へ] を選択します。
  3. 適切なサーバーを選択します (既定では、ローカル サーバーが選択されます)。 [次へ] を選択します。
  4. [役割] ツリーで [Web サーバー (IIS)] を展開し、 [Web サーバー][アプリケーション開発] の順に展開します。
  5. [WebSocket プロトコル] を選択します。 [次へ] を選択します。
  6. 追加機能が不要な場合は、 [次へ] を選択します。
  7. [インストール] を選択します。
  8. インストールが完了したら、 [閉じる] を選択してウィザードを終了します。

Windows 8 以降で WebSocket プロトコルのサポートを有効にするには

注意

これらの手順は、IIS Express を使用する場合は必要ありません

  1. [コントロール パネル] > [プログラム] > [プログラムと機能] > [Windows の機能の有効化または無効化] (画面の左側) に移動します。
  2. 次のノード: [インターネット インフォメーション サービス] > [World Wide Web サービス] > [アプリケーション開発機能] を開きます。
  3. [WebSocket プロトコル] 機能を選択します。 [OK] を選択します。

Node.js で socket.io を使用する場合に WebSocket を無効にする

Node.jssocket.io で WebSocket サポートを使用する場合、webSocket 要素を使用する既定の WebSocket モジュールを web.config または applicationHost.config で無効にします。この手順が実行されない場合、IIS WebSocket モジュールは Node.js とアプリ以外の WebSocket コミュニケーションを処理しようとします。

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

サンプル アプリ

この記事に添えられているサンプル アプリは、エコー アプリです。 これには、WebSocket 接続を作成する Web ページがあり、サーバーが受け取るすべてのメッセージをクライアントに再送信します。 このサンプル アプリは、IIS Express を使用して Visual Studio から実行するように構成されていないため、コマンド シェルで dotnet run を使用してアプリを実行し、ブラウザーで http://localhost:5000 に移動します。 Web ページに接続状態が表示されます。

WebSocket 接続前の Web ページの初期状態

[接続] を選択し、表示されている URL に WebSocket 要求を送信します。 テスト メッセージを入力し、 [送信] を選択します。 完了したら、 [Close Socket](ソケットを閉じる) を選択します。 [Communication Log](コミュニケーション ログ) セクションに、発生した各オープン、送信、クローズのアクションが表示されます。

WebSocket 接続とテスト メッセージの送受信が行われた後の Web ページの最終的な状態