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のミドルウェアを使うことが確認できれば準備完了です。

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

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

app.Use(async (context, next) =>
{
    if (context.WebSockets.IsWebSocketRequest)
    {
        // process...
    }
    else
    {
        await next();
    }
});

使うメソッドと注意点

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

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()を利用しましょう。

// 受信
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の感覚で書いてはいけません。(下は擬似コードですが、これで永遠にオウム返しのやりとりができるはずです。)

server.on('connection', function(socket) {
    socket.on('message', function(data) {
        socket.send(data);
    });
});

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

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

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

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クラスはスレッドセーフなコレクションらしいです。

// 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の使用をオンにしないと、ポートが開かないので接続できないはず。

参考記事

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です