Azure SignalR Serviceを試す

このエントリは2019/07/12現在の情報に基づいています。将来の機能追加や廃止に伴い、このエントリの内容との乖離が発生する可能性があります。

Azure SignalR Serviceとは

リアルタイムWebを実現するためのコンポーネントを提供するマネージドサービス。簡単に言うと、WebSocketsやServer Sent Events、Long Pollingなどの機能を提供する。詳細は以下のドキュメントを参照。

Azure SignalR サービスとは / What is Azure SignalR Service?
https://docs.microsoft.com/azure/azure-signalr/signalr-overview

単純にTutorialをやるのも何かあれなので、FucntionsでCosmos DBの変更フィードを読み取り、SignalR Serviceに投げ込んで、投げ込まれたメッセージをリアルタイムで確認する、というある種の技術の無駄遣いを通じて、動作確認をしてみた。使う言語はいつも通りJavaのみである。

なお、Cosmos DBの変更フィードを取得して複数のサービスに伝播する場合、Functionsの後続にEvent HubsやEvent Grid、もしくはService Busを配置して連携するのが王道である。

やりたいこと(動作確認のためのシナリオ)

IoTデバイス等からCosmos DBに温度、湿度、デバイスID、測定時刻がPOSTされるので、そのデータ着信を変更フィードで受け取り、WebSocket経由でデータを受け取りたい。具体的には、下図の赤枠で囲んだ部分。簡単のため、認証の仕組みは入れていない。

必要なもの

  • Azure SignalR Service
  • Functions
  • Cosmos DB

準備

Azure SignalR Service

ふつうにインスタンスを作成する。接続文字列は後ほどFunctionsで使うため、記録しておく。モードはServelessにする(設定>Settings。インスタンス生成時に設定してもよい)。

Cosmos DB

こちらもふつうにPortalもしくはAzure CLIでCosmos DBのアカウントを作成する。変更フィードを利用するため、SQL APIもしくはGremlin APIを選択しなければならないが、今回はSQL APIを使う。接続文字列はFunctionsで使うため、記録しておく。 Database、Containerも作成しておく。スループットはデフォルトでかまわない。

今回は以下のようなデータを使う。

{
    "device":"deviceXXX",
    "timestamp":"2019-07-01T01:00:00+09:00",
    "humid":40.1,
    "temp":22.1
}

Functions

PortalもしくはAzure CLIでFunctionsアプリを作成する。SignalRとCosmos DBの接続文字列を設定しておく(Fuction App > 構成)。

SignalRの接続文字列は、[アプリケーション設定]で設定する。名前をAzureSignalRConnectionString としておく。

Cosmos DBの接続文字列は、[接続文字列]で設定する。名前はAzureCosmosDBConnection としておく。データソースの種類はCustomでよい。

開発言語としてJavaを使うため、以下を実行してソースコードを生成しておく。リージョンなどは適宜変更する。

mvn archetype:generate \
-DarchetypeGroupId=com.microsoft.azure \
-DarchetypeArtifactId=azure-functions-archetype \
-DappName={functionAppName} \
-DappRegion={region} \
-DresourceGroup={resourceGroup} \
-DgroupId=com.{functionAppName}.group \
-DartifactId={functionAppName}-functions \
-Dpackage=com.{functionAppName} \
-DinteractiveMode=false

ローカル環境でテストできるよう、以下のコマンドを実行して、local.settings.jsonにAzure Portalで登録した値を反映させておく。

func azure functionapp fetch-app-settings {functionAppName}

上記コマンドを実行すると、Azure上の設定をlocal.settings.jsonに反映できる。以下は実行した結果のlocal.settings.jsonの例(文字列等はマスクしている)。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "...",
    "FUNCTIONS_WORKER_RUNTIME": "java",
    "FUNCTIONS_EXTENSION_VERSION": "~2",
    "APPINSIGHTS_INSTRUMENTATIONKEY": "...",
    "AzureSignalRConnectionString": "..."
  },
  "ConnectionStrings": {
    "AzureCosmosDBConnection": {
      "ConnectionString": "..."
    }
  },
  "Host": {
    "CORS": "*"
  }
}
negotiate function

