SignalRでチャットサービス構築

2021-05-25
SignalR

SignalRは何度か取り上げたのですが改めて今日はまとめてみます。

SignalRのドキュメントはこちらにまとまっています。 https://dotnet.microsoft.com/apps/aspnet/signalr

SignalRとは

ASP.NET SignalR は、リアルタイム web 機能をアプリケーションに追加するプロセスを簡略化する ASP.NET 開発者向けのライブラリです。 リアルタイム web 機能を使用すると、クライアントが新しいデータを要求するのをサーバーが待機するのではなく、接続されているクライアントにコンテンツをプッシュすることができるようになります。

SignalR を使用すると、ASP.NET アプリケーションに任意の種類の "リアルタイム" web 機能を追加できます。 多くの場合、チャットは例として使用されますが、さらに多くのことを行うことができます。 ユーザーが新しいデータを表示するために web ページを更新するたびに、またはページが 長いポーリング を実装して新しいデータを取得すると、SignalR を使用することができます。 例としては、ダッシュボードとアプリケーションの監視、コラボレーションアプリケーション (ドキュメントの同時編集など)、ジョブの進行状況の更新、リアルタイムフォームなどがあります。

また、SignalR は、リアルタイムゲームなど、サーバーからの高頻度の更新を必要とする、まったく新しい種類の web アプリケーションを有効にします。

SignalR は、サーバーからクライアントへのリモートプロシージャコール (RPC) を作成するためのシンプルな API を提供します。この API は、クライアントブラウザー (およびその他のクライアントプラットフォーム) で、サーバー側の .NET コードから JavaScript 関数を呼び出します。 SignalR には、接続管理用の API (接続イベントや切断イベントなど) や、接続のグループ化も含まれます。

とのことです。こちらを参照しました。 https://docs.microsoft.com/ja-jp/aspnet/signalr/overview/getting-started/introduction-to-signalr

SignalRはオープンソースということで、こちらでソースコードが公開されています。 https://github.com/signalr

それでは早速こちらのドキュメントに沿って実装していきます。 https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/signalr?tabs=visual-studio&view=aspnetcore-5.0

JSライブラリーの取得、ダウンロード

VisualStudioソリューション エクスプローラー で、プロジェクトを右クリックし、 [追加] > [クライアント側のライブラリ] を選択。

VisualStudioソリューション エクスプローラー で、プロジェクトを右クリックし、 [追加] > [クライアント側のライブラリ] を選択

[ライブラリ] に、「@microsoft/signalr@latest」と入力、そしてインストール。

[ライブラリ] に、「@microsoft/signalr@latest」と入力、そしてインストール。

インストールボタンを押すとあとはよしなにダウロードしてくれます。

インストールボタンを押すとあとはよしなにダウロードしてくれます

SignalR Hubの作成

SignalRの処理をしてくれるのがHubの役目になります。Hub.csを格納するためにHubフォルダーを作成します。

下記がHub.csになります。 個別に解説していきます。

using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
using wppIDP.Data;
using wppIDP.Helpers;
using wppIDP.Models;

namespace wppIDP.Hubs
{
    public class ChatHub : Hub
    {
        private readonly ApplicationDbContext _db;

        public ChatHub(ApplicationDbContext db)
        {
            _db = db;
        }

        public async Task SendMessage(string user, string userName, string userIcon, string type, string message)
        {
            var now = DateTime.Now;

            await Clients.All.SendAsync("ReceiveMessage", user, userName, userIcon, type, message, now.ToString("yyyy-MM-dd HH:mm:ss"));

            var recordResult = await RecordChat(new MessageModel()
            {
                MessageRoomId = "all",
                Text = message,
                From = user,
                IsRead = false,
                To = "",
                Type = "Message to All"
            });
        }

