双方向通信大事。 というわけでASP.NET CoreでWebSocketを使ったチャットのサンプル作りました。
でなんでこんなことやっているかというと、msのドキュメントに書いてあるあるやつ だと自分が送信したものに対して自分に返ってくるだけで双方向通信生かされる感じしなかったから(表面的にはjsちょっと書けばできる感じがつまらない)。あと既存のasp.net coreでwebsocket使っているやつのサンプルあったりするのだけど、いまmasterの動くやつは実装が重厚だったり、ブログ用のサンプルはちょっと古かったりするので書きました。今回のサンプルは基本的には上記のものを弄った感じです。
あとASP.NET Coreで双方向通信する時、一番最初にSignalRが出てくるけど、これ内部的にはWebSocketとか使っていることもあるので(WebSocketとは限らない)、まぁ一度生のWebSocket叩いてみるのがいいでしょう。というモチベ。
サンプル
完成品はこちら。とりあえず動くと思います。 使い方はサーバー立てたらhttps://localhost:port/chat.htmlにアクセスすると今回のでもが動きます。複数タブで開いてみていろいろ送ると良いでしょう。
パッケージ
WebSocketのパッケージをadd packageをしておきましょう。
ソース
ソースすべて解説すると大変なのとそこまで複雑ではないので掻い摘んで解説します。WebSocketChatというディレクトリの中に入ってます。
WebSocketObjectHolder
このソースはwss://で接続が要求されたらWebSocketオブジェクトを保持,削除などをするクラスです。
WebSocketHandler
抽象クラス。これを継承したクラスはDIコンテナにシングルトンとして登録され、ASP.NET Coreのミドルウェアで使用します。
ChatMessageHandler
上記のWebSocketHandlerを継承していて、接続時/受信時/切断時の処理が書かれている。
WebSocketChatMiddleware
ASP.NET Coreのミドルウェアを自作します。 https接続ではなく、wss(WebSocket)から通信が要求された場合ここの処理が挟まれるようにします。22行目でIsWebSocketRequestをつかってそれを判断します。 その後はcontext.WebSockets.AcceptWebSocketAsync()でソケット実体を作り、ハンドラに渡します。その後はReceive()でWebSocketの接続が切断されるまで待ち受けます。
while (webSocket.State == WebSocketState.Open) { var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); handleMessage(result, buffer); }
こんな待ち受け方してええんか、と思うかもしれません。クライアント側でC#を書いてきているとasync/await、多くがメインスレッドを固めないため、Task.Run()で別のスレッドでほげほげ処理をしてから返ってくる、的な使い方が多い気がします。 そのノリだと上記のような書き方だと一つスレッド食いつぶしているので沢山接続あった場合死んでしまうやん、という感じがしますが実際にはそうなりません。詳しくはASP.NET CoreなどのサーバーサイドC#におけるasync/awaitについて。 に書いたので気になる方は読んでみてください。
WebSocketChatExtensions
DIコンテナにWebSocketHandlerを継承しているものをシングルトンとして登録する関数と、上記のWebSocketChatMiddlewareを対応するパスにMapする関数があります。
リクエストのパイプラインにMapWebSocketChatMiddleware関数を挟むことで、対応するpathに対してリクエストが飛んできた時にミドルウェアに処理を投げるような感じになっております。
wwwroot/chat.html
WebSocketのオブジェクトを作って、wss://でエンドポイントに投げて接続、あとは送信、受信しているだけ。受信した際にdom操作してulにペチペチとアイテムを一つづつけている。
以上でだいたい説明終わりです。
ブラウザが閉じられた時
ブラウザ開いているひとがわざわざ自分でdisconnect叩かないでもブラウザを閉じた時に勝手にWebSocketMessageType.Closeを飛ばしてくれるのでクライアント側が閉じられているのにサーバー側でずっと待ちぼうけ、みたいなことにはなりません。素晴らしい。
注意点
httpsだと、WebSocketで通信する際、ws://だとだめで、wss://で通信を開始しないといけません。chat.htmlのWebSocketの通信のエンドポイントを設定しているところでwsにしてみると通信できなくなるのがわかると思います。
まとめ
比較的簡単にWebSocketそのものをたたくことが出来る。
疑問
フレームワークそのものの設計に関する疑問なのだが、なぜMiddlewareはリフレクションでInvoke()を探すような実装にしたのだろうか。。。 以下がInvoke探しているコードだったりするのですが、
MethodInfo[] array = ((IEnumerable<MethodInfo>) middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public)).Where<MethodInfo>;((Func<MethodInfo, bool>) (m =>; { if (!string.Equals(m.Name, "Invoke", StringComparison.Ordinal)) return string.Equals(m.Name, "InvokeAsync", StringComparison.Ordinal); return true; })).ToArray<MethodInfo>();
なにかしらInterfaceでも実装させるなりした方がいろいろ丸い気がするのだけどなぁ。用途わかりやすいし。どうしてこうなったのか。
参考
https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.2 http://sourcechord.hatenablog.com/entry/2016/11/02/123327 https://radu-matei.com/blog/aspnet-core-websockets-middleware/