2017-06-13 23 views
6

私は、サーバー(MVC Core Webアプリケーション)で何らかのイベントが発生したときにWebソケットプロトコルを使用してクライアント(Webブラウザ)に通知するソリューションを開発中です。私はMicrosoft.AspNetCore.WebSocketsナゲットを使用します。このビューは、ソケット要求をロードされている MVCコア、Webソケットとスレッド

$(function() { 
    var socket = new WebSocket("ws://localhost:61019/data/openSocket"); 

    socket.onopen = function() { 
     $(".socket-status").css("color", "green"); 
    } 

    socket.onmessage = function (message) { 
     $("body").append(document.createTextNode(message.data)); 
    } 

    socket.onclose = function() { 
     $(".socket-status").css("color", "red"); 
    } 
    }); 

はすぐにMVC Coreアプリケーションに送信されます。

は、ここに私のクライアント側のコードです。コントローラーアクションは次のとおりです。

[Route("data")] 
public class DataController : Controller 
{ 
    [Route("openSocket")] 
    [HttpGet] 
    public ActionResult OpenSocket() 
    { 
     if (HttpContext.WebSockets.IsWebSocketRequest) 
     { 
      WebSocket socket = HttpContext.WebSockets.AcceptWebSocketAsync().Result; 

      if (socket != null && socket.State == WebSocketState.Open) 
      { 
       while (!HttpContext.RequestAborted.IsCancellationRequested) 
       { 
        var response = string.Format("Hello! Time {0}", System.DateTime.Now.ToString()); 
        var bytes = System.Text.Encoding.UTF8.GetBytes(response); 

        Task.Run(() => socket.SendAsync(new System.ArraySegment<byte>(bytes), 
         WebSocketMessageType.Text, true, CancellationToken.None)); 
        Thread.Sleep(3000); 
       } 
      } 
     } 
     return new StatusCodeResult(101); 
    } 
} 

このコードは非常にうまくいきます。ここのWebSocketは送信専用に使用され、何も受信しません。ただし、問題は、キャンセル要求が検出されるまで、WhileループがDataControllerスレッドを保持し続けることです。

ここでWebソケットはHttpContextオブジェクトにバインドされています。 WebリクエストのHttpContextが破棄されるとすぐに、ソケット接続は直ちに閉じられます。

質問1:コントローラスレッド外でソケットを保存できる方法はありますか? メインアプリケーションスレッドで動作しているMVCコアスタートアップクラスに存在するシングルトンに入れようとしました。 whileループでコントローラスレッドを保持し続けるのではなく、ソケットを開いたままにしたり、メインアプリケーションスレッド内から接続を再確立する方法はありますか? ソケット接続用のコントローラスレッドを開いたままにしておくことがOKであるとみなされても、OpenSocketのwhileループの中に入れる良いコードはないと思います。コントローラーで手動リセットイベントを実行し、OpenSocketアクション内のwhileループ内でそのイベントがセットされるのを待つのはどう思いますか?

質問2:MVCでHttpContextオブジェクトとWebSocketオブジェクトを分離できない場合、ソケット接続の再利用を実現するために、他にどのような代替技術や開発パターンを利用できますか? SignalRやそれに類するライブラリがHttpContextから独立したソケットを持つことを可能にするいくつかのコードを持っていると誰かが考えているなら、いくつかのコード例を教えてください。 MVCに独立したソケット通信を処理する機能がない場合、この特定のシナリオでMVCに代わるより優れた方法があると考える人は、例を挙げてください。純粋なASP.NETまたはWeb APIに切り替えるのは気にしません。

質問3:明示的なタイムアウトまたはユーザーによるキャンセル要求まで、ソケット接続を有効にしておくか、再接続できるようにする必要があります。考えられるのは、確立されたソケットからデータを送信するトリガーとなる独立したイベントがサーバー上で発生するということです。 Webソケット以外のいくつかの技術(HTML/2やストリーミングなど)がこのシナリオでもっと役立つと思われる場合は、使用するパターンやフレームワークを記述できますか?

P.S.可能な解決策は、毎秒AJAX要求を送信してサーバーに新しいデータがあるかどうかを尋ねることです。これが最後の手段です。

+0

'SignalR'は非常に有望ですが、多くのことを知らないのでコードを提供することはできません。 – VMAtm

答えて

4

