xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Azure Functions + Azure SignalR Service でメッセージを Push 配信する

リアルタイムな双方向通信フレームワークである SignalR にスケーラブルな接続管理を提供する Azure SignalR ServiceGA されて半年ほどが経ちました。これと Azure Functions を利用し、いわゆるサーバーレスアーキテクチャの構成でクライアントにリアルタイムにメッセージを Push 配信することができるようになりました。この構成を作るまでの手順を残します。

以下の公式サンプルが超参考になるのでオススメです。

作りたい構成

全体像としてはザックリと下図のようなものを想定しています。Kinect とか HoloLens などでクライアント側を仮に表現していますが、これは作りたいものによって変えていただければ OK なので、チャットのようなものを想定していただいても良いかと思います。

f:id:xin9le:20190428105723p:plain

クラウド側の作り方

Step.1 : Azure Functions のインスタンスを作る

Azure Functions のインスタンスを作成します。何ということはなく、ただ作るだけです。唯一あるとすればランタイムスタックを「.NET」にすることくらいでしょうか。(ちなみに Linux 環境下では試したことがないので、できるのか確信はありません)

f:id:xin9le:20190428111517p:plain

Step.2 : Azure SignalR Service のインスタンスを作る

次に Azure SignalR Service のインスタンスを作成します。東日本にもすでに来ているのが嬉しいところですね。

f:id:xin9le:20190428112332p:plain

f:id:xin9le:20190428112428p:plain

大事なポイントは Service Mode を Serverless にする ことです。Service Mode についてはしばやんの記事が詳しいのでそっちに譲ります。

Step.3 : Azure SignalR Service の接続文字列を Azure Functions に設定する

Azure Functions から Azure SignalR Service に Push 配信したいメッセージを投げる必要があるわけですが、どの Azure SignalR Service に対して投げるべきなのか、そのエンドポイントを知る必要があります。ということで Azure SignalR Service への接続文字列を Azure Functions に設定してあげます。以下のような感じです。キー名は AzureSignalRConnectionString で固定で、値に接続文字列を入れるのがポイント!

f:id:xin9le:20190428114149p:plain

f:id:xin9le:20190428114729p:plain

ここまでで Azure 側での設定はおしまいです。簡単ですね!

Step.4 : SignalR Service Binding の利用準備

ここからは Azure Functions の実装をしていきます。Visual Studio を開き、Azure Functions の .NET Core テンプレートから開始します。

f:id:xin9le:20190428131836p:plain

Azure Functions と Azure SignalR Service の間は SignalR Service Binding を用いて行います。これは Azure Functions の拡張機能である「入出力バインディング」で実現されており、GitHub にてオープンソースとして公開されています。この NuGet Package をインストールしましょう。

f:id:xin9le:20190428134512p:plain

PM> Install-Package Microsoft.Azure.WebJobs.Extensions.SignalRService

Step.5 : Azure SignalR Service のエンドポイントを問い合わせるメソッドを実装

Push 配信でメッセージを受信したいクライアント (冒頭の図だと HoloLens なので、ここでは HoloLens として話を進めます) は、事前に Azure SignalR Service との間に Connection を張っておく必要があります。ですが、HoloLens は Azure SignalR Service のエンドポイントを知りません。なので、すでにエンドポイントを知っている Azure Functions (Step.3 で接続文字列を指定しましたよね!) に「どこに繋ぎに行ったらいいんだい?」というのを聞くことにします。以下のような感じで実装します。

[FunctionName("negotiate")]  // 'negotiate' で固定
public static SignalRConnectionInfo Negotiate
(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest _,  // POST で受ける
    [SignalRConnectionInfo(HubName = "sample")] SignalRConnectionInfo info  // 接続先情報を injection してもらう
) => info;  // それを返す

こんな短い実装ですが、ポイントがいくつかあります。これは SignalR の Client ライブラリは POST メソッドで /negotiate という固定の URL で接続先情報を取得するように実装されているためです。

  1. FunctionName 属性に negotiate を指定したメソッドを作る
  2. POST で動作する [HttpTrigger] な関数とする
  3. SignalRConnectionInfo を返す

