以下はASP.Net CoreとMicrosoft.AspNetCore.SignalRで作ったチャットの基礎になるアプリです。接続すると全員に接続した旨と時刻とConnectionIdを表示し、切断した場合も同様の表示をします。チャット機能は省略しています。
Contents
単純に接続と切断だけを通知するサンプルアプリ
Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddSignalR(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.MapHub<App1.Pages.IndexPageHub>("/IndexPageHub"); app.Run(); |
Pages\Index.cshtml.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using Microsoft.AspNetCore.SignalR; namespace App1.Pages { public class IndexPageHub : Hub { public override async Task OnConnectedAsync() { await base.OnConnectedAsync(); string now = DateTime.Now.ToString("H:mm:ss"); string id = Context.ConnectionId; await Clients.Caller.SendAsync("notice", $"<tr><td>接続(自分)</td><td>{now}</td><td>{id}</td></tr>"); await Clients.Others.SendAsync("notice", $"<tr><td>接続</td><td>{now}</td><td>{id}</td></tr>"); } public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); string now = DateTime.Now.ToString("H:mm:ss"); string id = Context.ConnectionId; await Clients.All.SendAsync("notice", $"<tr><td>切断</td><td>{now}</td><td>{id}</td></tr>"); } public void Disconnect() { Context.Abort(); } } } |
Pages\Index.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
@page @{ ViewData["Title"] = "Home page"; Layout = null; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - App1</title> <style> table{ margin-top:10px; } td { border: solid 1px #000; padding:5px; } #container { padding:50px; } </style> </head> <body> <div id="container"> <input type="button" value="接続" onclick="connect()"> <input type="button" value="切断" onclick="disconnect()"> <table id="notice"></table> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <script> let connection = new signalR.HubConnectionBuilder().withUrl("./IndexPageHub").build(); function connect() { connection.start().catch(function (err) { document.getElementById("notice").innerHTML += '<tr>接続失敗</tr>'; }); } function disconnect(){ connection.invoke("Disconnect").catch(function (err) { return console.error(err.toString()); }); } connection.on("notice", function (text) { document.getElementById("notice").innerHTML += text; }); </script> </body> </html> |
これで[接続]ボタンや[切断]ボタンをクリックすると以下のように表示されます。またブラウザを閉じた場合も切断ボタンをクリックしたときと同じように動作します。
サーバー上ではブラウザを閉じた直後にOnDisconnectedAsyncが実行されない
これで完成♪のはずなのですが、これをサーバー上で動作させてみると問題点があることに気づきします。[切断]ボタンをクリックしたときはすぐに「切断 XX:XX:XX XXXXXXXXXXXXXXXXXXXXXX」と表示されるのですが、ブラウザを閉じた場合は切断された表示がされるまでかなりの時間がかかるのです(ローカルでテストした場合はこの問題は発生しない)。
改善案
これを解決する方法として以下があります(ベストプラクティスかどうかわからないのですが……)。
接続したら接続しているあいだ断続的にサーバーサイドからクライアントサイドにデータを送り続け、これに失敗したら切断されたと判断します。この方法だとブラウザを閉じたときもすぐに結果が反映されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
namespace App1.Pages { public class IndexPageHub : Hub { // フィールド変数を追加 static System.Timers.Timer _timer = new System.Timers.Timer(); static Dictionary<string, IClientProxy> _clientProxy = new Dictionary<string, IClientProxy>(); static bool _isFirst = true; public override async Task OnConnectedAsync() { await base.OnConnectedAsync(); string now = DateTime.Now.ToString("H:mm:ss"); string id = Context.ConnectionId; await Clients.Caller.SendAsync("notice", $"<tr><td>接続(自分)</td><td>{now}</td><td>{id}</td></tr>"); await Clients.Others.SendAsync("notice", $"<tr><td>接続</td><td>{now}</td><td>{id}</td></tr>"); // if文を追加 if (_isFirst) { _isFirst = false; _timer.Interval = 100; _timer.Elapsed += Timer_Elapsed; } // 追加:タイマーイベント発生時に全員に送信したいので静的変数にClients.Callerを格納している _clientProxy.Add(Context.ConnectionId, Clients.Caller); // 追加: if (_clientProxy.Count == 1) _timer.Start(); } // 追加:イベントハンドラ private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { foreach (IClientProxy client in _clientProxy.Values) { Task.Run(() => { _ = client.SendAsync("idle"); }); } } public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); _clientProxy.Remove(Context.ConnectionId); string now = DateTime.Now.ToString("H:mm:ss"); string id = Context.ConnectionId; await Clients.All.SendAsync("notice", $"<tr><td>切断</td><td>{now}</td><td>{id}</td></tr>"); // 追加:だれも接続していない場合はタイマーを止める(サーバーへの負荷を考慮) if (_clientProxy.Count == 0) _timer.Stop(); } public void Disconnect() { Context.Abort(); } } } |
通信が切れたことを知る
あと通信が切れた場合、クライアントサイドで気づくことができるようにします。scriptタグ内に以下を追加します。
Pages\Index.cshtml
1 2 3 4 5 |
<script> connection.onclose(async () => { document.getElementById("notice").innerHTML += "<tr>通信が切れました</tr>"; }); </script> |
自動的に再接続する
なんらかの理由で切断されてしまった場合、再接続するのであれば以下のようにします。[一時的に切断(自動再接続)]ボタンをクリックした場合は一時的に切断されますが、3秒後に再接続を試みます。もし再接続できない場合はあきらめます。
Pages\Index.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<div id="container"> <input type="button" value="接続" onclick="connect()"> <input type="button" value="切断" onclick="disconnect()"> <input type="button" value="一時的に切断(自動再接続)" onclick="disconnect2()"> <table id="notice"></table> </div> <script> // 自分の意思で切断した場合は再接続しない let dont_connect = false; function disconnect() { dont_connect = true; connection.invoke("Disconnect").catch(function (err) { return console.error(err.toString()); }); } function disconnect2() { connection.invoke("Disconnect").catch(function (err) { return console.error(err.toString()); }); } connection.onclose(async () => { document.getElementById("notice").innerHTML += "<tr>通信が切れました</tr>"; if(dont_connect) return; setTimeout(function () { connection.start().catch(function (err) { document.getElementById("notice").innerHTML += '<tr>再接続失敗</tr>'; }); }, 3000); }); </script> |
再接続者の同一性の確認
再接続した場合、ConnectionIdも変わってしまいます。再接続できた場合、それが同一者であることを確認する手段を用意しなければなりません。
一例として接続に成功したらクライアントサイドにConnectionIdを送信します。クライアントサイドではこれを保存しておきます。再接続に成功したら新しいConnectionIdが送信されますが、このとき古いConnectionIdと新しいConnectionIdをサーバーサイドに送り返すことで同一性が認識できます。
OnConnectedAsyncメソッド内に1行追加します。そしてクライアントサイドからSendIdToServerが呼び出されるので、そのメソッドを定義します。それ以外の変更点はありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
namespace App1.Pages { public class IndexPageHub : Hub { public override async Task OnConnectedAsync() { await base.OnConnectedAsync(); string now = DateTime.Now.ToString("H:mm:ss"); string id = Context.ConnectionId; await Clients.Caller.SendAsync("notice", $"<tr><td>接続(自分)</td><td>{now}</td><td>{id}</td></tr>"); await Clients.Others.SendAsync("notice", $"<tr><td>接続</td><td>{now}</td><td>{id}</td></tr>"); await Clients.Caller.SendAsync("SendIdToClient", id); // 追加 if (_isFirst) { _isFirst = false; _timer.Interval = 100; _timer.Elapsed += Timer_Elapsed; } _clientProxy.Add(Context.ConnectionId, Clients.Caller); if (_clientProxy.Count == 1) _timer.Start(); } public void SendIdToServer(string oldConnectionId, string newConnectionId) { string now = DateTime.Now.ToString("H:mm:ss"); string str = $"(新){newConnectionId} (旧){oldConnectionId}"; if (oldConnectionId != "") { _ = Clients.All.SendAsync("notice", $"<tr><td>通知</td><td>{now}</td><td>{str}</td></tr>"); } } } } |
Pages\Index.cshtml
グローバル変数 oldConnectionIdとnewConnectionIdを用意します。function connect()を修正するとともに、サーバーサイドから”SendIdToClient”が送信されたときの処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<script> let oldConnectionId = ""; let newConnectionId = ""; // 修正 // function connect() { // connection.start().catch(function (err) { // document.getElementById("notice").innerHTML += '<tr>接続失敗</tr>'; // }); // } function connect() { connection.start() .then(function () { // 本当に切断したあと再接続するときは変数をリセットする oldConnectionId = ""; newConnectionId = ""; }) .catch(function (err) { document.getElementById("notice").innerHTML += '<tr><td>接続失敗</td></tr>'; } ); } // 保存されているものと新しいものを入れ替える connection.on("SendIdToClient", function (id) { oldConnectionId = newConnectionId; newConnectionId = id; connection.invoke("SendIdToServer", oldConnectionId, newConnectionId); }); |