vaguely

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

ASP.NET CoreでWebSocket

はじめに

以前 Spring boot で WebSocket を使う、てなことをやりましたが、今回は ASP.NET Core を使ってやってみます。

なぜ急に WebSocket か? お察しください

なおリアルタイムで処理を行う機能としては ASP.NET SignalR というものがあって、 ASP.NET Core では ver.2.1 から使えるとのことなので、こちらも近いタイミングで試してみたいと思います。

なおクライアント側は下記と同じく websocket-sharp を使うことにします。

http://mslgt.hatenablog.com/entry/2017/05/11/073758

インストール

ASP.NET Core では Microsoft による WebSocket パッケージがありますのでそれを使用します。

NuGet で Microsoft.AspNetCore.WebSockets パッケージをインストールすれば OK です。

簡単ですね。

サンプルコードを試してみる

まずはサンプルコードを試してみることにします。

docs.microsoft.com

github.com

↑を試してみると、確かに WebSocket で接続することができました。

ただ、(サンプルコードとしては素晴らしいのですが) Startup クラスが少々長くなってしまうのが気になりました。

Configure() の中身を見てみると、関連するコードは(オプションを除くと)以下のようでした。

Startup.cs

~省略~
  public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
    ~省略~
    app.UseWebSockets();
    ~省略~
    app.Use(async (context, next) => {
        if (context.Request.Path == "/ws") {
            if (context.WebSockets.IsWebSocketRequest) {
                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
                await Echo(context, webSocket);
            }
            else {
                context.Response.StatusCode = 400;
            }
        }
        else {
            await next();
        }
    });
  }
~省略~

WebSocket を有効にする UseWebSockets() はともかく、接続するところは Controller クラスでもできるんじゃね?

と思ってコピペしたらエラーになりました。

ですよね~/(^o^)\

HomeController.cs

~省略~
  [Route("ws")]
  public async void RouteWebsocketAccess() {
      if (this.HttpContext.WebSockets.IsWebSocketRequest) {
          WebSocket webSocket = await this.HttpContext.WebSockets.AcceptWebSocketAsync();
          await Echo(this.HttpContext,  await this.HttpContext.WebSockets.AcceptWebSocketAsync());
      }
      else {
          this.HttpContext.Response.StatusCode = 400;
      }
  }
~省略~

WebSocket のリクエストかどうかを確認する IsWebSocketRequest は正しく動作していましたが、実際にデータの送受信をする Echo の部分でエラーが発生していました。

The remote party closed the WebSocket connection without completing the close handshake.

また、 WebSocket のリクエストではなかった場合の、 this.HttpContext.Response.StatusCode も正しく動作していませんでした。
(Startup.cs で実行した場合は前回の 404 と同じように処理されます)

Controller クラスではできないのか、見様見真似で書いたものの間違いがあってできないのかはわからないのですが、とりあえずこのままではダメ、ということがわかりました。

とりあえず今回は別の方法を選ぶことにします。

app.Use()とapp.Map()

tamafuyou.hatenablog.com

この記事を見てみると、 Startup では app.Map() を使ってマッピングを行い、先ほどの app.Use() の部分をそのまま chatServer.Map に移していました。

 app.Map("/ws", chatServer.Map);

では、この app.Map() や app.Use() って何なのでしょうか。

というのを少しだけ見てみます。

docs.microsoft.com

まだわかるようなわからないようなというところですが、

そもそもまずこの Configure というメソッドは、

  1. HTTP アクセスがあった場合に毎回呼び出される
  2. DB 、ログ出力、ルーティングなどを行うミドルウェアを利用している場合に、それらの処理を呼び出す

というものであり、 app.Use() は 2. を実行するためのもの、ということのようです。

ルーティングのようにデフォルトで用意されているものは、 app.UseMvc() のようなメソッドが用意されているのでそれを使う、と。

で、 app.Map() というのは特定のパスに対して処理を割り当てるもの、ということのようです。

このミドルウェアは複数扱うことができ、 app.Use() などの中から app.Next() を呼ぶと次のミドルウェアの処理に移すことができます。

先ほどの WebSocket の処理で、 URL が ~/ws ではなかった場合に app.Next() を呼び忘れると該当のページが表示されないため注意が必要です。

