LoginSignup
2
2

More than 5 years have passed since last update.

SignalR Service 調査メモ(2)

Posted at

SignalR Service 調査メモ(2)

前回記事 で調べると書き残して〆た Azure Functions から SignalR Service を使って通知を送信する部分を調べました。

RealtimeSignIn の README によると動作の概要はこんな感じ。

  • index.html を開くとサインインを行い、Functions → ブラウザ に SignalR Serviceのエンドポイントとアクセストークン、統計情報を返す
  • Functionsではサインインが行われる度にテーブルを更新し、サインインの統計情報を SignalR Service を通じてブロードキャストする
  • ブラウザはサインイン時に受け取ったエンドポイント・アクセストークンを使って JavaScript で SignalR Service に接続し、受信した統計情報を元に画面を更新する。

Azure Functions と SignalR Service の接点

Azure Functions と SignalR Service の接点は

  • 統計情報のブロードキャスト
  • サインイン時のブラウザ用エンドポイント・アクセストークンの生成

の二箇所になります。

統計情報のブロードキャスト

AzureSignalR.cs
private static void ParseConnectionString(string connectionString, out string endpoint, out string accessKey)
{
    var dict = connectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Split(new[] { '=' }, 2)).ToDictionary(p => p[0].Trim().ToLower(), p => p[1].Trim());
    if (!dict.TryGetValue("endpoint", out endpoint)) throw new ArgumentException("Invalid connection string, missing endpoint.");
    if (!dict.TryGetValue("accesskey", out accessKey)) throw new ArgumentException("Invalid connection string, missing access key.");
}

private string GenerateJwtBearer(string issuer, string audience, ClaimsIdentity subject, DateTime? expires, string signingKey)
{
    SigningCredentials credentials = null;
    if (!string.IsNullOrEmpty(signingKey))
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
        credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
    }

    var token = jwtTokenHandler.CreateJwtSecurityToken(
        issuer: issuer,
        audience: audience,
        subject: subject,
        expires: expires,
        signingCredentials: credentials);
    return jwtTokenHandler.WriteToken(token);
}

private Task<HttpResponseMessage> PostJsonAsync(string url, object body, string bearer)
{
    var request = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri(url)
    };

    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
    request.Headers.Accept.Clear();
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    request.Headers.AcceptCharset.Clear();
    request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("UTF-8"));

    var content = JsonConvert.SerializeObject(body);
    request.Content = new StringContent(content, Encoding.UTF8, "application/json");
    return httpClient.SendAsync(request);
}

public async Task SendAsync(string hubName, string methodName, params object[] args)
{
    var payload = new PayloadMessage()
    {
        Target = methodName,
        Arguments = args
    };
    var url = $"{endpoint}:5002/api/v1-preview/hub/{hubName}";
    var bearer = GenerateJwtBearer(null, url, null, DateTime.UtcNow.AddMinutes(30), accessKey);
    await PostJsonAsync(url, payload, bearer);
}

流れは

  1. SignalR Service の接続文字列をパースしてエンドポイント・アクセスキーを抜き出す
  2. アクセスキーからJWTを生成する
  3. エンドポイント・ハブ名から SignalR Service 送信用URLを作る
  4. 送信用URL・JWTを使用して送信したいデータをPOSTする

こんな感じですね。SDK等が提供されているわけではありませんが、このままで単純な送信は対応可能です。

ブラウザ用エンドポイント・アクセストークンの生成

