SignalR でChat アプリを作ってみた

サーバーから、クライアントへの通知を行いたくて、SignalR を調査してみた。サンプルは、JavaScript と、.NET サーバーのものが多かったので、ASP.NET MVC + .NET Client という構成で簡単なサンプルを作ってみた。通知をしたいなら、ポーリングでもいいし、Notification Hub でもいいはず。でもそれらのメリット・デメリットを理解したいので、まず、SignalR をやってみることとにした。

<img width="1358" alt="SignalR.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/e03144e1-e657-0e6b-93da-476518700d27.png">

# SignalR とは

SignalR はリアルタイムの双方向通信を簡単におこなうライブラリで、例えばチャットや、対戦ゲーム、プログレスバーみたいな用途に使えるようです。Microsoft で使っているような言語のライブラリが用意されています。問題はWebの情報が古いので動くかドキドキすることですが、意外とあっさり動いてくれました。手順は、VS2012 とかの手順ですが、同じ感じでVS2017でも動作しました。

SignalR の良いところは、双方向通信と聞くと、「Websocketじゃね？」と思うのですが、WebSocketが使えないケースでもServer Sent Events, Forever Frame, Ajax long polling など、Comet ベースのトランスポートもサポートしてくれています。それをライブラリが吸収してくれているらしいです。

* [Introduction to SignalR](https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/introduction-to-signalr)

# アーキテクチャ

下記のようになっていて、サーバー側からクライアントのコードを、クライアントからサーバーのコードを、Hub/HubProxyを通じて双方向に呼べるようになっています。これが隠蔽されていて簡単にコードで書けるのでいい感じですね。

