xin9le.net

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

gRPC / MagicOnion 入門 (14) - 接続ユーザーを特定する

gRPC で通信を行う際、サーバー側でアクセスしてきているユーザーを特定したいケースは多々あります。これを実現する最も基本的で素朴な方法が、gRPC / MagicOnion 入門 (10) - ヘッダーの利用 で解説したヘッダーにユーザー固有の ID を埋め込むことです。

そのままやろうとすると結構手間な実装になりますが、MagicOnion はこれを簡単に実現できるよう ConnectionId の概念をサポートしてくれています。今回はその機能について見ていきます。

クライアントから ConnectionId を送信する

クライアントから ConnectionId を送信する場合、これまで紹介してきた方法と若干違う方法で初期化する必要があります。以下のように ChannelContext でラップしつつ、通信を行います。

using System;
using System.Threading.Tasks;
using Grpc.Core;
using MagicOnion.Client;
using MagicOnionSample.ServiceDefinition;

namespace MagicOnionSample.Client
{
    class Program
    {
        static void Main() => MainAsync().Wait();

        static async Task MainAsync()
        {
            var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);

            //--- ChannelContext でチャンネルとユーザー固有の ID をラップ
            var connectionId = "なにかしらユーザー固有のID";
            var context = new ChannelContext(channel, () => connectionId);
            await context.WaitConnectComplete();  // 接続待ち

            //--- API クライアントを生成して通信
            var client = context.CreateClient<ISampleApi>();
            var result = await client.Sample();
            Console.ReadLine();
        }
    }
}

サーバー側で ConnectionId を取り出す

サーバー側で ConnectionId を取り出すのは非常に簡単で、以下のようにします。

using MagicOnion;
using MagicOnion.Server;
using MagicOnionSample.ServiceDefinition;
using MessagePack;

namespace MagicOnionSample.Service
{
    public class SampleApi : ServiceBase<ISampleApi>, ISampleApi
    {
        public async UnaryResult<Nil> Sample()
        {
            //--- こんな感じで取り出せます
            //--- たったこれだけ!
            var connectionId = this.GetConnectionContext().ConnectionId;
            return Nil.Default;
        }
    }
}

この ConnectionId の仕組みも、実はヘッダーを利用して行われています。MagicOnion が面倒な部分を上手くラップしてくれていることもあり、簡単に使えるのでオススメです。

ImageMagick でお手軽 TGA → PNG 変換

業務で .tga で納品される大量のファイルを一括して .png に変換したいということがあったので、ImageMagick Converter を使ってチャチャっとやってみた系のメモです。

ImageMagick をダウンロード

下記サイトからダウンロードできます。各々の OS 環境に合わせてダウンロードしてください。Windows 環境の場合は portable 版を落とすのがお手軽でよさそう。

f:id:xin9le:20180313004617p:plain

バッチ処理で一括変換

ダウンロードした ImageMagick の .zip ファイルを解凍して、中にある convert.exe を利用しましょう。特定フォルダに含まれる .tga ファイルを一括で .png にする場合、例えば以下のようなバッチファイルになるでしょう*1

@echo off

set CONVERTER=ImageMagick-7.0.7-26-portable-Q16-x64\convert.exe
set SOURCE=<tgaのあるフォルダパス>
set TARGET=<pngの出力先フォルダパス>

for /r %SOURCE% %%A in (*.tga) do (
    echo %%A
    %CONVERTER% %%A %TARGET%\%%~nA.png
)

一度作ってしまえば、あとはフォルダに入れて叩くだけ!また、この手法を使えば別の拡張子間での変換もお手軽にできるはずです。

*1:パスやフォルダは適宜読み替えてください

ASP.NET Core SignalR を試す

これまで ASP.NET Core には SignalR が提供されていませんでしたが、最近ようやく利用できるようになってきたということで実際に動かして試してみました。今回は、空プロジェクトから簡易チャット機能を動作させるところまでを紹介します。