        /// <summary>
        /// Send message in the room
        /// </summary>
        /// <param name="user"></param>
        /// <param name="userName"></param>
        /// <param name="userIcon"></param>
        /// <param name="type"></param>
        /// <param name="message"></param>
        /// <param name="groupId"></param>
        /// <returns></returns>
        public async Task SendMessageToGroup(string user, string userName, string userIcon, string type, string message, string groupId)
        {
            var now = DateTime.Now;

            await Clients.Group(groupId).SendAsync("ReceiveMessage", user, userName, userIcon, type, message, now.ToString("yyyy-MM-dd HH:mm"));

            var recordResult = await RecordChat(new MessageModel()
            {
                MessageRoomId = groupId,
                Text = message,
                FromIconUrl = userIcon,
                From = user,
                IsRead = false,
                To = groupId,
                Type = type
            });
        }

        /// <summary>
        /// Record chat
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        private async Task<MessageModel> RecordChat(MessageModel msg)
        {
            var now = DateTime.Now;

            msg.Id = Guid.NewGuid().ToString();
            msg.Status = "Active";
            msg.TimestampSent = Helpers.DateTimeHelper.GetUnixTime(now);

            _db.Messages.Add(msg);

            await _db.SaveChangesAsync();

            return msg;
        }

        /// <summary>
        /// Add to the group
        /// </summary>
        /// <param name="user"></param>
        /// <param name="userName"></param>
        /// <param name="userIcon"></param>
        /// <param name="type"></param>
        /// <param name="message"></param>
        /// <param name="groupId"></param>
        /// <returns></returns>
        public async Task AddToGroup(string user, string userName, string userIcon, string type, string message, string groupId)
        {
            var now = DateTime.Now;

            await Groups.AddToGroupAsync(Context.ConnectionId, groupId);

            message = $"{userName} joined.";

            await Clients.Group(groupId).SendAsync("ReceiveMessage", user, userName, userIcon, type, message, DateTime.Now.ToString("yyyy-MM-dd HH:mm"));

            var recordResult = await RecordChat(new MessageModel()
            {
                MessageRoomId = groupId,
                Text = message,
                IsRead = false,
                From = user,
                To = groupId,
                Type = type,
                FromIconUrl = userIcon
            });

        }

        /// <summary>
        /// Remove from the group
        /// </summary>
        /// <param name="user"></param>
        /// <param name="userName"></param>
        /// <param name="userIcon"></param>
        /// <param name="type"></param>
        /// <param name="message"></param>
        /// <param name="groupId"></param>
        /// <returns></returns>
        public async Task RemoveFromGroup(string user, string userName, string userIcon, string type, string message, string groupId)
        {
            var now = DateTime.Now;

            await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupId);

            message = $"{userName} has left.";

            await Clients.Group(groupId).SendAsync("ReceiveMessage", user, userName, userIcon, type, message, DateTimeHelper.GetUnixTime(now));

            var recordResult = await RecordChat(new MessageModel()
            {
                MessageRoomId = groupId,
                Text = message,
                IsRead = false,
                From = user,
                To = groupId,
                Type = type,
                FromIconUrl = userIcon
            });
        }
    }
}

メッセージの送信

メッセージの送信部分はこちらです。

  public async Task SendMessage(string user, string userName, string userIcon, string type, string message)
{
   var now = DateTime.Now;

   await Clients.All.SendAsync("ReceiveMessage", user, userName, userIcon, type, message, now.ToString("yyyy-MM-dd HH:mm:ss"));

   var recordResult = await RecordChat(new MessageModel()
    {
      MessageRoomId = "all",
      Text = message,
      From = user,
      IsRead = false,
      To = "",
      Type = "Message to All"
   });
}

引数は下記のように定義しています。

変数タイプ 名前 備考
string user 送信者ユーザーID
string userName 送信者ユーザーの表示名
string userIcon 送信者ユーザーのアイコンURL
string type 送信者タイプ
string message 送信者からのメッセージ

ここではまず時間の取得、そしてClient.All.SendAsyncを読んでいます。名前の通りすべてのユーザーにこのメッセージを送信するtことになります。