長年の研究の末、私はカスタムミドルウェアソリューションを手に入れました。ここに私のミドルウェアクラスがあります:

 public class SocketMiddleware 
    { 
     private static ConcurrentDictionary<string, SocketMiddleware> _activeConnections = new ConcurrentDictionary<string, SocketMiddleware>(); 
     private string _packet; 

     private ManualResetEvent _send = new ManualResetEvent(false); 
     private ManualResetEvent _exit = new ManualResetEvent(false); 
     private readonly RequestDelegate _next; 

     public SocketMiddleware(RequestDelegate next) 
     { 
      _next = next; 
     } 

     public void Send(string data) 
     { 
      _packet = data; 
      _send.Set(); 
     } 

     public async Task Invoke(HttpContext context) 
     { 
      if (context.WebSockets.IsWebSocketRequest) 
      {  
       string connectionName = context.Request.Query["connectionName"]); 
       if (!_activeConnections.Any(ac => ac.Key == connectionName)) 
       { 
        WebSocket socket = await context.WebSockets.AcceptWebSocketAsync(); 
        if (socket == null || socket.State != WebSocketState.Open) 
        { 
         await _next.Invoke(context); 
         return; 
        } 
        Thread sender = new Thread(() => StartSending(socket)); 
        sender.Start(); 

        if (!_activeConnections.TryAdd(connectionName, this)) 
        { 
         _exit.Set(); 
         await _next.Invoke(context); 
         return; 
        } 

        while (true) 
        { 
         WebSocketReceiveResult result = socket.ReceiveAsync(new ArraySegment<byte>(new byte[1]), CancellationToken.None).Result; 
         if (result.CloseStatus.HasValue) 
         { 
          _exit.Set(); 
          break; 
         } 
        } 

        SocketHandler dummy; 
        _activeConnections.TryRemove(key, out dummy); 
       } 
      } 

      await _next.Invoke(context); 

      string data = context.Items["Data"] as string; 
      if (!string.IsNullOrEmpty(data)) 
      { 
       string name = context.Items["ConnectionName"] as string; 
       SocketMiddleware connection = _activeConnections.Where(ac => ac.Key == name)?.Single().Value; 
       if (connection != null) 
       { 
        connection.Send(data); 
       } 
      } 
     } 

     private void StartSending(WebSocket socket) 
     { 
      WaitHandle[] events = new WaitHandle[] { _send, _exit }; 
      while (true) 
      { 
       if (WaitHandle.WaitAny(events) == 1) 
       { 
        break; 
       } 

       if (!string.IsNullOrEmpty(_packet)) 
       { 
        SendPacket(socket, _packet); 
       } 
       _send.Reset(); 
      } 
     } 

     private void SendPacket(WebSocket socket, string packet) 
     { 
      byte[] buffer = Encoding.UTF8.GetBytes(packet); 
      ArraySegment<byte> segment = new ArraySegment<byte>(buffer); 
      Task.Run(() => socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None)); 
     } 
    } 

このミドルウェアはすべてのリクエストで実行されます。 Invokeが呼び出されると、Webソケット要求かどうかがチェックされます。そうである場合、ミドルウェアは、そのような接続がすでに開かれているかどうかをチェックし、そうでない場合は、ハンドシェイクを受け入れ、ミドルウェアがそれを接続の辞書に追加する。辞書が静的なので、アプリケーションの存続期間中に1回だけ作成されることが重要です。

ここで停止してパイプラインを上げると、HttpContextが最終的に破棄され、ソケットが正しくカプセル化されていないため、HttpContextも閉じられます。したがって、ミドルウェアスレッドを実行したままにしておく必要があります。これは、ソケットに何らかのデータを受け取るように要求することによって行われます。

送信する必要がある場合、何かを受け取る必要があるのはなぜですか?答えは、確実にクライアントの切断を確実に検出する唯一の方法です。 HttpContext.RequestAborted.IsCancellationRequestedは、whileループ内で常時送信する場合にのみ機能します。 WaitHandleでサーバーイベントを待つ必要がある場合、キャンセルフラグは決してtrueではありません。私は終了イベントとしてHttpContext.RequestAborted.WaitHandleを待つことを試みましたが、決して設定されません。だから、ソケットに何かを受け取るように依頼し、何かがCloseStatus.HasValueをtrueに設定すると、クライアントが切断されたことがわかります。何か他のものを受け取った場合(クライアント側のコードは安全ではありません)、無視して再度受信を開始します。

送信は別のスレッドで行われます。理由は同じです。メインのミドルウェアスレッドを待つと、切断を検出できません。クライアントが切断したことを送信者スレッドに通知するには、_exit同期変数を使用します。 SocketMiddlewareインスタンスは静的コンテナに保存されているので、ここでプライベートメンバーを持つことは忘れないでください。

今、この設定で実際に何かを送信しますか?サーバー上でイベントが発生し、一部のデータが利用可能になるとします。簡単にするために、このデータが通常のhttp要求の中に到着したものと見なして、何らかのコントローラの動作に使用するものとします。 SocketMiddlewareは、リクエストごとに実行されますが、それはウェブソケット要求ではないので、_next.Invoke(コンテキスト)が呼び出され、要求がこのような何かを見てもコントローラのアクションに到達している:

[Route("ProvideData")] 
[HttpGet] 
public ActionResult ProvideData(string data, string connectionName) 
{ 
    if (!string.IsNullOrEmpty(data) && !string.IsNullOrEmpty(connectionName)) 
    { 
     HttpContext.Items.Add("ConnectionName", connectionName); 
     HttpContext.Items.Add("Data", data); 
    } 
     return Ok(); 
} 

コントローラがあるアイテムのコレクションを移入しますコンポーネント間でデータを共有するために使用されます。その後、パイプラインは再びSocketMiddlewareに戻ります。そこでは、コンテキスト内で興味深いものがあるかどうかをチェックします。ディクショナリからそれぞれの接続を選択し、データ文字列を設定して_sendイベントを設定し、送信スレッド内でwhileループを1回実行できるSend()メソッドを呼び出します。

また、サーバー側のイベントを送信するソケット接続があります。この例は非常に原始的なものであり、概念を説明するためだけにあります。もちろん、このミドルウェアを使用するために、あなたはMVCを追加する前に、スタートアップクラスに以下の行を追加する必要があります。

app.UseWebSockets(); 
app.UseMiddleware<SocketMiddleware>(); 

コードは非常に奇妙で、うまくいけば、我々はSignalRのために非常に良く何かを書くことができるようになります最終的にdotnetcoreが終了しました。うまくいけば、この例は誰かにとって役に立ちます。コメントや提案は大歓迎です。