以下に公式ブログに getting started がありますが、空プロジェクトからの開始ではないのでよりシンプルにしてみます。

Step.1 : 環境構築

まず、以下のセットアップを行いましょう。

Step.2 : プロジェクトの準備

次に Visual Studio で ASP.NET Core テンプレートから空プロジェクトを作成します。

f:id:xin9le:20180312011235p:plain

f:id:xin9le:20180312011620p:plain

続いて、npm (Node Package Manager) を用いてクライアント実装で利用する JavaScript ライブラリを取得します。以下のコマンドを入力しましょう。

npm install @aspnet/signalr 

上記コマンド実行によってインストールされた .js ファイル群を wwwroot 配下にコピーします。手コピが面倒ですが、そういうものということにしましょう。

コピー フォルダパス
C:\Users\<ユーザー名>\node_modules\@aspnet\signalr\dist\browser
<プロジェクトフォルダ>\wwwroot\libs\signalr

最終的なプロジェクト構造は以下のようになる想定です。

f:id:xin9le:20180312040050p:plain

Step.3 : Hub (サーバー側) を作成

チャット機能のサーバー側実装として Hub を作成します。Hub は MVC でいうところの Controller に当たる部分です。クライアントからのリクエストを処理し、接続されているユーザーにデータを返します。今回は簡易チャット機能なので、以下のような実装をします。

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace SignalRSample.Hubs
{
    public class ChatHub : Hub
    {
        public Task Broadcast(string message)
        {
            //--- 接続されている全ユーザーにメッセージを配信
            var timestamp = DateTime.Now.ToString();
            return this.Clients.All.SendAsync("Receive", message, timestamp);
        }
    }
}

続いて、ASP.NET Core アプリケーションで SignalR の機能を有効にします。Startup.cs に以下のようなコードを記述します。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SignalRSample.Hubs;

namespace SignalRSample
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSignalR();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDefaultFiles();
            app.UseStaticFiles();
            app.UseSignalR(x => x.MapHub<ChatHub>("/chat"));
        }
    }
}

Step 4 : クライアント側を作成

メインとなるページ (index.html) を wwwroot 直下に作成します。中身は以下のような感じにします。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>SignalR Sample</title>
</head>
<body>
    <form>
        <input type="text" id="message" />
        <button type="submit" id="button">Send</button>
    </form>
    <div id="messages"></div>

    <script src="libs/signalr/signalr.js"></script>
    <script src="scripts/chat.js"></script>
</body>
</html>

次に、サーバーと接続してデータのやりとりをする処理を実装します。ここでは wwwroot\scripts\chat.js として作成します。

//--- ChatHub とのコネクションを生成
const connection = new signalR.HubConnection('/chat');

//--- 受信したときの処理
connection.on('Receive', (message, timestamp) => {
    const item = document.createElement('li');
    item.innerHTML = "<div>" + timestamp + " - " + message + "</div>";
    document.getElementById('messages').appendChild(item);
});

//--- ボタンをクリックしたらデータを送信
document.getElementById('button').addEventListener('click', event => {
    const message = document.getElementById('message').value;
    connection.invoke('Broadcast', message).catch(e => console.log(e));
    event.preventDefault();
});

//--- 接続を確立
connection.start().catch(e => console.log(e));

Step.5 : 実行

実装が完了したので実行してみましょう。複数のブラウザ間でメッセージのやり取りができることが確認できると思います。

f:id:xin9le:20180312035722p:plain

細かい説明は省略していますが、とりあえず Hello World ができれば大きな一歩!

4 色オセロ対決 again - 仙台 IT 文化祭 2017

少し遅くなってしまいましたが...!10/28 (土) ~ 10/29 (日) にかけて仙台 IT 文化祭というイベントに参加/登壇してきました。人生初めての仙台、人生初めての東北。新しいところに踏み入れるのは不思議な緊張感があっていいものですね!

ドキドキ・ライブコーディング対決 出張編

