お世話になっております。
しゃまとんです😊

gPRCのデフォルトシリアライザにはProtocol Buffersが使われています。
以前から、JSONに置き換えることはこちらの記事で知っていたのですが、JSONに置き換えられるのならMessagePackにも出来るのかな?と思ったのでやってみました。

ちなみに Unity/C# では、MagicOnion というgRPC上にMessagePackがのっているフレームワークが公開されておりますね。

手元で試す場合まずは、protocなどのコマンドを用意する必要がありますが、最新のバージョンを入れるようにしましょう!出力結果や実行オプションが違ってたりしました。
しゃらくせぇ!って方は今回試したものをリポジトリにしているのでよしなにしてください。

CodecはJSONに習って encoding.Codec を満たすようにしておけば良いです。(試していたときは難しいのでは…?と思っていたけど、そんなことはなかった。さすがgoogle)

リポジトリにはgRPCのexampleにある HelloRequest/HelloResponse を使っていますが、 ここにはこれらのデータがinterfaceで入ってくるので、ただシリアライズすればよいのですね。

const MsgpackCodecName = "msgpack"

type msgpackCodec struct {
}

func (c *msgpackCodec) Marshal(v interface{}) ([]byte, error) {
    // protoからgenerateされていればproto.Messageを満たすはず
    msg, ok := v.(proto.Message)
    if !ok {
        return nil, fmt.Errorf("not a proto message but %T: %v", v, v)
    }

    return msgpack.Marshal(msg)
}

func (c *msgpackCodec) Unmarshal(data []byte, v interface{}) error {
    _, ok := v.(proto.Message)
    if !ok {
        return fmt.Errorf("not a proto message but %T: %v", v, v)
    }
    return msgpack.Unmarshal(data, v)
}

func (c *msgpackCodec) Name() string {
    return MsgpackCodecName
}

あとはCodecをサーバ起動時に登録し、クライアントでは使うように設定すればよいです。

// server
func main() {
    encoding.RegisterCodec()
}
// client
opts := []grpc.DialOption{
    grpc.WithDefaultCallOptions(grpc.CallContentSubtype(encoding.MsgpackCodecName)),
}

ここで、HelloRequestのコードを見てみましょう。JSONはタグが使われていますね。 msgpackはタグがありません。つけなくても動くのは動くんですが、JSONと同じようについてないのでなんとかしてつけたくなりました。(データとして使うメンバ以外はprivateになっているため) どうやってやればいいのかというとprotobuf-goのこの辺をイジる必要があるんですね。 うまいこと注入できる感じだと良かったんだけどなーという印象。

https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.pb.go#L48

type HelloRequest struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

https://github.com/protocolbuffers/protobuf-go/blob/fb30439f551a7e79e413e7b4f5f4dfb58e117d73/cmd/protoc-gen-go/internal_gengo/main.go#L700

仕方がなかったので、protobuf-goをforkして、tagを設定している箇所でmsgpackも同じように設定してあげるようにしました。

https://github.com/shamaton/protobuf-go/commit/56cfa3aa7824c592d2bcef9dcb098a293919b5bb#diff-9d6e6c652dbe94644b20e53076db2ae4306dd0a4e62aa48dbc2dd2690bffb8fcR405

これを加えた上で protoc を実行すると…タグがつくようになりました 🙆

image

一応、この状態で実行してみると… replyが返ってきます。

// go run ./cmd/client
2022/01/09 22:24:27 send bytes: 81 a4 6e 61 6d 65 a5 77 6f 72 6c 64
2022/01/09 22:24:27 recv bytes: 81 a7 6d 65 73 73 61 67 65 ac 48 65 6c 6c 6f 20 77 6f 72 6c 64 2e
2022/01/09 22:24:27 Greeting: Hello world.

// go run ./cmd/server
2022/01/09 22:24:27 recv bytes: 81 a4 6e 61 6d 65 a5 77 6f 72 6c 64
2022/01/09 22:24:27 call from world
2022/01/09 22:24:27 send bytes: 81 a7 6d 65 73 73 61 67 65 ac 48 65 6c 6c 6f 20 77 6f 72 6c 64 2e

また、Marshal/Unmarshalしているときのバイナリを確認してみると 当たり前なんですがmsgpackのデータ構造になっていますね。(81はfixmap)

protobuf-goを管理するのはつらいので、やるならmsgpackをjsonタグでも動くようにする…みたいなのがよいのかも… と思いましたが、無理してやる必要はないと思いましたw

以上です〜👋