gRPC repeated な Message を Wireshark で確認する

2022/08/06に公開

gRPC というよりは Protocol Buffers のシリアライゼーションの確認になりますが、repeated で宣言したデータがサーバ・クライアント間でどのようにやりとりされるのか確認してみます。

Wireshark の設定方法などは過去の記事に簡単に書いているので参考にしてください。

Transform を 10 件 repeated で送信した時

解析対象プログラムの準備

細かい説明は過去記事をご確認ください。

protoc

grpc/game.proto
syntax = "proto3";

option go_package = ".;grpc";

package grpc;

service GameService {
  rpc BidirectTransform(stream Transform) returns (stream WorldData) {}
}

message Vector3 {
  float x = 1;
  float y = 2;
  float z = 3;
}

message Transform {
  Vector3 position = 1;
  Vector3 rotation = 2;
}

message WorldData {
  repeated Transform players = 1;
}

server

main.go
package main

import (
	"flag"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net"
	"time"

	pb "github.com/hidingfox/grpc"
	"google.golang.org/grpc"
)

var (
	port = flag.Int("port", 50051, "The server port")
)

type server struct {
	pb.UnimplementedGameServiceServer
}

func (s *server) BidirectTransform(stream pb.GameService_BidirectTransformServer) error {
	for {
		in, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		log.Printf("Received: %v", in)
		var ts []*pb.Transform
		for i := 0; i < 10; i++ {
			p := pb.Vector3{X: rand.Float32(), Y: rand.Float32(), Z: rand.Float32()}
			r := pb.Vector3{X: rand.Float32(), Y: rand.Float32(), Z: rand.Float32()}
			t := pb.Transform{Position: &p, Rotation: &r}
			ts = append(ts, &t)
		}
		wd := pb.WorldData{Players: ts}
		if err := stream.Send(&wd); err != nil {
			return err
		}
	}
}

func main() {
	flag.Parse()
	rand.Seed(time.Now().UnixNano())
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGameServiceServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

client

main.go
package main

import (
	"context"
	"flag"
	"io"
	"log"
	"math/rand"
	"time"

	pb "github.com/hidingfox/grpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

var (
	addr = flag.String("addr", "localhost:50051", "the address to connect to")
)

func bidirectStream(c pb.GameServiceClient) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	stream, err := c.BidirectTransform(ctx)
	if err != nil {
		log.Fatalf("BidirectTransform failed: %v", err)
	}
	waitc := make(chan struct{})
	go func() {
		for {
			in, err := stream.Recv()
			if err == io.EOF {
				// read done.
				close(waitc)
				return
			}
			if err != nil {
				log.Fatalf("BidirectTransform failed: %v", err)
			}
			log.Printf("Got message %v", in)
		}
	}()
	// 本来は 1 度だけでなく定期的に Send する想定
	p := pb.Vector3{X: rand.Float32(), Y: rand.Float32(), Z: rand.Float32()}
	r := pb.Vector3{X: rand.Float32(), Y: rand.Float32(), Z: rand.Float32()}
	t := pb.Transform{Position: &p, Rotation: &r}
	if err := stream.Send(&t); err != nil {
		log.Fatalf("BidirectTransform: stream.Send(%v) failed: %v", t, err)
	}
	stream.CloseSend()
	<-waitc
}

func main() {
	flag.Parse()
	rand.Seed(time.Now().UnixNano())
	// Set up a connection to the server.
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGameServiceClient(conn)
	bidirectStream(c)
}

パケット解析

処理の流れ

  1. クライアントが BidirectTransform に Transform を 1 件投げる
  2. サーバが Transform を 10 件格納した WorldData を返す

  • No.217 - 219 c <=> s
    • TCP 3 way handshake
  • No.220 - 221 c => s
    • HTTP2 Magic
  • No.222 - 229 c <=> s
    • HTTP2 Settings
  • No.230 - 231 c => s
    • HTTP2 Header POST /grpc.GameService/BidirectTransform
  • No.232 - 233 c => s
    • HTTP2 DATA grpc.Transform
    • HTTP2 DATA End Stream
  • No.234 - 235 s => c
    • HTTP2 WINDOW_UPDATE
    • HTTP2 PING Ping
  • No.236 - 237 c => s
    • HTTP2 PING Pong
  • No.238 - 239 s => c
    • HTTP2 Headers 200 OK
    • HTTP2 DATA grpc.Transform
  • No.240 - 241 s => c
    • HTTP2 Headers End Stream
  • No.242 - 243 c => s
    • HTTP2 WINDOW_UPDATE
    • HTTP2 PING Ping
  • No.244 - 245 s => c
    • HTTP2 PING Pong
  • No.156
    • TCP rst

TCP 3 way handshake, HTTP2 Magic, HTTP2 Settings までは Unary RPC, Server streaming RPC と変わらず。

No.230 - 233 クライアントが BidirectTransform に Transform を 1 件投げる
No.238 - 239 サーバが Transform を 10 件格納した WorldData を返す

WorldData 応答時のデータ構造

  • HTTP2 DATA (374 Bytes)
    • GRPC Message (365 Bytes)
      • Protocol Buffers response (360 Bytes)
        • Message: grpc.WorldData (360 Bytes)
          • Field(1): players (36 Bytes)
            • Message: grpc.Transform (34 Bytes)
              • Field(1): position (17 Bytes)
                • Message: grpc.Vector3 (15 Bytes)
                  • Field(1): x float (5 Bytes)
                  • Field(2): y float (5 Bytes)
                  • Field(3): z float (5 Bytes)
              • Field(2): rotation (17 Bytes)
                • Message: grpc.Vector3 (15 Bytes)
                  • Field(1): x float (5 Bytes)
                  • Field(2): y float (5 Bytes)
                  • Field(3): z float (5 Bytes)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)
          • Field(1): players (36 Bytes)
            • (snip)

