gRPCの概要をC#でサクッと理解する

gRPCの概要を理解するためにC#で簡単なプロジェクトを作って実行するまでの流れをまとめました。

gRPCとは?

gRPCはHTTP/2プロトコル上で動作するRPCフレームワークです。
要するにHTTP/2に対応したサーバを立てて、クライアントからそのメソッドを呼ぶ(RPC)ということができます。

本記事ではこの動きを確認するため、gRPCでサーバを作って他のPCから簡単にRPCを実行してみます。

またgRPCではリクエストとレスポンスのインタフェースをProtocol Buffersで記述します。
そこから各言語のソースコードを自動生成することで、サーバとクライアントで整合性の取れたインタフェースを使用できます。

インストールとセットアップ

さてそれではgRPCを使った簡単なアプリケーションを作ってみます。

プロジェクト作成

まずVisual Studioでプロジェクトを作成します。
今回はコンソールアプリとしてプロジェクトを作成します。

f:id:halya_11:20200616234702p:plain
コンソールアプリを作る

パッケージをインストール

次にgRPC関連のパッケージをインストールします。
NuGetで以下のパッケージをインストールしてください。

  • Grpc.Core
  • Grpc.Tools
  • Google.Protobuf

f:id:halya_11:20200616235627p:plain
NuGet

Protocol Buffersの対象ファイルを指定

次に使用するProtocol Buffersのファイルパスをプロジェクトファイルに定義します。
プロジェクトファイル(csprojファイル)を開いて以下を追記します。

  <ItemGroup>
    <Protobuf Include="Protos\*.proto" GrpcServices="Both" />
  </ItemGroup>

そしてプロジェクトファイルと同階層にProtosフォルダを作成し、これから作る.protoファイルはここに格納していきます。

なお、上記ではGrpcServices="Both"としていますが、この場合はサーバ用のコードとクライアント用のコードが両方とも生成されます。
これをサーバ用のアプリケーションを作る場合にはGrpcServices="Server"
クライアントアプリケーションを作る場合にはGrpcServices="Client"とすれば余計なコードが生成されなくなります。

サーバサイドアプリを作る

では次にサーバサイドのアプリケーションを作っていきます。

protoファイルからコード生成

まずはprotoファイルを作成します。
前述のプロジェクトファイルの設定はGrpcServices="Server"にしておきます。

次にProtosフォルダ配下にexample.protoという名前で以下のファイルを作成します。

syntax = "proto3";

option csharp_namespace = "GrpcExample";

import "google/protobuf/empty.proto";

service Example {
  // マシン情報を取得
  rpc GetMachineInfo (google.protobuf.Empty) returns (ExampleReply);
}

message ExampleReply {
  // コンピュータ名
  string MachineName = 1;
  // ユーザ名
  string UserName = 2;
}

大体見ればわかると思うのでprotoファイルの細かい書き方は割愛します。
注意点として、今回定義しているメソッドは引数を持たないメソッドになりますが、
この場合には上記のように引数にgoogle.protobuf.Emptyを設定します。

この状態で一度ビルドするとobjフォルダ配下にExample.csExampleGrpc.csが出力されていることが確認できます。
この自動生成されたクラスを使ってサーバのコードを書いていきます。

サーバコードを書く

次にProgram.csにサーバコードを記述します。

using System;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using GrpcExample;

namespace GrpcGreeterClient
{
    class Program
    {
        public static void Main(string[] args)
        {
            var server = new Server
            {
                // 自動生成されたクラスを使ってこう書く
                Services = { Example.BindService(new ExampleService()) },
                // ホストとポート番号を入力
                Ports = { new ServerPort("123.456.7.89", 12345, ServerCredentials.Insecure) }
            };
            server.Start();

            Console.WriteLine("Press any key to shutdown the server...");
            Console.ReadKey();

            server.ShutdownAsync().Wait();
        }
    }

    public class ExampleService : Example.ExampleBase
    {
        public override Task<ExampleReply> GetMachineInfo(Empty request, ServerCallContext context)
        {
            // マシン名とユーザ名を返す
            return Task.FromResult(new ExampleReply
            {
                MachineName = Environment.MachineName,
                UserName = Environment.UserName
            });
        }
    }
}

Serverのインスタンスを生成する部分はほぼボイラープレートです。
とりあえず通信を確認する必要があるのでホストにはプライベートIPアドレスとかを入れておいてください。

ExampleServiceはprotoファイルで定義した基底クラスの実装です。
今回はちゃんと通信できてることを確認するためにマシン名とユーザ名を返しているだけです。

クライアントサイドアプリを作る

次にクライアントサイドのアプリを作っていきます。
別のPCで実行するとちゃんと通信できてることが実感しやすいかと思います。

protoファイルからコード生成

protoファイルはサーバサイドアプリを作った時と同じものを使います。

プロジェクトファイルの設定だけGrpcServices="Client"にしてください。

クライアントコードを書く

次にProgram.csにクライアントコードを記述します。

using System;
using GrpcExample;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;

namespace ExampleGrpcClient
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            var channel = new Channel("123.456.7.89", 12345, ChannelCredentials.Insecure);
            var client = new Example.ExampleClient(channel);
            var machineInfo = client.GetMachineInfo(new Empty());
            Console.WriteLine($"MachineName: {machineInfo.MachineName} / UserName: {machineInfo.UserName}");
        }
    }
}

こんな感じで自動生成されたExampleClientをインスタンス化して使います。
ホストとポートはサーバに設定したものと同じ値を指定します。

実行する

サーバサイドとクライアントサイドのアプリができたらそれぞれのアプリを実行します。
クライアントサイドで以下のようにサーバサイドのPCの情報が得られれば成功です。

f:id:halya_11:20200617004158p:plain
実行結果