今回登場しませんでしたが、 app.Run() を実行すると処理がそこで終わってしまうため、最後に実行する必要があります。

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
  if (env.IsDevelopment()) {
      app.UseBrowserLink();
      app.UseDeveloperExceptionPage();
  }
  app.Run(async (context) => {
    await Task.Run(() => {
          Console.WriteLine("hello goodbye");
    });
  });
  // ここから先の処理が実行されない/(^o^)\.
  app.UseStaticFiles();

  ~省略~

}

WebSocketを使うコードを見る

ようやく今回のテーマである(寄り道しすぎ)、 WebSocket を利用するコードを見てみることにします。

先ほどから登場しまくりである下記のサンプルコードを参考に(ほぼコピペ)、 WebSocket で接続し、メッセージを受信したら接続先に返す、という内容となっています。

ASP.NET Core での Websocket のサポート | Microsoft Docs

ASP.NET Coreで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;
                }
            });
            // app.Map("/ws", websocket.Map); でマッピングしてるので他のパスに対する処理はしない.
        }

        // 引数の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);
        }
    }
}

接続は Map() の context.WebSockets.AcceptWebSocketAsync() で行われて、メッセージの送受信は Echo() で行われるようですね。

メッセージの送受信

先ほどのコードで触れていなかったメッセージの送受信について追いかけてみます。

メッセージの受信

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

送信の引数にもなっていますが、 ArraySegment は配列の一部を持つもので、渡している buffer( byte の配列)にはクライアントから送信されたデータが格納されます。

第二引数には処理をキャンセルする必要がある場合にその通知を渡すもの( CancellationToken )ということなのですが、空で渡しています。

送受信をキャンセルするようなこともできる、ということなのでしょうか。

戻り値となる WebSocketReceiveResult は、受信したデータそのものは持っておらず、受信データの型( MessageType )や接続が Close されたか( CloseStatus )などの情報を持っています。

試しに下記のようにログを出してみました。

WebSocketController.cs

 byte[] buffer = new byte[1024 * 4];

Console.WriteLine("before bLength " + buffer.Length + " b0 " + buffer[0]);

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

Console.WriteLine("bLength " + buffer.Length + " b0 " + buffer[0] + " rCount " + result.Count + " rEnd " + result.EndOfMessage + " rType " + result.MessageType);

while (result.CloseStatus.HasValue == false) {
    await webSocket.SendAsync(new ArraySegment< byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

    Console.WriteLine("while bLength " + buffer.Length + " b0 " + buffer[0] + " rCount " + result.Count + " rEnd " + result.EndOfMessage + " rType " + result.MessageType);

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

で、クライアントから「世界さん、チーッす!」という文字列を1回送信したところ、結果は下記のようになりました。

before bLength 4096 b0 0
bLength 4096 b0 228 rCount 30 rEnd True rType Text
while bLength 4096 b0 228 rCount 30 rEnd True rType Text

result.Count

result.Count で取得できる 30 という数値。

これは、 buffer にあらかじめ設定している 4096 という要素数のうち、実際にデータが格納されている(つまり値が 0 でない)要素数を示しているようです。

メッセージの送信

await webSocket.SendAsync(new ArraySegment< byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

送信側も引数は受信とあまり変わらない(今回は受け取ったデータをそのまま渡しているため)ようです。

buffer

0 から buffer の中のデータが格納された要素数分を保持する ArraySegment を渡しています。

result.EndOfMessage

( buffer の要素数などの静減から)メッセージを複数に分割して送信したい場合は false を渡してやれば良さそうです。

サンプルでは受信したデータをそのまま送信していますが、例えば固定で文字列を追加して送信したい場合、下記のようにすれば実現できます。

// 追加する文字列(byteの配列として持つ).
byte[] additionalBytes = System.Text.Encoding.UTF8.GetBytes("hello: ");
// 追加する文字列を受信したデータとマージする.
byte[] newBytes = new byte[additionalBytes.Length + result.Count];
additionalBytes.CopyTo(newBytes, 0);
ArraySegment< byte> receivedDataSegment = new ArraySegment< byte>(buffer, 0, result.Count);
receivedDataSegment.CopyTo(newBytes, additionalBytes.Length);

// データを送信する.
await webSocket.SendAsync(newBytes, result.MessageType, result.EndOfMessage, CancellationToken.None);    

配列のマージについてはもう少しうまいやり方があるかもしれませんが。

おわりに

今回はひたすらサンプルを追っかける回になってしまいました。

今回に限らずですが、サンプルコードがちゃんと動くってのはありがたいですねぇ(..)_

もうちょっとだけ続くんじゃ、ということで、次回はもう少しこれをベースに遊んでみたいと思います。

参照

ArraySegment