xin9le.net

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

gRPC / MagicOnion 入門 (5) - Server Streaming 通信

前回は最も簡単な Unary 通信について解説しました。今回はサーバーからのプッシュ配信を行うための Server Streaming 通信について見ていきます。

f:id:xin9le:20170604153542p:plain

Step.1 - サービス定義

まずサーバー側で提供するサービスのインターフェースを定義します。例えば以下のようになります。

using System.Threading.Tasks;
using MagicOnion;

namespace MagicOnionSample.ServiceDefinition
{
    public interface ISampleApi : IService<ISampleApi>
    {
        Task<ServerStreamingResult<int>> Repeat(int value, int count);
    }
}

ポイントは以下の通りですが、Unary 通信のときと比べ戻り値以外の違いはありません。

  • IService<T> インターフェースを実装する
  • IService<T> の型引数には自身のインターフェースを入れる
  • メソッドの戻り値を Task<ServerStreamingResult<T>> にする

Step.2 - サービスの実装

Step.1 で定義したインターフェースを実装します。例えば以下のような感じです。

using System;
using System.Linq;
using System.Threading.Tasks;
using MagicOnion;
using MagicOnion.Server;
using MagicOnionSample.ServiceDefinition;

namespace MagicOnionSample.Service
{
    public class SampleApi : ServiceBase<ISampleApi>, ISampleApi
    {
        public async Task<ServerStreamingResult<int>> Repeat(int value, int count)
        {
            Console.WriteLine($"(value, count) = ({value}, {count})");

            //--- WriteAsync するたびにレスポンスが返る
            var streaming = this.GetServerStreamingContext<int>();
            foreach (var x in Enumerable.Repeat(value, count))
                await streaming.WriteAsync(x);

            //--- 完了信号を返す
            return streaming.Result();
        }
    }
}

ポイントは以下の 4 点です。

  • GetServerStreamingContext<T> からストリーミング通信するためのコンテキストを取得
  • WriteAsync メソッドで値をクライアント側に返す (= プッシュ配信)
  • Result メソッドで完了通知を送る
  • プッシュ配信はサーバー側のメソッドが呼び出されてから終了するまでの間に行う

SignalR などでの開発をしたことがある方は、ここでひとつ疑問に思うかもしれません。たとえば「Unary 通信をしている途中で Server Streaming で複数のユーザーに結果を返したいときはどうしたら良いのか」などです。今回は説明を割愛しますが、ちょこっと手間をかければ可能です。これについては別途解説したいと思います。

Step.3 - クライアントの実装

最後に、Step.2 までで実装した API を呼び出すクライアントを作成します。以下のような感じです。

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

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

        static async Task MainAsync()
        {
            //--- API に接続するためのチャンネルとクライアントを生成
            var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);
            var client = MagicOnionClient.Create<ISampleApi>(channel);

            //--- サーバーが WriteAsync すると ForEachAsync が動く
            //--- サーバーから完了信号が送られると ForEachAsync が終了する
            var streaming = await client.Repeat(3, 5);
            await streaming.ResponseStream.ForEachAsync(x => Console.WriteLine($"Result : {x}"));

            //--- アプリが終わらないように
            Console.ReadLine();
        }
    }
}

サーバーからの送信されたメッセージは ResponseStream から流れてきます。標準ではこれを MoveNext / Current で読み出すのですが、これは結構に煩雑な書き方です。なので MagicOnion では簡略化した方法として ForEachAsync メソッドを用意しています。ForEachAsync メソッドを利用すれば、await が完了するまでストリームを読み続けることができます。

実行してみる

実行すると以下のような結果が得られます。思い通りの動きをしていますね。

f:id:xin9le:20170606011225p:plain

gRPC / MagicOnion 入門 (4) - Unary 通信

ここから実際に gRPC で API を作成し、クライアント/サーバー間で通信していきます。まず「gRPC / MagicOnion 入門 (2) - 4 種類の通信方式」で紹介した最も簡単な Unary 通信から見て行きます。

f:id:xin9le:20170604153513p:plain

Step.1 - サービス定義

最初のステップでは、サーバー側にどのような API を用意するのか、そのサービスのインターフェースを決定していきます。例えば以下のような感じになります。

using MagicOnion;

namespace MagicOnionSample.ServiceDefinition
{
    public interface ISampleApi : IService<ISampleApi>
    {
        UnaryResult<int> Sum(int x, int y);
    }
}

非常にシンプルなインターフェースです。ポイントは以下の 3 点のみです。

  • IService<T> インターフェースを実装する
  • IService<T> の型引数には自身のインターフェースを入れる
  • メソッドの戻り値を UnaryResult<T> にする

