ASP.NET Core で WebSocket を使う

とあるコンテストで WebSocket を使うサーバープログラムが必要になったので、 ASP.NET Core ではどう作るのかを調べてみました。 SignalR を使うプログラムではないです。全体のソースコード

準備

まずは Visual Studio 2015 で “ASP.NET Core Web Application” からプロジェクトを作成します。今回は空のテンプレートを選びました。

次に、 WebSocket に関する ASP.NET Core のミドルウェア Microsoft.AspNetCore.WebSockets.Server を NuGet からインストールします。

最後に、 WebSocket のミドルウェアを使うことが確認できれば準備完了です。

34
35
var options = new WebSocketOptions { ReceiveBufferSize = 4096 };
app.UseWebSockets(options);

WebSocket の接続開始リクエストは下のように受け取ります。そうでなかった場合は await next() で下流のパイプラインに処理を流します(表現がムズカシイ)。

42
43
44
45
46
47
48
49
50
51
52
app.Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
// process...
}
else
{
await next();
}
});

使うメソッドと注意点

接続開始リクエストが来たら、普通はリクエストを受け付けて接続を開始すると思います。

1
2
3
4
5
6
7
8
9
10
11
12
if (context.WebSockets.IsWebSocketRequest)
{
// WebSocket request
using (var socket = await context.WebSockets.AcceptWebSocketAsync())
{
if (socket?.State == WebSocketState.Open)
{
// Handle the socket
// ....
}
}
}

WebSocket のやり取りは受信と送信の繰り返しと終了処理となります。それぞれ下のように書きます。 CancellationToken も必要ならしっかり設定したいところ。なお、 byte[] でのやり取りになるので、文字列などはしっかり変換しなければなりません。 Encoding.UTF8.GetString()Encoding.UTF8.GetBytes() を利用しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 受信
var receiveToken = CancellationToken.None;
var receiveBuffer = new ArraySegment<byte>(new byte[4096]);
var received = await socket.ReceiveAsync(receiveBuffer, receiveToken);

// 送信
var sendingToken = CancellationToken.None;
var sendingData = Encoding.UTF8.GetBytes("hoge");
var sendingBuffer = new ArraySegment<byte>(sendingData);
await socket.SendAsync(sendingBuffer, WebSocketMessageType.Text, true, sendingToken);

// 終了
var closeToken = CancellationToken.None;
await socket.CloseAsync(received.CloseStatus.Value, received.CloseStatusDescription, closeToken);

必要なメソッドは揃いましたが、 JavaScript ( Node.js ) での WebSocket の感覚で書いてはいけません。(下は擬似コードですが、これで永遠にオウム返しのやりとりができるはずです。)

1
2
3
4
5
server.on('connection', function(socket) {
socket.on('message', function(data) {
socket.send(data);
});
});

これに似せたコードが Startup.cs の 60 行目から 94 行目ですが、こうすると 1 度送信して同じメッセージが返ってきたあとは無反応になります。

ASP.NET Core での WebSocket に関して誤解を恐れずに言えば、最初の方に出たリクエストというのは、コネクションの確立のためのリクエストであって、メッセージの受け渡しに関するリクエストではないということです。

接続を維持して通信するためには、永久ループの形をとり、終了時にループを出る構造にすると良いと思います。(受信待ち時は await で止まっているので暴走の心配はない)

1
2
3
4
5
6
7
8
9
10
11
var received = await socket.ReceiveAsync(receiveBuffer, receiveToken);

// Binary -> fail, Close -> close connection
while (received.MessageType == WebSocketMessageType.Text)
{
await socket.SendAsync(sendingBuffer, WebSocketMessageType.Text, true, sendingToken);

received = await socket.ReceiveAsync(receiveBuffer, receiveToken);
}

await socket.CloseAsync(received.CloseStatus.Value, received.CloseStatusDescription, closeToken);

Startup.cs の 101 行目から 142 行目にそのままありますが、形を抜き出すと上の感じです。繰り返しになりますが、ループによってやり取りを繰り返し、終了の合図でループを出て終了処理を行います。今回はテキストデータだけを受送信する仕様にしているので種類が Text であるかを確かめていますが、バイナリデータも受信する場合は種類がCloseでないことをwhileの条件にすれば良いはずです。

ブロードキャスト

今のままでは、よくあるチャットプログラムの例のようには全員宛のメッセージが送れないので、接続中のクライアント全部に送ることができるよう拡張します。

具体的には Startup.cs の 151 行目から 197 行目に書いてありますが、コレクションを用意して接続されたものを追加しておくという、特にサプライズの無いコードを追加すれば OK です。そして、送る時にはコレクションの中身を列挙してそれぞれ SendAsync() メソッドを実行すればよいのです。まだ詳しくわかりませんが、 ConcurrentBag クラスはスレッドセーフなコレクションらしいです。

1
2
3
// send message to all connections
await Task.WhenAll(allSockets.Where(x => x?.State == WebSocketState.Open)
.Select(x => x.SendAsync(sendingBuffer, WebSocketMessageType.Text, true, sendingToken)));

ちなみに Azure にデプロイして WebSocket のサーバーを動かす際には、ポータル上の設定で WebSocket の使用をオンにしないと、ポートが開かないので接続できないはず。

参照記事