はじめに
Unity + Azure SignalR でリアルタイム通信 ではクライアント側の紹介をしましたが、サーバー側の実装も紹介したいと思います。
今回は SignalR を Azure Functions で使用します。
また、前回のクライアントコードを利用する前提となっております。
SignalR に接続するための negotiate 関数
SignalR に接続するためにはクライアント側の紹介でも記載しましたが .NET ライブラリの Microsoft.AspNetCore.SignalR.Client を使用する必要があります。
SignalR.Client では HubConnection というクラスがありますが、このクラスが SignalR に接続する際にデフォルトでは最初に negotiate 関数にアクセスを行います。
negotiate 関数では SignalR に接続するための URL やアクセストークンなどの情報をクライアントに返す役割をも持っているので、この関数を配置します。(ちなみに negotiate 関数を使わずに行うことも可能です)
ソースコード
今回は以下の条件で Azure Functions を実装しています。
- 関数単位の認証を行う
- SignalR への接続は任意の ID を基に行う
では、早速ソースコードです。
using System;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Extensions.Logging;
public static class Negotiate
{
const string Function_Name = "negotiate";
const string User_Id = "x-userid";
[FunctionName(Function_Name)]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "POST")] HttpRequest req,
IBinder binder,
ILogger logger)
{
// 以下に出現する AppSettings は設定を保存する単なる静的なクラスです
using (logger.BeginScope(Function_Name)) {
try {
if (!!string.IsNullOrEmpty(req.Headers[User_Id])) {
return new UnauthorizedResult();
}
logger.LogTrace($"negotiate: HubName={AppSettings.SignalR_HubName}, User ID={req.Headers[User_Id]}");
var attribute = new SignalRConnectionInfoAttribute {
HubName = AppSettings.SignalR_HubName,
UserId = req.Headers[User_Id],
ConnectionStringSetting = AppSettings.SignalR_Connection_Key
};
var connectionInfo = await binder.BindAsync<SignalRConnectionInfo>(attribute);
logger.LogTrace($"SignalR Connection Info: Url={connectionInfo.Url}, AccessToken={connectionInfo.AccessToken}");
return new OkObjectResult(connectionInfo);
} catch (Exception ex) {
return new ExceptionResult(ex, true);
}
}
}
}
関数単位での認証
条件にも記載しましたが、『関数単位での認証』を行うためバインドの設定を行っています。
[HttpTrigger(AuthorizationLevel.Function, "POST")] HttpRequest req,
この設定を行った場合、HTTP で呼び出す際にヘッダーの『x-function-key』に Azure ポータルで取得可能なホストキーを指定する必要があります。
ホストキーは『関数アプリ > アプリキー > ホストキー』で取得可能です。
SignalR とユーザーの関連付け
ユーザーを識別するために SignalR にそれを教えてあげる必要がありますが、ネット上に公開されている SignalR の説明で以下のようなソースコードをよく見かけます。
public static SignalRConnectionInfo Negotiate(
[HttpTrigger(AuthorizationLevel.Function, Route = "negotiate")] HttpRequest req,
[SignalRConnectionInfo(HubName = "hub")]SignalRConnectionInfo signalRConnectionInfo) =>
signalRConnectionInfo;
この書き方だと特定のユーザーを識別する方法がなく、また DI を利用している関係で SignalRConnectionInfo に対して、クライアントから渡すヘッダー情報を参照すること出来ません。(出来る方法があるのかもしれませんが、ちょっと調べても分からなかったです)
今回はクライアントから渡されてくる ID を利用したいと思いますので、カスタムバインディングを利用して SignalRConnectionInfoAttribute に ID を設定します。
var attribute = new SignalRConnectionInfoAttribute {
HubName = AppSettings.SignalR_HubName,
UserId = req.Headers[User_Id],
ConnectionStringSetting = AppSettings.SignalR_Connection_Key
};
var connectionInfo = await binder.BindAsync<SignalRConnectionInfo>(attribute);
return new OkObjectResult(connectionInfo);
これで SignalR から特定のユーザーに対してのメッセージを送れる準備が出来ました。
サーバーからクライントに対してメッセージを送る
では、実際にサーバーからクライアントに対してメッセージを送るにはどうすればよいかと言うと IAsyncCollector と SignalRMessage というクラスを利用します。
IAsyncCollector に対して SignalRMessage を追加することでメッセージを送ることが出来ます。
// 特定のユーザーに対してメッセージを送信する
public static async Task SendMessage(this IAsyncCollector<SignalRMessage> message, string userId, string target, object arg)
=> await message.AddAsync(
new SignalRMessage() {
UserId = userId,
Target = target,
Arguments = new[] { arg }
});
// SignalR に接続されている全ユーザーに対してメッセージを送信する
public static async Task SendMessage(this IAsyncCollector<SignalRMessage> message, string target, object arg)
=> await message.AddAsync(
new SignalRMessage() {
Target = target,
Arguments = new[] { arg }
});
上記のメソッドの target にはクライアント側の呼び出すメソッド名を記載します。
クライアント側の紹介コードで言えば "Hello_World" ということになります。
サンプルコード
上記の SignalR メッセージの送信のサンプルですが、5分おきにメッセージをサーバーから送信する内容です。
using System.Collections.Generic;
using System.Threading.Tasks;
using GrimoireEngine.SignalR;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Extensions.Logging;
public static class Watchdog
{
const string Function_Name = "Watchdog";
[FunctionName(Function_Name)]
public static async Task Run(
[TimerTrigger("0 5 * * * *")] TimerInfo timer,
[SignalR(HubName = AppSettings.SignalR_HubName, ConnectionStringSetting = AppSettings.SignalR_Connection_Key)] IAsyncCollector<SignalRMessage> message,
ILogger logger)
{
await message.SendMessage("Hello_World", null);
}
}
いきなり Azure Functions のタイマートリガーを利用していますが、こちらはサーバー主導で実行するためのサンプルですので、そういうものだと思ってください💦
上記の内容で Azure Functions を起動すると登録されたクライアント側では5分おきにサーバーから "Hello_World" というメソッドが呼び出されると思います。
サンプルでは全ユーザーに対してメッセージを送信していますが、ゲームプログラムなどではグルーピングして送信するなどの処理が必要になると思いますが、今回はゴッソリ割愛してしまっていますが、SignalRGroupAction を使えば簡単に実装できます。
おわりに
今回の SignalR サーバーで一番ハマったところとしては ID をサーバーに渡す際にカスタムバインディングを利用しないといけない部分でした。
SignalR の説明と言ってもなかなかこの辺りの情報が非常に少なく手探り状態でした。
とはいえ、ユーザーを識別さえ出来てしまえば、それ以降の実装は非常に簡単ですので、リアルタイム通信が必要な場合は一度検討してもらってもいいんじゃないかと思います。