いつもは北陸の勉強会で行っていて、若干恒例になりつつある C# 大好き MVP によるドキドキ・ライブコーディング対決 !!。プログラミングを見て/聞いて/一緒に考えて楽しむエンターテイメントとして行っています。今回は出張編と題して、年始に BuriKaigi 2017 in Toyama で行ったのと同じ 4 色オセロ対決 をメインに行いました。お品書きはこんな感じ。

  • ライブコーディング対決
    • FizzBuzz
    • 閏年の判定
    • 2017/10 のカレンダー表示
  • 4 色オセロ対決
    • 各自のアルゴリズム紹介
    • 実際に勝負

ライブコーディング対決って言っても、問題が簡単だと思うじゃないですか!でも案外瞬間的には出てこないもので、出題されて 2、3 分で答えを出すのって案外難しいんです。出題される問題を本当に知らないので、本当に毎回テンパります。でもきっとそれが面白く映るんだと思います。実際ほとんどの方が笑ってくださっていましたし、所変えてもポジティブな感想や反響は非常に多かったです。セッションを聞いて、プログラミングを始めるために Visual Studio をインストールした!という方が出たくらい!

4 色オセロのアルゴリズム

さて、今回のメインだった 4 色オセロ対決。前回の対決での僕はと言えば、4 色オセロの強い AI を作ることができず、結果魂を悪魔に売って強いと思われる人の AI を勝手に使うという手法に出ました...w これは正直全然良くないアプローチで、確実に勝てるとは限りませんでした (実際負けた)。

なので今回は考えを大きく改めて、勝ち筋を再現するというアプローチを取ることにしました。これは以下のような発想で作ったものです。

  • 自分の手を乱数で決定
  • 自分の手はすべて履歴として記録
  • 他人のアルゴリズムと事前に 1000 回ほど戦っておく
    (このぐらいやると大体数十回は勝つ)
  • 勝った手筋のうち、最終的に一番自分の石が多く盤面に残っている手筋を選択
  • 実際の対決の際には、選択された手筋をそのままトレースして打つ

この手法は、比較的弱いサンプル AI に対して以下のような大勝することもできました。

f:id:xin9le:20171104032018p:plain

f:id:xin9le:20171104032026p:plain

ただし、これには相手が自分の手に対して必ず同じ答えを返してくるという前提が必要です。もう少し別の言い方をすれば、相手が乱数で決まるアルゴリズムを使ってこないことが条件となります。しかし、これはエンジニア心理としてほとんど発生しないと踏みました。この手のアルゴリズムを考える場合、大抵のエンジニアは自分の手を神に委ねるような真似をしません。「お前がそう打つなら、俺はこう打ってやるぜ」を地で行きます。乱数のないアルゴリズムであれば自分の手に対して毎回同じ答えを返すので、事前に評価された結果と同じ未来が訪れます。あとはランダムに打つ自分のアルゴリズムが試行回数内に勝つことを祈るのみですw

本番では別のところに予期せぬ問題 *1 が発生して上手くいきませんでしたが、イベント終了後に不整合を修正したら僕の AI が勝ちました!

我々が勝手に考えたルールの 4 色オセロなので、定石なんてものはありません。また、総当たりに近い計算をしてしまうと (計算量が多くなってしまうために) 現実的な時間で結果を出せなくなってしまいます。そういったところに対して、勝率と実装コストのバランス両方を考慮したアルゴリズムを考えられたと思います。

セッション資料

関連記事

東北関連の与太話

ずいぶんと話を戻せば、僕がプログラミング関連の勉強を頑張るようになったのは東北大震災が大きなキッカケのひとつでした。当時の僕は Windows のデスクトップアプリしか作ることができず、Web/クラウドの知識も一切なかったので、エンジニアであるにも関わらずプログラムを使った即効性のある支援が一切できませんでした。それがとっても悔しくって Web/クラウド関連に触れるようになりました。それから数年経って東北の地に行って、何もできなかったあの頃を懐かしいなぁと思うのと同時に、もうちょっと頑張らなきゃなって少しヤル気が出ました。

