vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【ASP.NET Core】Postで受け取った値をWebSocketで送信する

はじめに

今回はこの二つをつなげてみることにします。

流れとしては

  1. WebSocket で接続
  2. Post リクエストを送信する
  3. 2.で受け取った値を 1.に送信する

本当は WebSocket の送信、またはプッシュ通知を使うべきところのような気もしますが、とりあえず今回はこれでいくことにします。

WebSocketのコントローラーをインジェクトできるようにする

以前 WebSocket の通信処理は WebSocketController というクラスで行っており、それは Startup から呼び出していました。

今回はルーティングを行う Controller クラスからも( Post リクエストで値を送信する際)アクセスしたいので、 DI を使ってインジェクトできるようにします。

IWebsocketController.cs

using Microsoft.AspNetCore.Builder;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public interface IWebsocketController {
        void Map(IApplicationBuilder app);
        void SendMessage(Message message);
    }
}

この interface を WebSocketController (クラス)で継承します。

ここで注意が必要なのは、 DI でインジェクトするために WebSocketController のコンストラクタを public にする必要があり、インジェクトする interface も public にする必要がある、ということです。

で、以前試した方法と同じように Startup クラスからインジェクトします。

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebApplication1.Controllers;

namespace WebApplication1 {
    public class Startup {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration) {
            Configuration = configuration;
        }
        public void ConfigureServices(IServiceCollection services) {
            services.AddSingleton< IWebsocketController, WebsocketController>();
            services.AddMvc();
        }
        ~省略~
    }
}

そしてインジェクトされる Controller クラスです。

HomeController.cs

using System;
using Microsoft.AspNetCore.Mvc;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public class HomeController : Controller {
        private readonly IWebsocketController websocket;

        public HomeController(IWebsocketController setWebsocket) {
            websocket = setWebsocket;
        }
        
        ~省略~

        [Route("/PostSample")]
        [HttpPost]
        public void PostSample([FromForm]Message message) {
            // WebSocket で接続しているデバイスに Post リクエスト受け取った値を送信する.
            websocket.SendMessage(message);
        }      
    }
}

接続したWebSocketの情報をキャッシュする

前回トライしたとき、 WebSocket の接続、情報の送受信は下記のようにしていました。

WebsocketController.cs

using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace WebApplication1.Controllers {
    public class WebsocketController {
        public void Map(IApplicationBuilder app) {
            app.UseWebSockets();
            app.Use(async (context, next) => {
                if (context.WebSockets.IsWebSocketRequest) {
                    // HTTPリクエストの内容が WebSocket のものであればAcceptし、メッセージの受信・送信を行うメソッドに渡す.
                    WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
                    await Echo(context, webSocket);
                }
                else {
                    // HTTPリクエストの内容が WebSocket のものでなければエラー.
                    context.Response.StatusCode = 400;
                }
            });
        }

        // 引数のHttpContextは今回未使用. やりたいことがこれだけなら不要かも.
        private async Task Echo(HttpContext context, WebSocket webSocket) {

            // 送受信するデータ(buffer)のサイズ指定.
            byte[] buffer = new byte[1024 * 4];

            WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);

            // 接続がCloseされるまでメッセージの送受信を繰り返し.
            while (result.CloseStatus.HasValue == false) {
                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);
            }
            // Whileを抜けたらClose処理.
            // 接続が Close されると、result.CloseStatusに「NormalClosure」のような値が入ってくる.
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
        }
    }
}

肝心の送受信のところは while でループしていて、 Post リクエストを受けたら送信、とするのは面倒そうです。

ということで、ここを変更してみます。

誰が WebSocket の通信に関する情報を持っているか

誰が WebSocket で通信しているクライアントの情報を持っているか、というと、 WebSocket クラスです。

そのままですね。

これを保持しておけば、ループ以外の場所からも送信ができるハズ。

一人だけ接続する場合

WebSocketController.cs

