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();
        }
    }
}

実行結果

  • クライアント
    image.png
  • サーバ
    image.png

まとめ

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







よければ、SNSにシェアをお願いします!