*1:@AILight さんの実装との相性問題があり、事前評価で使う AI と実行時の AI が異なってしまうという不具合

gRPC / MagicOnion 入門 (13) - 送受信されているデータを可視化する

gRPC に限った話ではありませんが、ネットワークを介して送受信しているデータをダンプして目視確認したいことは頻繁にあります。このようなとき、たいてい以下のような方法が採られます。

用途 実装
個別 ダンプしたい箇所にデバッグコードを仕込む
全体 全 API の入口/出口となる箇所にデバッグコードを仕込む

MagicOnion には後者 (全通信内容をダンプする) の機能が標準で用意されているので、今回はこれを利用してみます。

ダンプ用のロガーを仕込む

方法は非常に簡単で、MagicOnionEngine の生成時にロガーを仕込んだオプションを設定するだけです。

using System;
using Grpc.Core;
using Grpc.Core.Logging;
using MagicOnion.Server;

namespace MagicOnionSample.Service
{
    class Program
    {
        static void Main()
        {
            //--- gRPC のログをコンソールに出力
            GrpcEnvironment.SetLogger(new ConsoleLogger());

            //--- MagicOnion 側のログを gRPC のログに流し込む
            //--- そのとき名前付き (JSON 形式) でデータをダンプ
            var logger = new MagicOnionLogToGrpcLoggerWithNamedDataDump();
            var options = new MagicOnionOptions(true){ MagicOnionLogger = logger };
            var service = MagicOnionEngine.BuildServerServiceDefinition(options);

            //--- いつも通りの起動
            var port = new ServerPort("localhost", 12345, ServerCredentials.Insecure);
            var server = new Server(){ Services = { service }, Ports = { port } };
            server.Start();  // launch gRPC server
            Console.ReadLine();
        }
    }
}

実行してみる

これでダンプの準備は整いました。では、以下のような API で通信してみましょう。

using MagicOnion;

namespace MagicOnionSample.ServiceDefinition
{
    public interface ISampleApi : IService<ISampleApi>
    {
        UnaryResult<double> Sample(Vector2 value);
    }
}
using System;
using MagicOnion;
using MagicOnion.Server;
using MagicOnionSample.ServiceDefinition;

namespace MagicOnionSample.Service
{
    public class SampleApi : ServiceBase<ISampleApi>, ISampleApi
    {
        public async UnaryResult<double> Sample(Vector2 value)
        {
            //--- テキトーにピタゴラスでも
            var x2 = value.X * value.X;
            var y2 = value.Y * value.Y;
            return Math.Sqrt(x2 + y2);
        }
    }
}
using System;
using System.Threading.Tasks;
using Grpc.Core;
using MagicOnion.Client;
using MagicOnionSample.ServiceDefinition;

namespace MagicOnionSample.Client
{
    class Program
    {
        static void Main() => MainAsync().Wait();

        static async Task MainAsync()
        {
            var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);
            var client = MagicOnionClient.Create<ISampleApi>(channel);

            //--- こんな感じのデータを投げてみる
            var v = new Vector2(3, 4);
            var result = await client.Sample(v);

            Console.ReadLine();
        }
    }
}

実行すると以下のようなログが出力されます。

D0911 13:58:21.959994 BeginInvokeMethod type:Unary method:/ISampleApi/Sample size:11 dump:{"X":3,"Y":4}
D0911 13:58:21.979015 EndInvokeMethod type:Unary  method:/ISampleApi/Sample size:9 elapsed:86.2561  dump:5

たった 1 行 2 行の簡単なログですが、ここから以下のことが読み取れます。

  • 通信した時刻
  • API に入ったときか出て行くときか
  • 通信方法は何か (Unary / ServerStreaming / ClientStreaming / DuplexStreaming)
  • どの API にアクセスしたか
  • データサイズはどれだけか
  • API の処理時間
  • データをダンプした結果

デバッグ時に非常に役立つと思うので、ぜひ使ってみてください :)