Step.2 - サービスの実装

Step.1 で定義したインターフェースを実装する形でサービスの中身を書いていきます。今回は加算メソッドなので、例えば以下のような感じになります。

using System;
using MagicOnion;
using MagicOnion.Server;
using MagicOnionSample.ServiceDefinition;

namespace MagicOnionSample.Service
{
    public class SampleApi : ServiceBase<ISampleApi>, ISampleApi
    {
        //--- async/await をそのまま書ける
        public async UnaryResult<int> Sum(int x, int y)
        {
            Console.WriteLine($"(x, y) = ({x}, {y})");
            await Task.Delay(10);  // 何か非同期処理したり
            return x + y;
        }

        /*
        //--- 「await がないぞ!」と警告されるけれど、これでも OK
        //--- プロジェクトレベルで警告 CS1998 を抑制するのがオススメです
        public async UnaryResult<int> Sum(int x, int y)
            => x + y;

        //--- async を使わない書き方もできるけれどスマートじゃないので not recommend
        public UnaryResult<int> Sum(int x, int y)
            => new UnaryResult<int>(x + y);
        */
    }
}

実装も非常にシンプルだと思います。ポイントは以下の 3 点です。

  • ServiceBase<T> を継承する
  • サービス定義インターフェースを実装する
  • ServiceBase<T> の型引数にはサービス定義のインターフェースを入れる

また、UnaryResult<T> は C# 7.0 で追加された Task-like に対応しているため、C# 7.0 以降であれば Task<T> 抜きで自然に async/await を記述することができます。

作者である @neuecc さんによる解説記事はこちら。

neue cc - C# 7.0 custom task-like の正しいフレームワークでの利用法

ちなみに「async キーワードがあるのにメソッド本体に await がない」という警告 (CS1998) をプロジェクトレベルで抑制する場合は、プロジェクトのプロパティで以下のように設定します。

f:id:xin9le:20170605233428p:plain

Step.3 - gRPC サーバーを起動

ここまでで API 本体の実装が終わったので、次に gRPC サーバーを起動します。以下のようなコードを書けば OK です。

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

namespace MagicOnionSample.Service
{
    class Program
    {
        static void Main()
        {
            //--- ここで動的にアセンブリを解釈し、通信されてきたデータと API 本体とのマップを作る
            var service = MagicOnionEngine.BuildServerServiceDefinition();

            //--- API を公開する IP / Port / 認証情報などを決定し、gRPC サーバーを起動
            var port = new ServerPort("localhost", 12345, ServerCredentials.Insecure);
            var server = new Server{ Services = { service }, Ports = { port } };
            server.Start();

            //--- exe が終了しちゃわないように
            Console.ReadLine();
        }
    }
}

これでサーバー側の実装はすべて完了です。実行すれば localhost:12345 で gRPC サーバーが起動します。

Step.4 - クライアントの実装

最後に、Step.3 までで実装した API を呼び出すクライアントを作成します。以下のような感じです。

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()
        {
            //--- API が公開されている IP / Port / 認証情報を設定して通信路を生成
            var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);

            //--- 指定されたサービス定義用のクライアントを作成
            var client = MagicOnionClient.Create<ISampleApi>(channel);

            //--- RPC 形式で超お手軽呼び出し
            var result = await client.Sum(1, 2);

            //--- 結果表示
            Console.WriteLine($"Result : {result}");
            Console.ReadLine();
        }
    }
}

見た通り、非常にシンプルな書き心地でサーバー側のメソッドを叩くことができます。

実行してみる

実行してみると (当然ですが) 以下のような結果が得られます。

f:id:xin9le:20170604231733p:plain

あとは同様の手順で API を増やしていくだけです。簡単ですね!

gRPC / MagicOnion 入門 (3) - プロジェクト構造と実装の前準備

今回は MagicOnion を使った API 開発を行うまでの前準備にフォーカスを当てます。主に Console / WinForms / WPF アプリなど向けです。MagicOnion は Unity にも対応していますが、これはだいぶ大変でややこしいので別途解説することにします。

基本的なプロジェクト構造

MagicOnion は基本的に以下のようなプロジェクト構成をとります。

プロジェクト 役割と解説
ServiceDefinition API 定義のみ
サーバー / クライアントの両方から参照される
Service サーバー側
ServiceDefinition で定義されたインターフェースを実装
Client クライアント側
ServiceDefinition で定義されたインターフェースを通してサーバー側の API を呼び出す

