コーヒー飲みながら仕事したい

仕事で使う技術的なことの備忘録とか

Protocol Buffers の C# 版で遊んでみる

Protocol Buffers のC#

有名どころでは、以下の2つがあるようです。

※ 追記:前者でも .proto ファイルを使用することができるみたいです。

今回の私が手掛ける案件では、後者のほうが適する(CのサービスとC#のサービスがやりとりする)ので、ここでは Google.Protobuf を主に取り上げます。

参考サイト

tnakamura.hatenablog.com

C# 版 protobuf (Google.Protobuf) の導入

githubのReadmeに書かれている手順で導入していきます。

条件

以下が条件です。

  • Visual Studio 2012 意向であること
  • .NET4.5 以降または .NET Core であること

Nuget

Google.Protobuf を使用するだけなら、 Google.Protobuf を Nuget すればよいです。
しかし、それに加えて .proto ファイルを使用してクラスファイルを生成するならば、 Google.Protobuf.Tools も Nuget する必要があります。

Google.Protobuf はライブラリなのだが、 Google.Protobuf.Tools はライブラリではなく、バイナリ(ptoroc.exe)が入っています。
ちなみに、 Nuget した際のダウンロード先は、私の場合はC:\Users\XXX\.nuget\packages\google.protobuf.tools\3.5.1\tools でした。
ソリューションファイル内にダウンロードされているものだと思っていたので、ちょっとはまってしまいました。

Google.Protobuf でチュートリアルする

とりあえず、公式のチュートリアルを実行してみます。
公式は英語なので、備忘録として意訳したやつを残しておきます。

.proto ファイルの用意

とりあえずチュートリアルで示されている addressbook.proto を使用します。これはgithubに掲載されているのだが、このまま使用するとエラーになる(import "google/protobuf/timestamp.proto" なんてねーよって言われる)ので、修正したのを↓に載せときます。

// [START declaration]
syntax = "proto3";
package tutorial;

// [END declaration]

// [START java_declaration]
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]

// [START csharp_declaration]
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]

// [START messages]
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}
// [END messages]

.proto ファイルからクラスファイルを生成する

Nuget した Google.Protobuf.Tools 内に含まれる tools\windows_x64\protoc.exe を使用して、addressbook.protoシリアライズ/デシリアライズ用のクラスファイルを生成しあmす。

protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto

上記を実行すると、Addressbook.cs$DST_DIR 内に生成されます。

Addressbook.cs の中身を見ると、なかなかキモくて焦るが、たぶん利用する側は中身を意識する必要はそんなになさそう)

生成されたクラスファイルと Google.Protobuf を使用して、シリアライズ/デシリアライズをやってみる

チュートリアルを参考に、以下のように動作確認用の Program.cs を作成して実行してみます。
実行結果、正しく動作していることが確認できました。

using System;
using System.IO;
using System.Text;
using Google.Protobuf;
using Google.Protobuf.Examples.AddressBook; // protoc.exeにより自動生成されたクラスの名前空間
using static Google.Protobuf.Examples.AddressBook.Person.Types; // C# 6 の書き方で、クラス内クラスを省略形式で記述することができるようになる(protobufとは関係なし)

namespace ProtobufCsharp
{
    class Program
    {
        static void Main(string[] args)
        {
            // AddressBook.csで定義されているPresonクラスを実体化する
            Person person = new Person
            {
                Id = 1234,
                Name = "Yamada Tarou",
                Email = "yamada@sample.com",
                Phones = { new PhoneNumber { Number = "555-4321", Type = PhoneType.Home },
                    new PhoneNumber { Number = "222-1111", Type = PhoneType.Work } }
            };

            // 文字列にシリアライズ
            var data = Serialize(person);

            // シリアライズした文字列を読み込んでデシリアライズする
            var someone = Deserialize<Person>(data);

            // 動作確認
            Console.WriteLine($"Id:{someone.Id}, Name:{someone.Name}, Email:{someone.Email}, " +
                $"Phones[0](Number:{someone.Phones[0].Number}, Type:{someone.Phones[0].Type}), " +
                $"Phones[1](Number:{someone.Phones[1].Number}, Type:{someone.Phones[1].Type})");

            Console.ReadKey();
        }

        static byte[] Serialize<T>(T obj) where T : IMessage<T>
        {
            using (var stream = new MemoryStream())
            {
                obj.WriteTo(stream);
                return stream.ToArray();
            }
        }

        static T Deserialize<T>(byte[] data) where T : IMessage<T>, new()
        {
            var parser = new MessageParser<T>(() => new T());
            return parser.ParseFrom(new MemoryStream(data));
        }
    }
}

解説

  • WriteTo(stream) メソッドで、Stream 型にシリアライズすることができる
  • Parser.ParseFrom(stream) メソッドで、Stream 型からデシリアライズすることができる
  • 実際、ネットワーク間でメッセージのやり取りをする場合は、文字列が都合がいい場合が多いので、その場合は MemoryStream 型を byte 配列に変換すればよい

とりあえず思ったより簡単に実行できました。
ただし、Stream 型を使用するシリアライズ/デシリアライズはちょっと面倒だ。直に byte 配列にできればいいのに。
ちょっと調査します。

直に byte 配列にシリアライズ/デシリアライズするようにサンプルを変更してみました。たぶんこれでいける。