C#でgRPCクライアントを開発する――.NETのgRPCクライアントサポート:スキマ時間にこっそり学ぶ「gRPC」入門(3)
第3回は、連載第2回で作成したシンプルな既定のサーバアプリケーションに具体的なサービスを実装し、それにアクセスするクライアントアプリケーションを作成します。サーバアプリケーションと同じくC#と.NET 6で開発していきます。プロトコル定義ファイルはサーバとクライアントで共有できること、クライアントは手続きの呼び出しのみに集中すればよいことなどを紹介します。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
書籍情報検索サービスをテーマに、gRPCを使ったアプリケーションを実装してみる
連載第2回では、.NETにおけるgRPCのサポートを紹介しました。既定のサーバアプリケーションを作成し、それを汎用(はんよう)のgRPCクライアントEvansからアクセスして、動作を確認しました。さらに、サーバアプリケーションの基本的な構造にも触れました。
今回は、第2回の内容を踏まえた上でgRPCサーバとクライアントを実装していきます。作成するアプリケーションのテーマは「書籍情報検索サービス『BookInfo』」です。BookInfoは、静的に用意されたリストからキーワード検索し、結果を返すだけというシンプルなものですが、gRPCを用いて独自のサービスを実装したいときの手順として参考になるでしょう。
作業の内容は、既定のファイルを複製し、BookInfoサービスに合わせたクラス名などを修正していくというイメージです。大まかに、以下の手順でサービスを実装していきます。
- プロトコル定義ファイルの配置と修正(Protos/bookinfo.protoファイル)
- プロジェクト設定ファイルの修正とビルド(GrpcBookInfo.csprojファイル)
- サービス記述ファイルの複製と修正(Services/BookInfoService.csファイル)
- アプリケーション起動時のサービスの登録(Program.csファイル)
- 検索手続きの拡張(Services/BookInfoService.csファイル)
プロトコル定義ファイルを配置して修正する
BookInfoサービスのためのプロトコル定義ファイルをProtosフォルダに配置します。連載第1回で作成したbook.protoファイルを、bookinfo.protoファイルとしてProtosフォルダにコピーします。そして、以下のように修正しておきます。変更の多くは、C#のクラス名やフィールド名の衝突や混同を避けるためのものです。
// パッケージ名を変更する:AtmarkIt⇒bookinfo package bookinfo; …略… // GreeterService.csと同じ内容とするためにオプションを追加する option csharp_namespace = "GrpcBookInfo"; …略… // 名前の衝突を避けるためにBook⇒BookItemとメッセージ名を変更する message BookItem { …略… } …略… message SearchResponse { int32 status_code = 1; BookItem item = 2; // メッセージ名BookItemに合わせて変更 } // 名前の衝突を避けるためBookService⇒BookInfoと変更する service BookInfo { rpc Search(SearchRequest) returns (SearchResponse); }
プロジェクト設定ファイルを修正してビルドする
プロトコル定義ファイルbookinfo.protoファイルを配置、修正したら、プロジェクト設定ファイルであるGrpcGreeterClient.csprojに、このプロトコル定義ファイルのための<ItemGroup>要素を追加します。
…略… <ItemGroup> <Protobuf Include="Protos\bookinfo.proto" GrpcServices="Server" /> </ItemGroup> …略… </Project>
【補足】<ItemGroup>の内容
<ItemGroup>には、ビルド時に参照されるユーザー定義の要素を格納します。ここでは、<Protobuf>が相当します。これは、内部ツールであるdotnet-grpcがその処理のために参照する要素となります。Include属性は読み込むプロトコル定義ファイルであり、GrpcServices属性の"Server"は、サーバのために構成することを意味しています。
ここで、dotnet buildコマンドを実行しておきます。GrpcBookInfo/obj/Debug/net6.0/Protosフォルダ以下に、bookinfo.protoファイルから生成されるC#のソースファイルが2個(Bookinfo.cs、BookinfoGrpc.cs)追加されます。それぞれの役割は、連載第2回で大まかに紹介したように、以下の通りとなっています。
- BookInfo.csファイル……プロトコル定義ファイルに基づくメッセージのクラス(POCO)のコードが格納される
- BookInfoGrpc.csファイル……シリアライズなどの処理を実装したクラスのコードが格納される
ファイルを表示させてみると分かりますが、「DO NOT EDIT!」とコメントされているように、これらのファイルは参照専用です。どのようにProtocol Buffersの定義がクラスに反映され、どのようにシリアライズされているかということに興味があれば、中をのぞいてみるのもよいでしょう。しかし、そういったことに関知せずにgRPCを利用できるのがツールを使うメリットでもありますから、ここではこれ以上掘り下げないことにします。
サービス記述ファイルを複製して修正する
BookInfoサービスに必要なクラスの準備ができましたので、サービスの処理内容を記述できます。既定のGreeterService.csファイルをBookInfoService.csとしてコピーし、以下のようにクラス名などを修正します。
…略… // クラス名を修正する // public class GreeterService : Greeter.GreeterBase // ↓ public class BookInfoService : BookInfo.BookInfoBase { // クラス名を修正する // private readonly ILogger<GreeterService> _logger; // ↓ private readonly ILogger<BookInfoService> _logger; // クラス名を修正する // public GreeterService(ILogger<GreeterService> logger) // ↓ public BookInfoService(ILogger<BookInfoService> logger) …略… // gRPC手続きに合わせてメソッド名、クラス名を修正する // public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) // ↓ public override Task<SearchResponse> Search(SearchRequest request, ServerCallContext context) { // クラス名を修正する // return Task.FromResult(new HelloReply // ↓ return Task.FromResult(new SearchResponse { // 戻り値を構築する StatusCode = 0, (1) Item = new BookItem { Title = request.Text } }); } }
(1)以降の、戻り値を生成する部分では、さらにオブジェクト初期化子の構文でBookItemメッセージを生成しています。ここではTitleフィールドのみ検索語をそのまま設定していますが、他のフィールドにはデータ型ごとの既定値が入ることに注意してください。BookInfoサービスのSearchは検索のための手続きなので、実際にはDBなどからrequest.Textをキーにして検索し、結果を返すことになります。
アプリケーション起動時にサービスを登録して実行する
アプリケーションの起動ファイルProgram.csに、新しく作成したgRPCサービスを登録する行を追記します。Greeterサービスは不要なので、コメントアウトすればサービスは登録されなくなります。
//app.MapGrpcService<GreeterService>(); // コメントアウト app.MapGrpcService<BookInfoService>(); // 追加
ここで、dotnet runコマンドでサーバアプリケーションをビルド、実行します。問題なければ、連載第2回でインストールしたEvansをREPLモードで起動し、以下のように手続き呼び出しが成功するかどうかを確認しましょう。
% evans --proto Protos/bookinfo.proto --host localhost --port 5299 repl …略… bookinfo.BookInfo@localhost:5299> package bookinfo bookinfo@localhost:5299> service BookInfo bookinfo.BookInfo@localhost:5299> call Search text (TYPE_STRING) => Rust { "item": { "title": "Rust" } }
検索手続きを拡張する
ここまでで、サーバアプリケーションのミニマムな実装が完了しました。ただし、検索手続きSearchはほとんどダミーといえる状態でしたので、検索できるように機能を拡張してみましょう。とはいえ、静的に準備された書籍データから検索するのみです。実用的なサービスでは、DBやWebなどからの検索を実装するところですが、本連載のテーマとは関係が薄いため、単純化することにします。
サービスのクラスに、以下のように検索対象のフィールドを用意し、コンストラクタにて初期化します。検索手続きでは、検索対象フィールドから検索を実行して、その結果を返すようにします。
…略… // 検索対象のフィールドを追加 private readonly List<BookItem> _items; …略… // コンストラクタに初期化処理を追加する public BookInfoService(ILogger<BookInfoService> logger) { _logger = logger; _items = new List<BookItem> { new BookItem { Title = "Rust", Author = "Yamauchi" }, new BookItem { Title = "gRPC", Author = "Nao" }, new BookItem { Title = "WebAssembly", Author = "TamaDigi" } }; } …略… // 検索手続きを修正する public override Task<SearchResponse> Search(SearchRequest request, ServerCallContext context) { // 戻り値はStatusCodeを0として初期化する SearchResponse response = new SearchResponse{ StatusCode = 0 }; // すべての要素に対して実行する _items.ForEach(item => { // 見つかっていなければ、検索語を含めば成功とする if (response.StatusCode == 0) { if (item.Title.Contains(request.Text) || item.Author.Contains(request.Text)) { response.StatusCode = 1; response.Item = item; } } }); return Task.FromResult(response); }
これで、サーバアプリケーションへのBookInfoサービスの実装が完了しました。
クライアントアプリケーションの作成
ここからは、BookInfoサービスのためのクライアントアプリケーションを作成していきます。このアプリケーションは、コマンドラインパラメーターにて検索語を指定し、サーバの検索手続きを呼び出して、その結果を表示するものとします。大まかに、以下の手順で作成していきます。
- クライアントアプリケーションのベースを作成する
- ライブラリやツールなどをインストールする
- プロトコル定義ファイルを作成する(Protos/bookinfo.protoファイル)
- プロジェクト設定ファイルを修正する(GrpcBookInfo.csprojファイル)
- クライアントのためのコードを追加する(Program.csファイル)
クライアントアプリケーションのベースを作成する
サーバアプリケーションのプロジェクトをVSCodeで開いたままであれば、ターミナルでGrpcBookInfoフォルダがカレントフォルダになっているはずなので、1階層上がり、そこで以下のようにdotnet newコマンドを実行します。
% dotnet new console -o GrpcBookInfoClient テンプレート "コンソール アプリ" が正常に作成されました。 …略…
ここでは、テンプレートに「console」、出力先に「GrpcBookInfoClient」を指定しています。「テンプレート "コンソール アプリ" が正常に作成されました。」とあるように、gRPCとは全く関係のないコンソールアプリケーションを作成しています。gRPCサーバアプリケーションのようなテンプレートは、クライアントアプリケーションには用意されていないのです。
そのため、gRPCクライアントアプリケーションとするためには、幾つかの設定やコードの追加が必要になってきます。そこで、サーバアプリケーションと同様に、生成されたプロジェクトのフォルダ(GrpcBookInfoClient)をワークスペースに追加しておきます。このあと、サーバ用とクライアント用でターミナルを開いて、それぞれでプロジェクトのルートフォルダに移動しておくと便利です。
ライブラリとツールなどをインストールする
gRPCクライアントに必要な、ライブラリとツールなどのNuGetパッケージをインストールします。アプリケーションのルートフォルダGrpcBookInfoClientがカレントフォルダであることを確認してください。NuGetパッケージは、アプリケーションごとにインストールが必要なためです。
そして、以下のようにdotnet add packageコマンドを使って3つのパッケージ(gRPCクライアントのためのGrpc.Net.Client、Protocol BuffersのためのGoogle.Protobuf、protocツールなどのGrpc.Tools)をインストールしてください。「info」や「log」などのみが表示され、特にエラー表示がなければ問題なくインストールされています。
% dotnet add GrpcBookInfoClient.csproj package Grpc.Net.Client …略… % dotnet add GrpcBookInfoClient.csproj package Google.Protobuf …略… % dotnet add GrpcBookInfoClient.csproj package Grpc.Tools …略…
プロトコル定義ファイルを作成する
次に、プロトコル定義ファイルであるbookinfo.protoをプロジェクトに追加します。せんだって作成したgRPCサーバアプリケーションに作成したbookinfo.protoファイルを、クライアントアプリケーションの方にコピーします。プロトコル定義ファイルは、サーバとクライアントで共通だからです。GrpcBookInfoフォルダ以下のProtos/bookinfo.protoファイルを、GrpcBookInfoClientフォルダにProtosフォルダを作成してコピーします。
コピーしたbookinfo.protoファイルには、options命令にて名前空間が設定されています。そのままではサーバアプリケーションの名前空間になっていますので、これをクライアントアプリケーションの名前空間に変更します。
option csharp_namespace = "GrpcBookInfoClient"; // 変更
プロジェクト設定ファイルを修正する
そして、サーバアプリケーションと同様にクライアントアプリケーションのGrpcBookInfoClient.csprojファイルに<ItemGroup>要素を追加します。<Protobuf>の役割はサーバアプリケーションと同様ですが、GrpcServices属性の値が"Client"となっていることに注意してください。これは、gRPCクライアントとして構成することを意味しています。
…略… <ItemGroup> <Protobuf Include="Protos/bookinfo.proto" GrpcServices="Client" /> 追加 </ItemGroup> </Project>
ここで、dotnet buildコマンドにてクライアントアプリケーションをいったんビルドして、コピーしたプロトコル定義ファイルに基づくファイル(Bookinfo.cs、BookinfoGrpc.cs)を生成しておき、エラーのないことも確認しておきます。
% dotnet build …略… ビルドに成功しました。 …略…
クライアントのためのコードを追加する
最後に、ただのコンソールアプリケーションに、gRPCクライアントのためのコードを追加します。既定のコードである「Console.WriteLine…」の行は不要なのでコメントアウトしてください。
using System.Threading.Tasks; using Grpc.Net.Client; using GrpcBookInfoClient; if (args.Length == 0) { (1) Console.WriteLine("引数が必要です"); return; } using var channel = GrpcChannel.ForAddress("http://localhost:5299"); (2) var client = new BookInfo.BookInfoClient(channel); (3) var reply = await client.SearchAsync( (4) new SearchRequest { Text = args[0] }); if (reply.StatusCode == 0) { (5) Console.WriteLine($"見つかりません: {args[0]}"); } else { Console.WriteLine($"検索結果: {reply.Item.Title} by {reply.Item.Author}"); }
(1)では、コマンドライン引数であるargsの要素数を調べて、0ならばパラメーターがないものとしてメッセージを表示し、プログラムを終了させています。
(2)では、gRPCのチャネル(HTTP/2による通信路)を生成しています。gRPCサーバに接続するためのURLを引数にしています。このうちスキーマとポート番号の部分を、サーバアプリケーションのGrpcBookInfo/Properties/launchSettings.jsonファイルのapplicationUrlエントリに合わせて修正してください。HTTP用とHTTPS用があるので、macOSではHTTP用、WindowsではHTTPS用のスキーマおよびポート番号を使います。このようにしているのは、macOSの開発サーバがTLS上でのHTTP/2をサポートしていないためです。詳細は、連載第2回を参照してください。
(3)では、(2)で生成したチャネルを元に、クライアントであるBookInfoClientオブジェクトを生成しています。
(4)では、(3)で生成したクライアントオブジェクトに対して、非同期でSearch手続きを呼び出しています。このように、自動生成されるBookInfoGrpc.csファイルには非同期のメソッドも実装されるので、処理に時間がかかる手続きには非同期での呼び出しを指定できます。引数は、SearchRequestメッセージに相当するオブジェクトです。
最後に(5)では、検索結果を表示しています。レスポンスのStatusCodeが0ならば該当項目なし、それ以外であれば該当項目ありとして、レスポンスのBookItemメッセージのうち、TitleフィールドとAuthorフィールドのみを表示しています。
以下は、実行例です。サーバアプリケーションを起動しておくのを忘れないでください。
% dotnet run 引数が必要です % dotnet run Rust 検索結果: Rust by Yamauchi % dotnet run gRPC 検索結果: gRPC by Nao % dotnet run JavaScript 見つかりません: JavaScript
まとめ
今回は、連載第2回で作成したC#によるgRPCサーバアプリケーションに独自の書籍情報検索サービスを実装してみました。そして、このサービスにアクセスする専用のクライアントアプリケーションも作成し、その内容を見てみました。
次回は、Pythonによる、サーバサイドストリーミング型のgRPCサーバアプリケーションを開発します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.