Transform を 10 回 stream.Send した時の構造と比べると、Message: grpc.Transform のサイズは同じ 34 Bytes。

Transform を 1 件格納した HTTP2 Stream DATA を 10 件送信するか、Transform を 10 件格納した HTTP2 Stream DATAを 1 件送信するか、だと HTTP2 DATA に包む際に制御データが付与されてしまうので、repeated で Transform を 10 件格納した方がサイズは小さくなる。

  • Transform を 1 件格納した HTTP2 Stream DATA = 48 Bytes * 10 = 480 Bytes
  • Transform を 10 件格納した HTTP2 Stream DATA = 374 Bytes

Transform を 0 件 repeated で送信した時

server コードを一部コメントアウト

main.go
func (s *server) BidirectTransform(stream pb.GameService_BidirectTransformServer) error {
	for {
		in, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		log.Printf("Received: %v", in)
		var ts []*pb.Transform
		// for i := 0; i < 10; i++ {
		// 	p := pb.Vector3{X: rand.Float32(), Y: rand.Float32(), Z: rand.Float32()}
		// 	r := pb.Vector3{X: rand.Float32(), Y: rand.Float32(), Z: rand.Float32()}
		// 	t := pb.Transform{Position: &p, Rotation: &r}
		// 	ts = append(ts, &t)
		// }
		wd := pb.WorldData{Players: ts}
		if err := stream.Send(&wd); err != nil {
			return err
		}
	}
}

パケット解析

サーバがクライアントに送信するまではすべて同じ流れ
違いはサーバの送信する DATA

  • HTTP2 DATA (14 Bytes)
    • GRPC Message (5 Bytes)

無駄が一切ない感じがします。

すべて repeated な float で送信した時

Transform = Vector3 * 2
Vector3 = float32 * 3
ですので、Transform 10 件分を repeated float32 で送信した場合にどうなるか確認してみます。

解析対象プログラムの準備
grpc/game.proto
syntax = "proto3";

option go_package = ".;grpc";

package grpc;

service GameService {
  rpc BidirectTransform(stream Transform) returns (stream WorldData) {}
}

message Transform {
  repeated float v = 1;
}

message WorldData {
  repeated float v = 1;
}

server

main.go
package main

import (
	"flag"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net"
	"time"

	pb "github.com/hidingfox/grpc"
	"google.golang.org/grpc"
)