SignalRConnectionInfo は SignalR Service Binding が提供するもので、メソッドの引数に属性を与えておくとメソッドが呼び出し時に injection してくれます。なのでそれを返すだけの実装で OK です。

Step.6 : メッセージを Push 配信する

最後に Azure Functions で受け取り、それを Azure SignalR Service に投げてメッセージを Push 配信する API を実装します。例えば以下のような感じです。

[FunctionName("broadcast")]  // 任意
public static async Task BroadcastAsync
(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest request,  // POST で投げることにする
    [SignalR(HubName = "sample")] IAsyncCollector<SignalRMessage> messages
)
{
    var data = await request.ReadAsStringAsync();  // 受け取ったデータを取り出す
    await messages.AddAsync(new SignalRMessage  // 出力バインディングを使って SignalR Service に投げ込む
    {
        Target = "Receive",  // 配信先の 'Receive' を呼び出す
        Arguments = new[] { data },  // Push 配信するデータ
    });
}

重要なポイントは IAsyncCollector<SignalRMessage> の部分で、このインスタンスに AddAsync することで (出力バインディング経由で) SignalR Service にデータを投げ込むことができます。

ここまでできたら Azure Functions にデプロイ!以上でクラウド側の実装もすべておしまいです。簡単ですね :)

クライアント側の作り方

クライアント側は、相手がサーバーレスアーキテクチャだからと言って特別何か難しいことをする必要はありません。今回はサンプルとして WPF で実装してみます。

<Window x:Class="SignalRTestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="SignalRTestApp" Height="200" Width="500">
    <Grid Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <Label Name="StatusText" Grid.Row="0" BorderBrush="Red" BorderThickness="1" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" FontSize="30" />
        <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,5,0,0">
            <Button Content="Connect" Width="75" Click="OnConnectClick" />
            <Button Content="Disconnect" Width="75" Margin="5,0" Click="OnDisconnectClick" />
            <Button Content="Broadcast" Width="75" Click="OnBroadcastClick" />
        </StackPanel>
    </Grid>
</Window>
using System;
using System.Text;
using System.Net.Http;
using System.Windows;
using Microsoft.AspNetCore.SignalR.Client;

namespace SignalRTestApp
{
    public partial class MainWindow : Window
    {
        private const string RootUrl = "https://signalr-test-api.azurewebsites.net";  // 作った Azure Functions の URL
        private HttpClient HttpClient { get; } = new HttpClient();
        private HubConnection Connection { get; }

        public MainWindow()
        {
            this.InitializeComponent();
            this.Connection = new HubConnectionBuilder().WithUrl($"{RootUrl}/api").Build();

            // Azure SignalR Service から Push されてきたメッセージを受信して表示
            // 最初の図の HoloLens 側
            this.Connection.On<string>("Receive", data =>
            {
                this.Dispatcher.Invoke(() => this.StatusText.Content = data);
            });
        }

        private async void OnConnectClick(object sender, RoutedEventArgs e)
        {
            // 最初の図の HoloLens 側
            await this.Connection.StartAsync();  // '/negotiate' から接続情報を取得して接続
            this.StatusText.Content = "Connected!";
        }

        private async void OnDisconnectClick(object sender, RoutedEventArgs e)
        {
            // 最初の図の HoloLens 側
            await this.Connection.StopAsync();  // 切断
            this.StatusText.Content = "Disconnected!";
        }

        private async void OnBroadcastClick(object sender, RoutedEventArgs e)
        {
            // 日付文字列を POST で Azure Functions に投げ込む
            // 最初の図の Kinect 側
            var url = $"{RootUrl}/api/broadcast";
            var now = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff");
            using (var content = new StringContent(now, Encoding.UTF8))
                await this.HttpClient.PostAsync(url, content);
        }
    }
}

これを実行すると以下のような感じになるはずです。パッと動かす分には全然難しくないですね!

f:id:xin9le:20190429235248g:plain