【SignalR】ASP.NETでリアルタイムチャット機能を作る

## 本記事のゴール

・リアルタイムで複数人がやり取りできるチャット機能を作る

![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/5c497b85-419b-f5b7-f98e-e6c28ae4f0b4.png)

## SignalRとは

通常のWebアプリケーションでは、画面表示時に一度だけサーバーからクライアントへ通信を行い情報を渡しています。しかし、リアルタイムチャットを実現しようとするとメッセージが来たという情報をサーバー側から通知する必要があります。
このサーバーとクライアントの間の通信を解決するライブラリが「SignalR」です。SignalRはMicrosoftが提供しているライブラリであり、サーバー及びブラウザの対応状況に応じて、WebSocketやロングポーリングなどを切り替えることでこれらを実現します。

https://learn.microsoft.com/ja-jp/aspnet/signalr/overview/getting-started/introduction-to-signalr

:::note
WebSocket：ブラウザとサーバーとの間で**常に双方向の通信を継続**してやり取りを行う。
ロングポーリング：ブラウザがサーバーにリクエストを行い、サーバーは**通信したい状況になるまでレスポンスを保留**する。レスポンスが返ってくるとブラウザはすぐに次のリクエストを送信する。
:::

## SignalRを導入する

### サーバー側

1．最新のバージョンでは既にSignalRが含まれているためインストールなどは必要ありません。
※「依存関係」→「フレームワーク」に`Microsoft.AspNetCore.SignalR`などが存在することが確認できます。

2．`Program.cs`を以下のように書き換えてSignalRサーバーを構成するように設定します。

```diff_c_sharp:Program.cs
  var builder = WebApplication.CreateBuilder(args);

  // Add services to the container.
  builder.Services.AddControllersWithViews();
+ builder.Services.AddSignalR();

  var app = builder.Build();
  （下略）
```

### クライアント側（JavaScript）

:::note
TypeScriptを採用する場合、以下公式ドキュメントを参照してください。
ただし、処理の仕組みが異なる訳ではないため本記事も参考になるかと思います。
https://learn.microsoft.com/ja-jp/aspnet/core/tutorials/signalr-typescript-webpack?view=aspnetcore-8.0&tabs=visual-studio
:::

1．プロジェクトを右クリック→「追加」→「クライアント側のライブラリ」をクリックし、ライブラリ マネージャー (LibMan)を開きます。

![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/13562209-3e49-f69a-db04-5cb8e8f91095.png)

2．以下の通りに設定して「インストール」をクリックします。
- プロバイダー：`unpkg`
- ライブラリ：`@microsoft/signalr@latest`
- `特定のファイルの選択`をチェック
- ファイル/dist/browserの中の`signalr.js`と`signalr.min.js`のみを選択
- ターゲットロケーション：`wwwroot/js/signalr/`

![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/ecad7977-d8cf-9352-6459-787248bb6ffe.png)

`wwwroot`配下に`signalr.js`が配置されていればOKです。

![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/e261d4f2-85e9-5a6c-c059-416f15bf094a.png)

## リアルタイムチャット機能を実装する

### ハブを作成・登録

サーバー側にハブ（クライアント側にリアルタイムデータを配信するクラス）を作成します。
アプリケーション直下に`Hubs`フォルダを作成し、その中に`SimpleChatHub.cs`を作成します。

```c#:Hubs/SimpleChatHub.cs
using Microsoft.AspNetCore.SignalR;

namespace Web.Hubs
{
    public class SimpleChatHub : Hub
    {
        // クライアント側からこのSendMessageを呼び出す
        public async Task SendMessage(string message)
        {
            // クライアント側に定義されているメソッドReceiveMessage宛に自分以外に通知する
            await Clients.Others.SendAsync("ReceiveMessage", message);
        }
    }
}
```

![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/cfdafe36-0110-e801-6755-fecbcae9478c.png)

`Program.cs`に、作成したハブを登録します。

```diff_c_sharp:Program.cs
+ using Web.Hubs;

  var builder = WebApplication.CreateBuilder(args);
  （中略）
  app.MapControllerRoute(
      name: "default",
      pattern: "{controller=Home}/{action=Index}/{id?}");
+ app.MapHub<SimpleChatHub>("/SimpleChatHub");

  app.Run();
```


### 画面(View)を作成する

