C# で gRPC を使ってみた、ついでに Stream でファイル転送

最終更新日

はじめに

gRPC-Web が正式リリース されたとの事を聞いて、やっぱり gRPC が今後の通信プロトコルのデファクトスタンダードになるのかなと思い実際に使って使用感を確かめてみました。
gRPC-Web ではなく gRPC になります

開発環境

  • Visual Studio 2015
  • C#(自分が一番使いやすい言語)

学んだ内容

  • gRPC は Go言語 が本流
  • *.proto ファイルが通信・インターフェスの定義をする
  • *.proto ファイルから各言語の通信プログラムを生成する
  • メッセージ単位でデータのやり取りをする、送信・返信
  • メッセージのサイズは 4MB が最大?
  • メッセージのやり取りは1対1ではなく、N対1などでもできる。
    • その場合は Stream を使う

プログラム実装

  • チュートリアルを実行
  • Streamを使ってみた(ファイル転送)

チュートリアルの実行

gRPCの公式ドキュメントを見なかった理由はとりあえず動かしてみたかったからです。
codelabs の gRPC(C#) のチュートリアルを実行してみました。

Streamを使ってみた(ファイル転送)

greeter.proto ファイルに定義を追加

  • Emptyメッセージを追加
  • Chunkメッセージを追加
  • filesendメソッドを追加
greeter.proto
message Empty {
}

message Chunk {
    bytes chunk = 1;
}

service GreetingService {
    rpc greeting(HelloRequest) returns (HelloResponse);
    rpc filesend(stream Chunk) returns (Empty);
}

バッチで通信プログラムを生成

generate_protos.bat を実行

サーバのソースコード

filesend の受信時のメソッドを追加

  • N回リクエストが来るので ForEachAsync で回す
  • 受信したデータをbyte配列に結合する
  • 受信完了したら配列のサイズを画面に表示する
  • 受信完了したらEmptyメッセージを返す
GreeterServiceImpl.cs
namespace GreeterServer
{
    public class GreeterServiceImpl : GreetingService.GreetingServiceBase
    {
        public override Task<HelloResponse> greeting(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloResponse { Greeting = "Hello " + request.Name });
        }

        public override async Task<Empty> filesend(IAsyncStreamReader<Chunk> stream, ServerCallContext context)
        {
            List<byte> bytes = new List<byte>();
            await stream.ForEachAsync(request =>
            {
                var temp = request.Chunk_.ToByteArray();
                bytes.AddRange(temp);
                return Task.CompletedTask;
            });

            Console.WriteLine($"size={bytes.Count}");
            // Console.WriteLine(BitConverter.ToString(bytes.ToArray()));

            // 受信完了を返す
            return new Empty();
        }
    }
}

クライアントのソースコード

ファイルの読み込みを行い、送信処理を100回送ります。

  • 非同期APIなのでTaskを使う
  • stream.RequestStream.WriteAsync でリクエストメッセージを送信
  • stream.RequestStream.CompleteAsync でリクエスト終了を通知
  • await stream.ResponseAsync でサーバからのレスポンスを取得
  • 最後に実行時間を出力

4MBに収まるファイルでのみ動作確認しています(超える場合は、4MBに分割してリクエストメッセージを複数投げたら動くと思います)

Program.cs
namespace GreeterClient
{
    class Program
    {
        const string Host = "localhost";
        const int Port = 50051;

        public static void Main(string[] args)
        {
            // Create a channel
            var channel = new Channel(Host + ":" + Port, ChannelCredentials.Insecure);

            // Create a client with the channel
            var client = new GreetingService.GreetingServiceClient(channel);

            // Create a request
            var request = new HelloRequest{
                Name = "Mete - on C#",
                Age = 34,
                Sentiment = Sentiment.Happy
            };

            // Send the request
            Console.WriteLine("GreeterClient sending request");
            var response = client.greeting(request);
            Console.WriteLine("GreeterClient received response: " + response.Greeting);

            // チャンクデータの作成
            var fs = new FileStream("00013646_72B.jpg", FileMode.Open);
            byte[] data = new byte[fs.Length];
            fs.Read(data, 0, data.Length);

            // ファイルの送信処理
            var task = Task.Run(async () =>
            {
                Stopwatch sw = new Stopwatch();
                sw.Start();
                for (int i = 0; i < 100; i++)
                {
                    // 送信
                    var stream = client.filesend();
                    var chunk = Google.Protobuf.ByteString.CopyFrom(data, 0, data.Length);
                    await stream.RequestStream.WriteAsync(new Chunk() { Chunk_ = chunk });
                    await stream.RequestStream.CompleteAsync();
                    var res = await stream.ResponseAsync;
                }
                sw.Stop();
                Console.WriteLine(sw.Elapsed);
            });
            task.Wait();

            // Shutdown
            channel.ShutdownAsync().Wait();
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}

実行結果

HDDの読み込みなどのネックを無くすためにほぼほぼオンメモリで計測してみました、ローカルホストで実行しているのでだいぶ早い気がします。
100ファイルの送信で 0.84秒 になります。

1ファイル送信(1リクエスト、1レスポンス)でやっているのでここを、Nファイル送信(Nリクエスト、1レスポンス)に変更できればもっと早くなるかと思われます。

  • クライアント
    image.png

  • サーバ
    image.png

まとめ

めっちゃ簡単なのでこれからも使っていきたい。
非同期での処理が前提なので C# 以外の言語でどう記述するのかいまいちわかっていないがこれから使う機会があれば勉強できるので今のところはここまでにします。