var (
	port = flag.Int("port", 50051, "The server port")
)

type server struct {
	pb.UnimplementedGameServiceServer
}

// BidirectTransform サーバ・クライアント双方向 stream で Transform 情報を送受信する
func (s *server) BidirectTransform(stream pb.GameService_BidirectTransformServer) error {
	for {
		in, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		log.Printf("Received: %v", in)
		var fs []float32
		// Position = float32 * 3
		// Rotation = float32 * 3
		// Transform = Position + Rotation
		// Transform * 10 = float32 * 6 * 10 = float32 * 60
		for i := 0; i < 60; i++ {
			fs = append(fs, rand.Float32())
		}
		wd := pb.WorldData{V: fs}
		if err := stream.Send(&wd); err != nil {
			return err
		}
	}
}

func main() {
	flag.Parse()
	rand.Seed(time.Now().UnixNano())
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGameServiceServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

client

main.go
package main

import (
	"context"
	"flag"
	"io"
	"log"
	"math/rand"
	"time"

	pb "github.com/hidingfox/grpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

var (
	addr = flag.String("addr", "localhost:50051", "the address to connect to")
)

func bidirectStream(c pb.GameServiceClient) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	stream, err := c.BidirectTransform(ctx)
	if err != nil {
		log.Fatalf("BidirectTransform failed: %v", err)
	}
	waitc := make(chan struct{})
	go func() {
		for {
			in, err := stream.Recv()
			if err == io.EOF {
				// read done.
				close(waitc)
				return
			}
			if err != nil {
				log.Fatalf("BidirectTransform failed: %v", err)
			}
			log.Printf("Got message %v", in)
		}
	}()
	// 本来は 1 度だけでなく定期的に Send する想定
	var fs []float32
	for i := 0; i < 6; i++ {
		fs = append(fs, rand.Float32())
	}
	t := pb.Transform{V: fs}
	if err := stream.Send(&t); err != nil {
		log.Fatalf("BidirectTransform: stream.Send(%v) failed: %v", t, err)
	}
	stream.CloseSend()
	<-waitc
}

func main() {
	flag.Parse()
	rand.Seed(time.Now().UnixNano())
	// Set up a connection to the server.
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGameServiceClient(conn)
	bidirectStream(c)
}

パケット解析

通信内容は変わらないので、サーバからクライアントへの DATA 構造だけ確認します。

  • HTTP2 Data (257 Bytes)
    • GRPC Message (248 Bytes)
      • Protocol Buffers (243 Bytes)
        • Message: grpc.WorldData (243 Bytes)
          • Field(1): v (243 Bytes)
            • Repeated (240 Bytes)
              • Float (4 Bytes)
              • (snip 60 件続く)

Float がこれまでのように 5 Bytes ではなく 4 Bytes で格納できています。

これは Protocol Buffers のエンコード時の packed が有効な為です。
https://developers.google.com/protocol-buffers/docs/encoding#packed

Summary

連続するデータをいくつか形を変えて送信して解析してみました。

  1. Transform(float32 * 6) を 1 件格納した HTTP2 Stream DATA = 48 Bytes * 10 = 480 Bytes[1]
  2. Transform を 10 件 repeated で格納した HTTP2 Stream DATA = 374 Bytes
  3. float32 を repeated で 60 件格納した HTTP2 Stream DATA = 257 Bytes

3までいくと、流石にコード側で規約に基づいたノーマライズが必要になるでしょうし、RPC のスキーマ宣言による保護が効かなくなるので、gRPC の利点を一つ捨てることになりそうです。

ゲームの様な超効率が求められるシチュエーションなら UDP の効率と差があまりなくなるので、gRPC の他の利点を享受しつつ、効率的な通信をする、という用途では使えるのかも知れませんね。

また repeated が空の時は送信されるデータサイズが 0 になるのも確認できました。
repeated は使い勝手が良さそうです。

gRPC の Wireshark 解析の調査記事をいくつか書きましたが、これで以上にしたいと思います。
誰かの役に立つ情報になっていれば幸いです。

脚注
  1. 過去記事参照 ↩︎

Discussion