f:id:xin9le:20170604184611p:plain

通常の gRPC は Protocol Buffers の IDL を用いたサービス定義を行いますが、MagicOnion は IDL が不要*1です。その代わりにサービス定義をサーバーとクライアントで共有する必要があるため、サービス定義部分だけ別プロジェクトとして分離する必要があります。

NuGet Package の取得

MagicOnion は NuGet で配布されており、以下のコマンドで取得できます。

PM> Install-Package MagicOnion

このパッケージを前節で挙げたすべてのプロジェクトの参照設定に追加する必要があります。gRPC や MessagePack for C# などの MagicOnion 自体が利用するものは、MagicOnion をインストールする際に同時に取得されるので個別での対応は不要です。

*1:C# コード自体を IDL として利用し、実行時に動的解決する

gRPC / MagicOnion 入門 (2) - 4 種類の通信方式

gRPC は HTTP/2 の仕様に則った形で 4 種類の通信方式を提供しています。以下のドキュメントにも解説がありますが、簡単に図解します。

f:id:xin9le:20170604212009p:plain

Unary RPCs

最もシンプルな 1 リクエスト / 1 レスポンス型の通信方法です。通常の関数コールのように扱えるため、非常に分かりやすいのが特徴です。

f:id:xin9le:20170604153513p:plain

Server Streaming RPCs

クライアントが 1 度リクエストを送信し、サーバーから複数回数のレスポンスを返す方式です。クライアントはサーバーから送信完了の信号が来るまでストリームからメッセージを読み続けます。いわゆるサーバープッシュ型の実装に利用できます。

f:id:xin9le:20170604153542p:plain

Client Streaming RPCs

クライアントから複数回数のリクエストを送信し、それらを読み込んだサーバー側から 1 度のレスポンスを待つ方式です。サーバーはクライアントからリクエストの送信完了信号が来るまでストリームからメッセージを読み続け、レスポンスを返しません。

f:id:xin9le:20170604153550p:plain

Duplex Streaming RPCs

双方向ストリーミングの通信方式です。2 つのストリームはそれぞれ独立しているため、クライアントとサーバーはどのような順序でも読み書きが可能です。リクエストとレスポンスが 1 対 1 である必要がなく、扱い/実装共に難易度が高いのが特徴です。各ストリームのメッセージ送信順序は保証されます。

f:id:xin9le:20170604153558p:plain

ドキュメントには「Bidirectional Streaming」と記載されている場合がありますが、gRPC のコード上では「Duplex Streaming」とされているので、この連載では「Duplex Streaming」を使って解説を進めます。

gRPC / MagicOnion 入門 (1) - 概要

gRPC は Google が開発している HTTP/2 ベースの RPC 通信フレームワークで、以下のような特徴を備えています。

  • Protocol Buffers を利用したサービス定義
  • 多数のプラットフォーム/言語をサポート
  • HTTP/2 ベースの高パフォーマンスなストリーミング通信
  • 統合されたプラガブルな認証機構

gRPC は GitHub にて今も積極的に開発が進められており、Google も自身の Google Cloud Platform の API などとして積極的に採用しています。

f:id:xin9le:20170603185008p:plain

これまで HTTP/1 ベースでのストリーミング通信と言えば WebSocket などが有名ですが、通常の HTTP リクエストとは API が統合されておらず、サービスの口を 2 系統用意する必要がありました。ASP.NET に限って言えば、同様のコンセプトの SignalR がありますが、gRPC の方が高速で拡張性があり、先進的であると言えます。

MagicOnion

@neuecc さんが開発している、.NET 用の gRPC を薄くラップした高レベルな通信フレームワークです。素の gRPC の良さをそのまま残しつつ、C#er がより便利に利用できるように調整されています。

  • .NET Framework / .NET Core / Unity に対応
    • Unity 向けの gRPC Port を含み、それを利用して Unity で動作させる
  • MessagePack for C# を用いた超高速シリアライズ
  • Protocol Buffers を使わないため .proto (IDL : 中間定義) が一切不要
  • Heartbeat によるサーバー/クライアントでの切断検知
  • OWIN / ASP.NET Core MVC ライクな Filter による制御
  • Swagger を用いた HTTP/1 との相互運用

MagicOnion はグラニが開発/運用している黒騎士と白の魔王Project Sonata で採用されています。ただし、もし実プロダクトとしての採用を検討する場合は十分な検証を行ってください。

今すぐ試してみたいという方は、Quick Start が MagicOnionREADME.md に記載されているので是非トライしてみてください。