開発予定の C# を対象に調査していきます。
0.情報収集
gRPC の情報は比較的集めやすいです。
gRPC 公式の C# クイックスタート
gRPC 公式の C# チュートリアル
英語サイトですが平易な文体で記述されています。Google 翻訳を利用すれば内容は十分に把握できると思います。
gRPC 公式の C# サンプルリポジトリ(Github)
gRPC でサポートされている4つの通信方式( Unary, Server Streaming, Client Streaming, Duplex Streaming )の簡単なサンプルがあります。
MagicOnion のリポジトリ(Github)
MagicOnion は neuecc さんが開発している gRPC ラッパーです。非常に高機能なのですが C# に限定されている(限定することによる最適化を実現)ため、残念ながら私の目的には合わず。設計思想は非常に参考になります。
gRPC / MagicOnion 入門 - xin9le.net
MagicOnion が対象になっていますが gRPC そのものについての説明も多く、4つの通信方式の仕組みなどがわかりやすく説明されています。
1.プロジェクトの作成
Visual Studio で新規プロジェクトを作成します。.NET Framework の対応バージョンは 4.5 以降です。次の三つのプロジェクトを含むソリューションを作成しました。プロジェクトの種類は特に決まりはありません。gRPC ではタイプセーフティな API 定義をサーバープログラムとクライアントプログラムで共有しますので、共有ライブラリを作成しています。
- サーバープログラム:コンソール
 - クライアントプログラム:Windows フォームアプリケーション
 - 共有ライブラリ:クラスライブラリ
 
NuGet を使って各プロジェクトに gRPC のパッケージを追加します。
- Grpc
 - Grpc.Core ※Grpc を追加すると同時に追加されます。
 - Grpc.Tools ※これは参照設定するものではなく、ツールです。何れか一つのプロジェクトに追加すればよいです。
 - Google.ProtoBuf
 
2.API 定義の作成
gRPC には API のサービスや引数/戻り値の型を自動生成してくれるツールが存在しています。Grpc.Tools です。NuGet で追加したのであれば、ソリューションの packages フォルダに次の二つの exe ファイルが存在しているはずです。
場所:{ソリューションフォルダ}\packages\Grpc.Tools.1.8.3\tools\{プラットフォーム}
ファイル:protoc.exe, grpc_csharp_plugin.exe
これは PROTOC と呼ばれるソースコードジェネレーターツールとその C# 用プラグインです。Protocol Buffers の定義ファイル( .proto ファイル )にサービスや引数/戻り値の定義を記述し、このツールで C# のソースファイルを生成することになります。
Person クラスと Person を対象に検索を行う検索サービスクラスを定義してみました。
[ Person.proto ]
syntax = "proto3";
option csharp_namespace = "gRpcTest.Models";
message Person {
    string Code = 1;
    string Name = 2;
    int32 Age = 3;
}
[ PersonService.proto ]
syntax = "proto3";
import "person.proto";
option csharp_namespace = "gRpcTest.Services.PersonSearch";
message Request {
    string name = 1;
}
message Response {
    repeated Person Persons = 1;
}
service Service {
    rpc Search (Request) returns (Response){}
}
これらの定義ファイルから C# ソースファイルを生成するためのバッチファイルを作成します。
[ generate.bat ]
set PROTOC_DIR={ソリューションフォルダ}\packages\Grpc.Tools.1.8.3\tools\windows_x64\
set PROJECT_DIR={プロジェクトフォルダ}\
cd %PROTOC_DIR%
protoc
    person.proto
    --proto_path %PROJECT_DIR%proto.IDL
    --csharp_out %PROJECT_DIR%proto.csharp
    --grpc_out %PROJECT_DIR%proto.csharp
    --plugin=protoc-gen-grpc=%PROTOC_DIR%grpc_csharp_plugin.exe
protoc
    personService.proto
    --proto_path %PROJECT_DIR%proto.IDL
    --csharp_out %PROJECT_DIR%proto.csharp
    --grpc_out %PROJECT_DIR%proto.csharp
    --plugin=protoc-gen-grpc=%PROTOC_DIR%grpc_csharp_plugin.exe
protoc の行は実際には改行していません。オプションの意味は次の通りです。 - --proto_path:proto ファイルの格納ディレクトリ
 - --csharp_out:C# ソースファイルの出力ディレクトリ
 - --grpc_out:C# ソースファイルの出力ディレクトリ(サービス型?)
 - --pulugin=protoc-gen-grpc:言語プラグイン
 
3つの C# ソースファイルが生成されました。proto ファイルと同じ名前で作成され、サービスの定義が含まれる場合にはサービス周りの実装が *Grpc.cs ファイルに分離されるようです。
- Person.cs
 - PersonService.cs
 - PersonServiceGrpc.cs
 
