[Azure] AzureでSingalRを使ってリアルタイム通知を行う

# 本記事のゴール

本記事では、AzureFunctionからSignalR経由で送ったメッセージをReactアプリケーションを載せたWebクライアントで受信できるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/2ea21e54-20cf-da11-cd98-715dc8527931.png)



# AzureSignalRとは？

Azureの提供するリアルタイムプッシュ通信を提供するサービスです。

このサービスを利用すればサーバーサイド側のイベントをトリガーにクライアントアプリケーションに通知を行うことができます。

[SignalR Service - リアルタイム Web | Microsoft Azure](https://azure.microsoft.com/ja-jp/services/signalr-service/?cdn=disable#overview)

# 準備するリソース

先の概略図ではAzureFunctionsを１つしか描きませんでしたが、実際にAzureSignalRとクラインアントの接続を確立するには以下の図の①から③手順を踏むため、Functionsは２つ必要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/61b0f2a1-3237-f031-cae9-8d6d66831db3.png)


したがって、AzureFunctionsはnegotiation用とbroadcast用で２つ用意します。

上記を踏まえて必要なリソースは以下のようになります。

- AzureSignalR
- AzureFunctions(HttpTrigger)
- AzureFunctions(SingalRのnegotiation用)
- Azure WebApp(ReactAppのホスティング用)
- AzureContainerRegistry(ReactAppのイメージ格納用)

# 1. Azure SignalRを作成する

AzurePortalでAzureSignalRサービスを作成します。この際、サービスのモードはDefaultではなくServerlessとなるようにします。

またプランはFreeで大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/0f53d54f-bdac-1214-d4b7-a2ba991534ab.png)


# 2.Azure Functionsを用意する

次にnegotiation関数とbroadcast関数を作成します。

## negotiation関数の作成

### FunctionAppの作成

関数の入れ物になるFunctionAppを作成します。どのプランでも大丈夫です。

### 関数の中身の作成

テンプレートが用意されているので以下のコマンドで一発で作成できます

```bash
func new --name SingalrNegotiator --template "SignalR negotiate HTTP trigger"
```

negotiation関数は中身をいじる必要がないのでコードはテンプレートのままで大丈夫です。

### 関数のデプロイ

関数を作成したFunctionAppにデプロイします。

```bash
npm i 
npm run build

func azure functionapp publish ${作成したNegotiation用FunctionAppの名前}
```

### FunctionApp上の環境変数の設定

FunctionAppの構成ページで,templateで生成されたfunctions.jsonに記載されている`AzureSignalRConnectionString`をアプリケーション設定として追加します。

値は、SignalRの"キー"ページの"接続文字列"を入力します。

- SignalR
![Untitled (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/2987d3b7-55fa-cf29-e6a3-95373d862743.png)

- FunctionAppのアプリケーション設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/fc8658c0-8e3f-4fa1-69ee-fb6765859fb1.png)

これによってFunctionAppとSignalRを連携させます。

## broadcast関数の作成

### FunctionAppの作成

関数の入れ物になるFunctionAppを作成します。どのプランでも大丈夫です。

### 関数の中身の作成

今回はテストしやすいようにHttpTriggerの関数を作成します。こちらもテンプレートで作成できます。

```bash
func new --name BroadcastFunction --template "HTTP trigger"
```

次にfunction.jsonにsignalrとの連携用の記述を追加します.

```bash
{
      "type": "signalR",
      "name": "signalRMessages",
      "hubName": "default",
      "connectionStringSetting": "AzureSignalRConnectionString",
      "direction": "out"
}
```

最後にindex.tsを以下のように書き換えます
`context.bindings.signalRMessages`の部分がsignalRを使って送るメッセージを構築する部分です。targetで指定した文字列をクライアント側でも指定してサブスクライブします。

```tsx
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    context.log('HTTP trigger function processed a request.');
    const name = (req.query.name || (req.body && req.body.name));
    const responseMessage = name
        ? "Hello, " + name + ". This HTTP triggered function executed successfully."
        : "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.";

    // ここがsignalRを使って送るメッセージを構築する部分
    context.bindings.signalRMessages = [{
        "target": "test",
        "arguments": [{message:"hello,world",timestamp:new Date()}]
    }];

    context.res = {
        // status: 200, /* Defaults to 200 */
        body: responseMessage
    };

};

export default httpTrigger;
```

### 関数のデプロイ

関数を作成したFunctionAppにデプロイします。

```bash
npm i 
npm run build

func azure functionapp publish ${作成したBroadcast用FunctionAppの名前}
```