![image1.png](https://qiita-image-store.s3.amazonaws.com/0/3470/a8406e39-b52f-cadb-2528-a59122d6a073.png)

HTML5 ベース、そして、Cometベースのトランスポートに対応しています。

![image5.png](https://qiita-image-store.s3.amazonaws.com/0/3470/6d7c54f6-8a64-2edf-e0ae-32e475cfa700.png)

ちなみに、現時点で NuGetパッケージを検索すると、Angular をはじめ、いろいろなクライアントがサポートされていました。サンプルは、JQueryのサポートだったので、つらかったｗ。(Javaもあるみたいです）。

今回は、クライアント・サーバー両方 .NET での例で実施してみます。

# サーバーのコード

* [Tutorial: Getting Started with SignalR 2 and MVC 5](https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/tutorial-getting-started-with-signalr-and-mvc)

に従えば、ASP.NET MVC + javascript はできてしまうので、違うパターンで。

サーバーはとても簡単で、ASP.NET MVC のテンプレートを使います。（使わなくてもいいです）クラスを追加してみると、SignalR のテンプレートがすでに存在します。

<img width="704" alt="Hub.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/976a1905-76c2-0784-98c7-6a63c6880346.png">

これをプロジェクトを選択して追加します。

次にちょいとエディットします。

_ChatHub.cs_

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace SignalRSamples
{
    public class ChatHub : Hub
    {
        public void Send(string name, string message)
        {
            Clients.All.addNewMessageToPage(name, message);
        }
    }
}
```

このメソッドがクライアントから呼ばれたら、すべてのクライアントに対して、addNewMessageToPageというメッセージを投げるというプログラムです。そこに引数を持っています。

ちなみに、このテンプレートを使うと、こっそり、Reference と、 Javascript のスクリプトに、SignalRのライブラリが追加されていることがわかります。

<img width="404" alt="reference.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/3772383b-76bf-9be7-5f79-ecf32a426f9c.png">

<img width="241" alt="jquery.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/f5a2c19f-08ba-bd78-6e54-96a9e7fa3fcc.png">



これだけでは、だめなので、Startup.cs クラスを作成します。これもテンプレートで作れます。

<img width="707" alt="Startup.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/56f8ce7a-3a2f-844f-03eb-03174013d7c0.png">


_Startup.cs_

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(SignalRSamples.Startup))]

namespace SignalRSamples
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
            app.MapSignalR();
        }
    }
}

```

スタートアップ時に、ここで、Application Builder パイプラインに、SignalR を追加して有効化します。ここでデフォルトで、SignalRのエンドポイントは、`/singalr` になります。これを変更したい場合はこちら参照のこと。変更したい場合は、この`app.MapSignalR()` に引数を持たせることができます。

 * [ASP.NET SignalR Hubs API Guide - Server (C#)](https://docs.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-server)

このあたりのStartupがどういうものか？とかの構造に関してはここに解説があるみたい。また読んでみよう。（未読）

* [An Overview of Project Katana](https://docs.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/an-overview-of-project-katana)

そして、デバッグモードで起動する。ちなみに、最初に何故かArgumentOutOfRangeExceptionが出た。

<img width="593" alt="Error.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/dbb83e7d-2912-e6b5-4f7f-9e94b837e2e4.png">

StackOverflow だと、IISをいれるといいよだったけど、VS2017 を Administratorモードで動かしたら動くようになった。

* [Specified Argument was out of the range of valid values Parameter name : site [duplicate]](https://stackoverflow.com/questions/39034455/specified-argument-was-out-of-the-range-of-valid-values-parameter-name-site)

こういう直感的ではないエラーはつらいなー。でもなんとかなった。これでまずはサーバー終わり。

# クライアント

クライアントは簡単だ。簡単のため、コンソールアプリで作ってみた。NuGetで`Microsoft.AspNet.SignalR.Client` をインストールしています。それだけ。

_Program.cs_

```csharp
using Microsoft.AspNet.SignalR.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SignalRClient
{
    class Program
    {
        static void Main(string[] args)
        {
            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {

            Console.Write("What is your name? : ");
            var yourname = Console.ReadLine();
            var hubConnection = new HubConnection("http://localhost:1218/signalr"); // 1. Hub へのコネクション
            IHubProxy chatProxy = hubConnection.CreateHubProxy("ChatHub");   // 2. Proxy の作成
            chatProxy.On("addNewMessageToPage", (string name, string message) => // 3. サーバーからのコールバック設定
            {
                Console.WriteLine($"{name} : {message}");
                Console.Write("Message :");
            });
            await hubConnection.Start();  // 4. コネクションの開始

            Console.Write("Message :");
            while (true)
            {
                var yourmessage = Console.ReadLine();
                await chatProxy.Invoke("Send", yourname, yourmessage);  // 4. サーバーのメソッド呼び出し

            }
           
        }
    }
}

```

見た目のとおりなのですが、先ほど作成したサーバーへのコネクションを設定します。

```csharp
var hubConnection = new HubConnection("http://localhost:1218/signalr"); // 1. Hub へのコネクション

```

つぎにProxyの作成。Hubの名前はサーバー側で作ったものに合わせます。

```csharp

IHubProxy chatProxy = hubConnection.CreateHubProxy("ChatHub");   // 2. Proxy の作成
```

Proxyに対して、Onメソッドで、サーバーからのコールバックを設定します。

```csharp
            chatProxy.On("addNewMessageToPage", (string name, string message) => // 3. サーバーからのコールバック設定
            {
                Console.WriteLine($"{name} : {message}");
                Console.Write("Message :");
            });
```

先ほどのサーバー側のコードをもう一度見てみましょう。サーバー側からクライアントを読んでいるコードが、`Client.All.addNewMessageToPage` で、クライアント側のコールバックと引数含めて一致しています。これが呼ばれたら、クライアントのコールバックが呼ばれます。

```csharp
        public void Send(string name, string message)
        {
            Clients.All.addNewMessageToPage(name, message);
        }

```

次にサーバー側のメソッド呼び出しです。チャットなので何回もメッセージ送りたいので、ループさせてみました。

```csharp
            Console.Write("Message :");
            while (true)
            {
                var yourmessage = Console.ReadLine();
                await chatProxy.Invoke("Send", yourname, yourmessage);  // 4. サーバーのメソッド呼び出し

            }
```

サーバー側の、`Send` メソッドをプロキシ経由で呼び出しているのがわかると思います。これで双方向通信をよきに計らってくれるってこら簡単だわ！

実行。冒頭のと同じですが、１台のサーバーをあげて、２台のクライアントをあげてチャットしています。

<img width="1358" alt="SignalR.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/ffeeefd0-5175-8db6-d3ab-ac36a27df0a1.png">

片方のコンソールでメッセージをうつと、別の方にも即座に反映されます。こらちょっと楽しいなｗ

ちなみみに、今回のソースコードGitHubに上げています。

 * [TsuyoshiUshio/SignalRSample](https://github.com/TsuyoshiUshio/SignalRSample)

次は、Notification Hub とか、これをサーバーレスの環境でどう活用するかとかを楽しんでみたいと思います。ちなみに、私は個人的には、Javascript(JQuery) + ASP.NET MVC のチュートもやってみたのですが、同じぐらい簡単でした。これは、Angular でも簡単なんだろうなぁ。

# Resources
 * [TsuyoshiUshio/SignalRSample](https://github.com/TsuyoshiUshio/SignalRSample)
 * [Introduction to SignalR](https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/introduction-to-signalr)
 * [Specified Argument was out of the range of valid values Parameter name : site [duplicate]](https://stackoverflow.com/questions/39034455/specified-argument-was-out-of-the-range-of-valid-values-parameter-name-site)
 * [An Overview of Project Katana](https://docs.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/an-overview-of-project-katana)
 * [Tutorial: Getting Started with SignalR 2 and MVC 5](https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/tutorial-getting-started-with-signalr-and-mvc)
 * [Pluralsight: SignalR across web and devices](https://app.pluralsight.com/player?course=signalr-across-web-devices&author=samidip-basu&name=signalr-across-web-devices-m2-introduction-signalr&clip=0&mode=live)