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


本記事のゴール

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

AzureSignalRとは?

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

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

SignalR Service - リアルタイム Web | Microsoft Azure

準備するリソース

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

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

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

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

1. Azure SignalRを作成する

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

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

2.Azure Functionsを用意する

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

negotiation関数の作成

FunctionAppの作成

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

関数の中身の作成

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

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

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

関数のデプロイ

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

npm i 
npm run build

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

FunctionApp上の環境変数の設定

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

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

  • SignalR

  • FunctionAppのアプリケーション設定

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

broadcast関数の作成

FunctionAppの作成

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

関数の中身の作成

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

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

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

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

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

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にデプロイします。

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

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

雛形の作成

npx create-react-app webapp --template typescript

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

npm i @microsoft/signalr

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

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

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

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;

デプロイ

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

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

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

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なども登録すれば大丈夫です。

5. 動作確認

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

  1. WebAppにアクセスしてアプリケーションを開きます
    こんな感じの画面になるはずです。

  2. 別ウィンドウまた別タブでFunctionAppの関数ページに行き、テスト実行します
    curlでFunctionURLを叩いても良いです。

  3. アプリケーション上にメッセージと実行時間が表示されたら成功です!

おわりに

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

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