SignInFunction.cs
[FunctionName("signin")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req, TraceWriter log)
{
    ...
    return req.CreateResponse(HttpStatusCode.OK, new
    {
        authInfo = new
        {
            serviceUrl = signalR.GetClientHubUrl(HubName),
            accessToken = signalR.GenerateAccessToken(HubName)
        },
        stats = stats
    }, "application/json");
AzureSignalR.cs
public string GetClientHubUrl(string hubName)
{
    return $"{endpoint}:5001/client/?hub={hubName}";
}

public string GenerateAccessToken(string hubName)
{
    return GenerateJwtBearer(null, GetClientHubUrl(hubName), null, DateTime.UtcNow.AddMinutes(30), accessKey);
}

signin Function を叩かれたタイミングでClientHubのURLとアクセストークンを生成し、ブラウザ側に返す所の抜粋。
前項と同じく接続文字列から抜き出したエンドポイント・アクセスキーからClient用HubのURLとアクセストークンを生成して返しています。

まとめ

AzureSignalR.cs を組み込んで Azure Functions から SignalR Service へのデータ送信は簡単にできそうです。
しかしChatRoomアプリのような「ブラウザ→SignalR Service→サーバ(Azure Functions)」のフローはどう実現するのか謎なままです。
まあそこは普通にPOSTで代用してもいいのかもしれませんが、GAする頃には Azure Functions側に SignalR Service Trigger的なものが追加されているかもしれません。

おまけ(Serverless版ChatRoomを作ってみる)

というわけで ChatRoom・RealtimeSignIn をニコイチして改造し、Azure Functions/SignalR Service/Storage の3サービスだけで動く ServerlessChatRoom サンプルを作ってみました。

Functions側実装

Join

cs:Join.cs
[FunctionName("Join")]
public static IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req,
    TraceWriter log) {
    try {
        var signalR = new AzureSignalR(Environment.GetEnvironmentVariable("AzureSignalRConnectionString"));
        return new OkObjectResult(new {
            authInfo = new {
                serviceUrl = signalR.GetClientHubUrl(HubName),
                accessToken = signalR.GenerateAccessToken(HubName)
            }
        }
        );
    } catch(Exception ex) {
        log.Error(ex.Message);
        log.Error(ex.StackTrace);
        throw;
    }
}

チャットルーム参加時に呼び出すHttpTriggerです。
ブラウザからSignalR Serviceに接続するためのエンドポイントURL・アクセストークンを返します。

BroadcastMessage

BroadcastMessage.cs
[FunctionName("BroadcastMessage")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequest req,
    TraceWriter log) {
    var signalR = new AzureSignalR(Environment.GetEnvironmentVariable("AzureSignalRConnectionString"));
    string name = req.Query["name"];
    string message = req.Query["message"];

    // broadcast through SignalR
    await signalR.SendAsync(HubName, "broadcastMessage", name, message);

    return new OkObjectResult($"OK");
}

メッセージ送信用のHttpTriggerです。
発言者・メッセージをそのまま AzureSignalR を介してブロードキャスト送信します。

JavaScriptアプリ側実装

こちらは ChatRoom のindex.htmlを改造します。

SignalR Service 接続

function startConnection(url, accessToken) {
    var connection = new signalR.HubConnectionBuilder()
        .withUrl(url, { accessTokenFactory: () => accessToken })
        .build();
    bindConnectionMessage(connection);
    connection.start()
        .then(function () {
            onConnected(connection);
        })
        .catch(function (error) {
            console.error(error.message);
        });
}
$.getJSON('/Join').done(result => {
    startConnection(result.authInfo.serviceUrl, result.authInfo.accessToken);
});

Join HttpTrigger を呼び出し、返ってきたエンドポイントURL・アクセストークンを使用して SignalR の接続を作るように変更します。

BroadcastMessage送信

function broadcastMessage(name, message) {
    $.ajax({
        url: '/BroadcastMessage',
        type: 'GET',
        data: {
            name: name,
            message: message
        },
        timeout: 10000,
    }).done(function (data) {
    }).fail(function (XMLHttpRequest, textStatus, errorThrown) {
    });
}

...

//connection.send('broadcastMessage', '_SYSTEM_', username + ' JOINED');
broadcastMessage('_SYSTEM_', username + ' JOINED')

オリジナルだとSignalRで送信していた所を BroadcastMessage HttpTrigger を呼び出し、Azure Functions からメッセージを送信する形にします。

完成したソースを以下に置いておきます。
StorageにWebページを配置、Azure Functions をデプロイしてアプリケーション設定の"AzureSignalRConnectionString", "BlobHost"をお使いの SignalRServiceの接続文字列・Storageのホストに変更すれば動きます。

ソースコード

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2