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 の接点は
- 統計情報のブロードキャスト
- サインイン時のブラウザ用エンドポイント・アクセストークンの生成
の二箇所になります。
統計情報のブロードキャスト
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);
}
流れは
- SignalR Service の接続文字列をパースしてエンドポイント・アクセスキーを抜き出す
- アクセスキーからJWTを生成する
- エンドポイント・ハブ名から SignalR Service 送信用URLを作る
- 送信用URL・JWTを使用して送信したいデータをPOSTする
こんな感じですね。SDK等が提供されているわけではありませんが、このままで単純な送信は対応可能です。
ブラウザ用エンドポイント・アクセストークンの生成
[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");
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
[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
[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のホストに変更すれば動きます。