using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public class WebsocketController: IWebsocketController {
        private WebSocket webSocket;

        // DI によるインジェクト.
        public void Map(IApplicationBuilder app) {
            app.UseWebSockets();
            app.Use(async (context, next) => {
                if (context.WebSockets.IsWebSocketRequest) {
                    webSocket = await context.WebSockets.AcceptWebSocketAsync();
                    await Echo(context);
                }
                else {
                    context.Response.StatusCode = 400;
                }
            });
        }

        public async void SendMessage(Message message) {
            if (webSocket == null) {
                return;
            }
            byte[] additionalBytes = System.Text.Encoding.UTF8.GetBytes(message.BodyText);
            await webSocket.SendAsync(new ArraySegment< byte>(additionalBytes, 0, additionalBytes.Length),
                WebSocketMessageType.Text, true, CancellationToken.None);
        }
        private async Task Echo(HttpContext context) {
            byte[] buffer = new byte[1024 * 4];

            WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);

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

とりあえず一人だけ接続した体で書いてみました。

Echo で ReceiveAsync を残しているのは、接続が Close されたかを調べたいためです。

もちろん WebSocket で接続しているクライアント側からも送信したい場合は SendAsync も必要です。

ローカル変数がメンバー変数になっただけで、特に大きな違いはないかと思います。
送信時、 WebSocket で接続されていない、または接続を閉じた(+ Dispose) 場合に処理をスキップする辺りは多少注意がいるのかもしれません。

複数人で接続した場合

基本的には WebSocket をリスト化するだけです。

WebsocketController.cs

using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using WebApplication1.Domains;

namespace WebApplication1.Controllers {
    public class WebsocketController: IWebsocketController {
        private List webSockets = new List< WebSocket>();
        public void Map(IApplicationBuilder app) {
            app.UseWebSockets();
            app.Use(async (context, next) => {
                if (context.WebSockets.IsWebSocketRequest) {
                    WebSocket newWebSocket = await context.WebSockets.AcceptWebSocketAsync();
                    webSockets.Add(newWebSocket);
                    
                    await Echo(context, newWebSocket);
                }
                else {
                    context.Response.StatusCode = 400;
                }
            });
        }
        public async void SendMessage(Message message) {
            foreach (WebSocket w in webSockets) {
                if (w.State == WebSocketState.Closed) {
                    continue;
                }
                byte[] additionalBytes = System.Text.Encoding.UTF8.GetBytes(message.BodyText);
                await w.SendAsync(new ArraySegment< byte>(additionalBytes, 0, additionalBytes.Length),
                    WebSocketMessageType.Text, true, CancellationToken.None);
            }
        }
        private async Task Echo(HttpContext context, WebSocket webSocket) {
            byte[] buffer = new byte[1024 * 4];

            WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
            
            while (result.CloseStatus.HasValue == false) {
                result = await webSocket.ReceiveAsync(new ArraySegment< byte>(buffer), CancellationToken.None);
            }
            await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
            webSocket.Dispose();

            // 接続を切った後Listから削除するので、対象のインデックスはWebSocketを使って動的に取る.
            int index = webSockets.IndexOf(webSocket);
            if(index >= 0 && index < webSockets.Count) {
                webSockets.RemoveAt(index);
            }
        }
    }
}

コメントにも書きましたが、先ほど無くしていた Echo の引数の WebSocket ですが、接続が切れた WebSocket は List から外していくため RemoveAt を使うためには動的にインデックスを取る必要があります。

このインデックスを IndexOf で取得するためにもう一度 WebSocket を渡すようにしています。

以前 Spring boot で試したときは、 WebSocketSession が Id を持っていたためそれで判断していましたが、今回そのようなデータが取れないようだったため IndexOf を使っています。

で、この IndexOf 、どうやって WebSocket を比較しているのかを知りたかったのですが、これを書いている 2018.08.08 時点ではうまく情報を見つけることができませんでした。

これについては追加で情報が見つけられれば追記します。