クライアントが接続して、WebSocket接続のための情報を入手するためのFunction。これはお作法が決まっているので、Hubだけ変更し、それ以外はサンプルコードをそのまま使う。

Azure Functions における SignalR サービスのバインド / SignalR Service bindings for Azure Functions
https://docs.microsoft.com/azure/azure-functions/functions-bindings-signalr-service

具体的には以下。

@FunctionName("negotiate")
public SignalRConnectionInfo negotiate(
        @HttpTrigger(name = "req",
                methods = {HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> req,
        @SignalRConnectionInfoInput(name = "connectionInfo",
                hubName = "climatehub") SignalRConnectionInfo connectionInfo) {
    return connectionInfo;
}

このFunctionはHTTP Triggerなので、POSTで呼び出すと以下のような応答が返る。

{
    "url": "https://xxxx.service.signalr.net/client/?hub=climatehub",
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1NjIwMzU3MTYsImV4cCI6MTU2MjAzNzUxNiwiaWF0IjoxNTYyMDM1NzE2LCJhdWQiOiJodHRwczovL2NsaW1hdGVpbmZvLnNlcnZpY2Uuc2lnbmFsci5uZXQvY2xpZW50Lz9odWI9Y2xpbWF0ZWh1YiJ9.z7WSaTmMGT34D5leeiqfJT4XUUet66eDW7ZMMKpDubk"
}
sendMessage function

こちらは、Cosmos DBの変更フィードをSignalRに投げ込む。そのため、バインドは以下のものを使う。

  • Input : @CosmosDBTrigger
  • Output : @SignalROutput

@CosmosDBTriggerでCosmos DB接続のための設定を追加し、文字列(JSON)として渡ってくる変更フィードを受けるためのArrayListを引数にしている。Functionの戻り値をSignalRへの出力にバインドしている。

@FunctionName("sendMessage")
@SignalROutput(name = "$return", hubName = "climatehub")
public SignalRMessage sendClimateInfo(
        @CosmosDBTrigger(feedPollDelay = 1000,
                name = "climateBinding",
                databaseName = "ClimateDB",
                collectionName = "ClimateContainer",
                leaseCollectionName = "Leases",
                createLeaseCollectionIfNotExists = true,
                connectionStringSetting = "AzureCosmosDBConnection")
                ArrayList<String> climateJSONList,
        final ExecutionContext context) {
    context.getLogger().info(climateJSONList.size() + " record(s) is/are inserted.");
    climateJSONList.stream().forEach(s -> System.err.println("Changed: " + s));
    ArrayList<ClimateInfo> climateInfoArrayList = new ArrayList<>();
    for (String s : climateJSONList) {
        JSONObject json = new JSONObject(s);
        climateInfoArrayList.add(new ClimateInfo(json.getString("device"), json.getString("timestamp"), json.getFloat("humid"), json.getFloat("temp")));
    }
    SignalRMessage message = new SignalRMessage();
    message.target = "climates";
    message.arguments.addAll(climateInfoArrayList);
    return message;
}

Cosmos DBの変更フィードはCosmos DBに格納されているデータがそのまま渡ってくるため、必要な情報だけSignalRに渡すように編集している。

クライアントからの接続時には、Hub名としてmessage.targetに指定しているclimatesを使う(climatehubではないことに注意)。

クライアント

簡単のため、Javaコンソールアプリケーションを使うことにする(本当はJavaScriptのほうがもっと楽だとは思うが)。

public static void main(String... args) {
    connection = HubConnectionBuilder.create(apiBaseUrl).build();
    connection.setServerTimeout(100000);
    connection.start().blockingAwait();
    connection.on("climates", (message) -> {
        System.out.println("[climates] device: " + message.getDevice() + " timestamp: " + message.getTimestamp() + " humid: " + message.getHumid() + " temp: " + message.getTemp());
    }, ClimateInfo.class);
}

URLを指定して接続を作成、開始し、Hub名を指定して着信を待つ。

動作確認

事前にクライアントを複数起動しておき、Cosmos DBにデータを追加していった場合の挙動を確認する。クライアントにはデータが届いていることがわかる。

SignalRのメトリックを見ると、接続クライアント数は2。

そして、流通しているメッセージの件数もトラッキングできていることを確認。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中