ランキングを偽装させないゲームをつくるためにはクライアントサイドでは描画処理のみをおこなうようにして点数管理などの処理はサーバーサイドで管理するようにするしかなさそうです。これはクライアントサイドとサーバーサイドでデータをやりとりしなければならないことを意味します。そこで今回はゲームをつくるための準備として簡単なチャットを作成します。
タイトルとメッセージを入力してからSend Messageボタンを押すとタイトルとメッセージが表示されます。また別のタブで同じページを開いて同じようにメッセージをおくるとこれまで開いていたタブでも新しいメッセージを確認することができます。ただし後から開いたタブではこれまでに投稿された内容をみることはできません。また保存機能はありません。なので最初に開いたときは投稿内容はなにもありません。
Contents
SignalRを使って簡易チャットをつくってみる
チュートリアル: ASP.NET Core SignalR の概要 JavaScript による SignalRを見ながら作業をすることにしました。
上記のページに書かれている方法でSignalR クライアント ライブラリを追加します。
[Choose specific files](特定のファイルの選択) を選択して dist/browser フォルダーを展開し、signalr.js と signalr.min.js を選択する方法だと取得されたJavaScriptのファイルの階層がずいぶん深くなります。また一度取得したファイルを使い回すことはできないのでしょうか?
実際にやってみましたが問題ありません。上記のページに書かれている方法にともなって別のところにファイルができたり、既存のファイルが書き換えられることはなさそうです(実際にはlibman.jsonというファイルが生成されているが…)。
ChatHubクラスを作成する
次にChatHubクラスを作成するように書かれていますが、ファイル名はこれでなくてもかまいません。少しずつ変えてみて、どこは変更してもいいのか、どの部分は勝手に名前を変更してはいけないのかなどを試行錯誤してみました。鳩でもわかるC#管理人はChatHubTest1というクラス名で作成してみました。実際クラス名にあるようにテストだし、これから何回テストするかわかりません。1回目のテストです。
OnConnectedAsyncメソッドはハブとの新しい接続が確立されると呼び出されます。またOnDisconnectedAsyncメソッドは接続状態を切断したとき、ブラウザタグを閉じたり、ページをリロードしたり、GoBackしたときに呼び出されます。
SendMessageメソッドは接続されたときや切断されたとき、ユーザーが送信ボタンを押してメッセージを送信したデータを受け取ったときに、データをすべてのユーザーに送り返すときに呼び出されます。
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 |
namespace SignalRChat.Hubs { public class ChatHubTest1 : Hub { public override async Task OnConnectedAsync() { await Clients.Others.SendAsync("ReceiveMessage", "新しいユーザー", Context.ConnectionId + "が接続しました", GetDateText()); await Clients.Caller.SendAsync("ReceiveMessage", "接続成功", "あなたの ConnectionId は " + Context.ConnectionId + "です", GetDateText()); await Groups.AddToGroupAsync(Context.ConnectionId, GroupName); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { await Clients.All.SendAsync("ReceiveMessage", "切断", Context.ConnectionId + "が離脱しました", GetDateText()); await Groups.RemoveFromGroupAsync(Context.ConnectionId, GroupName); await base.OnDisconnectedAsync(exception); } // 日付を取得する string GetDateText() { DateTime now = System.DateTime.Now; return String.Format("{0:0000}-{1:00}-{2:00} {3:00}:{4:00}:{5:00}", now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); } // maxlengthを超える文字列の場合、長さをmaxlengthにする string GetSubstring(string str, int maxlength) { if (str.Length > maxlength) return str.Substring(0, maxlength); else return str; } public async Task SendMessage(string title, string message) { // 長大なデータが送りつけられるかもしれないので対策 title = GetSubstring(title, 32); message = GetSubstring(message, 128); // 引数が空文字列だったときはデフォルトの文字列にする if (title == "") title = "名無しさん"; title = title.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'").Replace("\"", """); if (message == "") message = "何も語らず"; // エスケープ処理 title = title.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'").Replace("\"", """); message = message.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'").Replace("\"", """); await Clients.All.SendAsync("ReceiveMessage", title, message, GetDateText()); } } } |
Program.csの変更
SignalR 要求が SignalR に渡されるように SignalR サーバーを構成します。Program.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 |
using SignalRChat.Hubs; // 追加 var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddSignalR(); // 追加 var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.MapHub<ChatHubTest1>("/chatHubFirst"); // 追加 クラス名を変更している /chatHubもこの名前でなくてもよいが後述するJavaScriptのコードと一致させなければならない app.Run(); |
レイアウトを作成する
次にクライアントサイドの処理を示したいのですが、その前にNET 6.0をエックスサーバーにインストールするで作成したサンプルページがあまりにも雑なのでレイアウトを整えます。そのためにプロジェクトのフォルダのなかにあるPages\Sharedフォルダのなかに_Layout2.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 |
@{ string baseurl = Global.BaseUrl; } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - 鳩でもわかるASP.NET Core</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script> <style> body { padding: 80px; } h1 { font-size:1.5em; } </style> </head> <body> <nav class='navbar navbar-expand-md navbar-dark bg-dark fixed-top'> <a class="navbar-brand" href="@baseurl/Index">TOP</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}"> <span class="navbar-toggler-icon"></span> </button> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="@baseurl/ChatTest">チャット A</a> </li> <li class="nav-item"> <a class="nav-link" href="@baseurl/deep/and/deep/ChatTestSecond">チャット B</a> </li> </ul> </nav> <div class="container"> <h1>@ViewData["Title"]</h1> @RenderBody() </div> <script src="@baseurl/lib/jquery/dist/jquery.min.js"></script> <script src="@baseurl/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="@baseurl/js/site.js" asp-append-version="true"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html> |
一番上にGlobal.BaseUrlとありますが、これは以下のように定義しています。要はサブディレクトリで公開するときにややこしくなるのでデバッグ時とじっさいにサーバーにアップするとき用にわけているわけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
namespace Zero { public class Global { public static bool IsDebug { get { return true; } } public static string BaseUrl { get { if (IsDebug) { return ""; } else { return "公開したいurl(最後の/は不要)"; } } } } } |
ページを作成する
ChatTest.cshtmlというファイルをPagesフォルダ内に作成して以下のように書きます。
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 |
@page @{ ViewData["Title"] = "チャットのテスト"; Layout = "_Layout2"; string baseurl = Global.BaseUrl; } <div class="container"> <p id = "about">このページのurl</p> <p id = "counter">カウンタ</p> <div class="form-group"> <div class="col-2"><label>Title</label></div> <div class="col-2"><input type="text" id="titleInput" /></div> </div> <div class="form-group"> <div class="col-2"><label>Message</label></div> <div class="col-2"><input type="text" id="messageInput" /></div> </div> <div class="form-group"> <input type="button" id="sendButton" value="Send Message" /> </div> </div> <table id = "messagesList" class="table table-striped table-hover"></table> <script src="@baseurl/js/signalr.js"></script> <script> "use strict"; let aboutText = `このページのurlは ${document.location} です。`; document.getElementById("about").textContent = aboutText; document.getElementById("sendButton").disabled = true; let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/chatHubFirst").build(); connection.start().then(function () { document.getElementById("sendButton").disabled = false; }); document.getElementById("sendButton").addEventListener("click", function (event) { var title = document.getElementById("titleInput").value; var message = document.getElementById("messageInput").value; connection.invoke("SendMessage", title, message).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); }); connection.on("ReceiveMessage", function (title, message, datetime) { var tr = document.createElement("tr"); document.getElementById("messagesList").appendChild(tr); tr.innerHTML = `<td>${title}<br>${datetime}</td><td>${message}</td>`; document.getElementById("titleInput").value = ""; document.getElementById("messageInput").value = ""; }); </script> |
深い階層のページでも動作するか?
さて1階層目にある場合はこれで動作することが確認できますが、深い階層にあるページの場合はどうなるのでしょうか? 書き換えが必要な部分がでてくるのでしょうか?
そこでPagesフォルダ内にフォルダを作成し、さらにそのなかにフォルダを作成してdeep\and\deep\ChatTestSecond.cshtmlというファイルを作成します。そして上記と同じコードを書きます(ViewData[“Title”] = “チャットのテスト”;の部分は変更したほうがいいかも)。
すると何の問題もなく動作することがわかります。
実はこの行だけ変更が必要(階層が異なると相対urlではうまくいかない)だったのですが、@baseurlが自動的に変更されるのでうまい具合に動作してくれるのです。またそのような事情でJSファイルとして独立させずにこの場所に書くことにしました。
1 |
let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/chatHub").build(); |
タイトルとメッセージを入力してからSend Messageボタンを押すとタイトルとメッセージが表示されます。また別のタブで同じページを開いて同じようにメッセージをおくるとこれまで開いていたタブでも新しいメッセージを確認することができます。ただし後から開いたタブではこれまでに投稿された内容をみることはできません。また保存機能はありません。なので最初に開いたときは投稿内容はなにもありません。