await Clients.All.SendAsync("ReceiveMessage", user, userName, userIcon, type, message, now.ToString("yyyy-MM-dd HH:mm:ss"));

その次に送信した内容をDBに格納しています。

var recordResult = await RecordChat(new MessageModel()
{
   MessageRoomId = "all",
   Text = message,
   From = user,
   IsRead = false,
   To = "",
   Type = "Message to All"
});

送信内容を記録するメソッドはクラス内に定義されています。

private async Task<MessageModel> RecordChat(MessageModel msg)
{

   var now = DateTime.Now;

   msg.Id = Guid.NewGuid().ToString();
   msg.Status = "Active";
   msg.TimestampSent = Helpers.DateTimeHelper.GetUnixTime(now);

   _db.Messages.Add(msg);

   await _db.SaveChangesAsync();

   return msg;
}

全員にメッセージを送るケースは少ないかと思います、実際は下記のように特定のグループにメッセージを送るケースが大半です。

public async Task SendMessageToGroup(string user, string userName, string userIcon, string type, string message, string groupId)
{
   var now = DateTime.Now;

   await Clients.Group(groupId).SendAsync("ReceiveMessage", user, userName, userIcon, type, message, now.ToString("yyyy-MM-dd HH:mm"));

   var recordResult = await RecordChat(new MessageModel()
   {
      MessageRoomId = groupId,
      Text = message,
      FromIconUrl = userIcon,
      From = user,
      IsRead = false,
      To = groupId,
      Type = type
   });
}

ここでの引数は下記のようになっています。

変数タイプ 名前 備考
string user 送信者ユーザーID
string userName 送信者ユーザーの表示名
string userIcon 送信者ユーザーのアイコンURL
string type 送信者タイプ
string message 送信者からのメッセージ
string groupId 送信先のグループID

groupId部分に特定のグループIDを指定することによって、そのグループIDを購読しているセッションにのみメッセージを送る仕組みです。

ここまでで全員にメッセージを送る、特定グループにメッセージを送るを見てきました。次は特定グループにユーザーを追加または削除する方法を見てみましょう。

ユーザーをグループに追加と削除

ユーザーを追加

ユーザーをグループ追加する部分は下記です。

public async Task AddToGroup(string user, string userName, string userIcon, string type, string message, string groupId)
{
   var now = DateTime.Now;

   await Groups.AddToGroupAsync(Context.ConnectionId, groupId);

   message = $"{userName} joined.";

   await Clients.Group(groupId).SendAsync("ReceiveMessage", user, userName, userIcon, type, message, DateTime.Now.ToString("yyyy-MM-dd HH:mm"));

   var recordResult = await RecordChat(new MessageModel()
   {
      MessageRoomId = groupId,
      Text = message,
      IsRead = false,
      From = user,
      To = groupId,
      Type = type,
      FromIconUrl = userIcon
    });
}

実際にユーザーが追加されている部分は下記です。

await Groups.AddToGroupAsync(Context.ConnectionId, groupId);

ユーザーを削除・除外

ユーザーをグループから削除するところは下記です。

public async Task RemoveFromGroup(string user, string userName, string userIcon, string type, string message, string groupId)
{
   var now = DateTime.Now;

   await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupId);

   message = $"{userName} has left.";

   await Clients.Group(groupId).SendAsync("ReceiveMessage", user, userName, userIcon, type, message, DateTimeHelper.GetUnixTime(now));

   var recordResult = await RecordChat(new MessageModel()
   {
      MessageRoomId = groupId,
      Text = message,
      IsRead = false,
      From = user,
      To = groupId,
      Type = type,
      FromIconUrl = userIcon
    });
}

SignalRを利用できるようにStartup.csの編集

Startup.csにてSignalRが利用できるように下記コメントある部分を追記します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;

namespace SignalRChat
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddSignalR();  // ここ
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapHub<ChatHub>("/chatHub"); // ここ
            });
        }
    }
}
公開日: 2021-05-25