生成された C# ソースファイルには次のクラスが定義されていました。斜体は proto ファイルのファイル名、太字は proto ファイルに記述した定義名に由来しています。
- gRpcTest.Models.PersonReflection
 - gRpcTest.Models.Person
 - gRpcTest.Services.PersonSearch.PersonServiceReflection
 - gRpcTest.Services.PersonSearch.Request
 - gRpcTest.Services.PersonSearch.Response
 - gRpcTest.Services.PersonSearch.Service
 - gRpcTest.Services.PersonSearch.Service.ServiceBase
 - gRpcTest.Services.PersonSearch.Service.ServiceClient
 
それぞれの定義内容は次の通りです。
- Person.proto ファイルの記述内容に対する Descriptor
 - Person クラス
 - PersonService.proto ファイルの記述内容に対する Descriptor
 - Request クラス
 - Response クラス
 - Service に関する静的メンバーと 7, 8 の入れ子クラスが定義された静的クラス
 - Service サービスクラスの基底実装クラス
 - Service クライアントクラス
 
アプリケーションコードから主に使用するのは、2, 4, 5, 7, 8 になると思います。サービス定義はこの生成ルールを考慮して proto ファイル上の定義名を決めるとよさそうです。ちなみに今回の例はあまりよい例ではありませんね。複数のサービスを呼び出す場合にクラス名だけで判別できず、名前空間を記述しなくてはならなくなります。
3.アプリケーションへの組み込み
生成された C# ソースファイルを共有ライブラリに追加してビルドします。Grpc.Core と Google.Protobuf が参照設定されている必要があります。
proto ファイルからのソースファイルの生成を繰り返すことになると想定されますので、 自動化の仕組みを考えておいたほうがよさそうです。
4.サーバー側の実装
サービスクラスの基底実装クラス(前述の例ではファイル7)を継承したサービスクラスを実装します。proto ファイルに定義した rpc メソッドが抽象メソッドとして実装されていますので、オーバーライドします。
[ PersonServiceImpl.cs ]
using gRpcTest.Services.PersonSearch;
internal class PersonServiceImpl : Service.ServiceBase
    public override Task<Response> Search(
        Request request
        , ServerCallContext context)
    {
        // 検索処理を実装
    }
    …
}
今回はサーバープログラムはコンソールアプリケーションにしましたので、Main メソッドにサービスの起動処理を実装します。サービスクラスに実装されている BindService メソッドを使用してサーバーに登録します。
[ Program.cs ]
using Grpc;
using Grpc.Core;
using gRpcTest.Services.PersonSearch;
static void Main(string[] args)
{
    Server server = null;
    try
    {
        server = new Server();
        server.Services.Add(Service.BindService(new PersonServiceImpl()));
        server.Ports.Add("localhost", 8081, ServerCredentials.Insecure);
        server.Start();
        Console.ReadLine();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
    finally
    {
        server.ShutdownAsync().Wait();
    }
}
5.クライアント側の実装
チャネルとサービスクライアントを生成し、サービスクライアントのメソッドを呼び出せば gRPC によるリモート呼び出しを行うことができます。次の例は最も簡単な Unary 通信を行うメソッドの例です。ストリーミング通信の場合にはフローが異なります。
[ Form1.cs ]
using Grpc;
using Grpc.Core;
using gRpcTest.Models;
using gRpcTest.Services.PersonSearch;
// サービスクライアントを生成
Channel channel = new Channel("localhost:8081", ChannelCredentials.Insecure);
ServiceClient client = new ServiceClient(channel);
// Search メソッドを呼び出し
Request request = new Request();
request.Name = "あ";
Response response = client.Search(request);
foreach ( Person person in response.Persons )
{
    Debug.WriteLine(person.Name);
}
呼び出すだけであれば非常に簡単に実装できますが、実際には例外処理やログ出力や各種割込み処理を行う必要があることを考えると自動生成されたサービスクライアントのメソッドをそのまま呼び出すのは適切でないと思います。汎用的なラッパークラスを独自に実装し、ラッパークラス経由でサービスクライアントのメソッドを呼び出すようにするのがよさそうです。
6.今後の取り組み
実際に開発プロジェクトで採用するには、まだ調査が必要です。
- セキュリティ。SSL/TSL の適用や Google 認証はサポートされているようですが、独自に暗号化や認証を行うときにどのように実装できるか。
 - Protocol Buffer の proto ファイルを効率的に管理する仕組み。バージョン管理は Git などを利用するとして、エディタやスキーマ定義から自動生成するようなツールがないか。
 - 汎用的なラッパークラスの設計。サービスメソッドの実行前/実行後処理や例外ハンドリング、比較的実装フローが複雑なストリーミング通信のラップなど、汎用化したほうがよい実装がたくさんあります。
 - 効率的なテストの仕組み。Visual Studio のテスト機能を利用するか、その他のテストツールを導入するか。
 
0 件のコメント:
コメントを投稿