```html:Views/Home/Index.html（一例）
@{
    ViewData["Title"] = "簡易チャット";
}
<style>
    .chat-container {
        max-width: 600px;
        margin: 0 auto;
        padding: 0;
        border: 1px solid #ccc;
        border-radius: 10px;
    }

    .chat-area {
        min-height: 150px;
        padding: 20px;
        border-top-left-radius: 10px;
        border-top-right-radius: 10px;
        background-color: #f8f9fa;
    }

    .chat-bubble {
        margin-bottom: 10px;
        width: 80%;
    }

    .chat-bubble.sent {
        text-align: right;
        margin-left: auto;
    }

    .chat-bubble.received {
        text-align: left;
        margin-right: auto;
    }

    .chat-bubble > span {
        display: inline-block;
        padding: 10px;
        border-radius: 10px;
    }

    .chat-bubble.sent > span {
        text-align: left;
        background-color: #dcf8c6;
    }

    .chat-bubble.received > span {
        background-color: #fff;
    }

    .form-area {
        border-top: 1px solid #ccc;
    }

    .form-area input {
        border: none;
        border-radius: 0;
        border-bottom-left-radius: 10px;
    }

    .form-area button {
        border-radius: 0;
        border-bottom-right-radius: 10px;
    }
</style>

<div class="container">
    <div class="chat-container">
        <div id="ChatArea" class="chat-area">
            <div class="chat-bubble received">
                <span>受信メッセージ1</span>
            </div>
            <div class="chat-bubble sent">
                <span>送信メッセージ1</span>
            </div>
            <div class="chat-bubble received">
                <span>受信メッセージ2</span>
            </div>
        </div>
        <div class="input-group form-area">
            <input id="MessageInput" type="text" class="form-control" placeholder="メッセージを入力" />
            <div class="input-group-append">
                <button id="SendButton" class="btn btn-primary">送信</button>
            </div>
        </div>
    </div>
</div>
```

![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/4ff22472-8f20-ac52-8fb1-7b1c064fa304.png)

### JavaScript処理を作成する

```javascript:wwwroot/js/SimpleChat.js
// コネクション確立
//  - withUrl内はProgram.csで定義したパスを指定
var connection = new signalR.HubConnectionBuilder().withUrl("/SimpleChatHub").build();

// コネクション開始時処理
connection.start()
    .then(() => {
        // コネクション正常開始
        console.info("connection start");
    }).catch((err) => {
        // コネクションエラー
        return console.error(err.toString());
    });

// 通知を受けたときの処理 メソッド名: ReceiveMessage
connection.on("ReceiveMessage", (message) => {
    this.AddChatBubble(false, message);
});

// 送信ボタンクリック時処理
document.getElementById("SendButton").addEventListener("click", (event) => {
    var message = document.getElementById("MessageInput").value;
    if (message.trim() === "") return;
    // SimpleChatHubのSendMessageを呼び出す
    connection.invoke("SendMessage", message)
        .then(() => {
            // メッセージ送信成功
            this.AddChatBubble(true, message);
            document.getElementById("MessageInput").value = "";
        }).catch((err) => {
            // メッセージ送信エラー
            return console.error(err.toString());
        });
    event.preventDefault();
});

// メッセージをチャット画面に追加する
function AddChatBubble(isSent, message) {
    var bubble = document.createElement('div');
    bubble.classList.add("chat-bubble");
    bubble.classList.add(isSent ? "sent" : "received");
    var span = document.createElement("span");
    var text = document.createTextNode(message);

    span.appendChild(text);
    bubble.appendChild(span);
    document.getElementById("ChatArea").appendChild(bubble);
}
```

画面の仮メッセージを削除し、JavaScriptファイルを呼び出す。

```diff_html:Views/Home/Index.html
  （上略）
  <div class="container">
    <div class="chat-container">
        <div id="ChatArea" class="chat-area">
-           <div class="chat-bubble received">
-               <span>受信メッセージ1</span>
-           </div>
-           <div class="chat-bubble sent">
-               <span>送信メッセージ1</span>
-           </div>
-           <div class="chat-bubble received">
-               <span>受信メッセージ2</span>
-           </div>
        </div>
        <div class="input-group form-area">
        （中略）
        </div>
    </div>
</div>

+ <script src="~/js/signalr/dist/browser/signalr.js"></script>@* signalrライブラリ *@
+ <script src="~/js/SimpleChat.js"></script>@* 自作したjsファイル *@
```

### 動作確認

Aさんの表示

![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/5c497b85-419b-f5b7-f98e-e6c28ae4f0b4.png)

Bさんの表示

![image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2779418/1ebf5500-05ce-66a1-4cfb-846f59fb5229.png)

## 参考記事

ASP.NET Core SignalR の概要 - Microsoft Learn
https://learn.microsoft.com/ja-jp/aspnet/core/signalr/introduction?view=aspnetcore-8.0