xin9le.net

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

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 を増やしていくだけです。簡単ですね!