### FunctionApp上の環境変数の設定

Negotiation関数の時と同様に、FunctionAppの構成ページで,templateで生成されたfunctions.jsonに記載されている`AzureSignalRConnectionString`をアプリケーション設定として追加します。


これによってFunctionAppとSignalRを連携させます。

# 3. クライアントアプリケーションを作成する

ホスティング方法はなんでも良いのですが本記事では次のチュートリアルに倣ってContainerRegistryにpushしたimageをWebAppがpullして利用する形を採用します。

ContainerRegistryとWebAppの作成方法は以下を参考にしてください。

[Azure App Service を使ってコンテナー化された Web アプリをデプロイして実行する - Learn](https://docs.microsoft.com/ja-jp/learn/modules/deploy-run-container-app-service/)

## クライアントアプリの中身の作成

雛形の作成

```bash
npx create-react-app webapp --template typescript
```

次にsignalrを使う上で必要な次のパッケージをインストールしてください。

```bash
npm i @microsoft/signalr
```

最後にApp.tsxを以下のように編集してください。メッセージを受け取ったらその内容と時刻を表示するだけのアプリケーションです。

.envにREACT_APP_NEGOTIATOR_NAMEを追加するのを忘れないようにしてください。

Broadcast関数でtargetに`’test’`を指定したのでconnection.onの第一引数には同じ文字列を指定します。

```bash
import React, {useEffect, useState} from 'react';
import './App.css';
import * as signalR from '@microsoft/signalr'
const connection = new signalR.HubConnectionBuilder()
  .withUrl(`https://${process.env.REACT_APP_NEGOTIATOR_NAME}.azurewebsites.net/api`)
  .configureLogging(signalR.LogLevel.Trace)
  .withAutomaticReconnect()
  .build()

function App() {
  const [data,setData] = useState({message:'',timestamp:''})

  useEffect(()=>{
    // Broadcast関数のtargetに合わせる
    connection.on('test', data => {
      console.log("Received data from signalR")
      console.log(data)
      setData(data)
    })

    connection.onclose(function() {
      console.log('signalr disconnected')
    })
    connection.onreconnecting(err =>
      console.log('err reconnecting  ', err)
    )
    connection
      .start()
      .then((res:any) => {})
      .catch(console.error)
  },[])

  return (
    <>
      <div>Message from SignalR</div>
      <div>{data.message}</div>
      <div>arrived at {data.timestamp}</div>
    </>
  );
}

export default App;
```

## デプロイ

以下のコマンドでデプロイします

```bash
acr build --registry ${コンテナレジストリの名前} --image ${イメージ名} ${DockerFileのあるディレクトリへのパス}"
```

参考までにDockerfileも記載しておきます.以下を配置いただいて

```bash
FROM node:14
RUN npm install -g serve # A simple webserver
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["serve", "-s", "build", "-l", "3000"]
```

# 4.SignalRとnegotiation関数でCORSの設定を行う

デフォルトではそれぞれ、同じオリジンからのアクセスしか受け付けてないので、ホスティングしたWebAppからもアクセスできるようにします。WebAppのURLはリソースのトップページで確認できます。

SignalRもnegotiation関数(FunctionApp)もサイドメニューにCORSメニューがあるのでそこから設定します。

ローカルで立ち上げたアプリでも接続したい場合は、http:// localhost:3000なども登録すれば大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/b5d040f1-89f2-b285-151a-f77ae1272d0b.png)


# 5. 動作確認

WebAppにアクセスしてアプリケーションを開いた状態で、FunctionAppをテスト実行します

1. WebAppにアクセスしてアプリケーションを開きます
こんな感じの画面になるはずです。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/da3ddb39-cd85-7e0f-e59b-0c539bc4accb.png)
 
    
2. 別ウィンドウまた別タブでFunctionAppの関数ページに行き、テスト実行します
curlでFunctionURLを叩いても良いです。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/faf7509f-1299-f539-9064-3903016b8a08.png)

    
3. アプリケーション上にメッセージと実行時間が表示されたら成功です！
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/2147ab3a-c0bb-ca85-b964-1544660876b9.png)

    

# おわりに

いかがでしたでしょうか?これでAzureでリアルタイムプッシュ通信をする方法がわかったかと思います。

今回はHttpTriggerをbroadcast関数のトリガーにしましたが、これをTimerやIoTHubのメッセージ受信をトリガーにしたら利用の幅が広がるかと思います。IoTHubとの連携については別記事を書く予定です。お楽しみに。