🙆

gRPC Unary 通信の内容を Wireshark で確認する

2022/08/05に公開約9,000字

gRPC 通信した時に、どういったパケットのやり取りが行われているのか理解を深める為に解析してみようと思います。

gRPC 公式サイトブログに Wireshark を使った解析についての記事がありました。

この記事を参考にしながら gRPC の通信を確認してみます。

Wireshark は通信パケット解説ツールですが、gRPC と Protocol Buffers にも対応しているらしいです。
ありがたい。

Wireshark のダウンロード

Wireshark 公式サイトで最新版をダウンロードしてインストールします。

v3.6.7 をインストールしました。

Wireshark 設定

Protocol Buffers は定義ファイルを読み込ませる必要があるようです。
定義ファイルの格納フォルダを Wireshark に指定します。

編集 > 設定 > Protocols > Protobuf を開いて Protobuf search paths の編集から、定義ファイルと protoc の include フォルダを指定する必要があるらしい。

定義ファイルの方は Load all files にチェックしておきましょう。

前回作ったクライアントとサーバの定義ファイルを指定しました。

さらに 分析 > ...としてデコード に HTTP2 として解析するように指定しておきます。

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

前回 stream 通信を試したサーバ・クライアントに Unary RPC のモードも追加しました。

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

grpc/game.proto
syntax = "proto3";

option go_package = ".;grpc";

package grpc;

service GameService {
  // Unary RPC
  rpc SayHello (HelloRequest) returns (HelloReply) {}

  // A server-to-client streaming RPC.
  rpc ListTransform(Transform) returns (stream Transform) {}

  // A client-to-server streaming RPC.
  rpc RecordTransform(stream Transform) returns (RecordReply) {}

  // A Bidirectional streaming RPC.
  rpc BidirectTransform(stream Transform) returns (stream Transform) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

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

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

message RecordReply {
  string message = 1;
}
main.go
package main

import (
	"context"
	"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) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

// ListTransform プレイヤーの Transform をサーバからクライアントへ stream で送信する
func (s *server) ListTransform(t *pb.Transform, stream pb.GameService_ListTransformServer) error {
	for i := 0; i < 100; 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}
		if err := stream.Send(&t); err != nil {
			return err
		}
	}
	log.Printf("Received: %v", t)
	return nil
}

// RecordTransform プレイヤーから stream で Transform 情報を受信する
func (s *server) RecordTransform(stream pb.GameService_RecordTransformServer) error {
	for {
		t, err := stream.Recv()
		if err == io.EOF {
			return stream.SendAndClose(&pb.RecordReply{
				Message: "OK",
			})
		}
		if err != nil {
			return err
		}
		log.Printf("Received: %v", t)
	}
}

// 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)
		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}
			if err := stream.Send(&t); 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)
	}
}
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"
)

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "localhost:50051", "the address to connect to")
	mode = flag.String("mode", "s", "RPC mode")
	name = flag.String("name", defaultName, "Name to greet")
)

func unaryRequest(c pb.GameServiceClient) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

func serverStream(c pb.GameServiceClient) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	p := pb.Vector3{X: 0, Y: 0, Z: 0}
	r := pb.Vector3{X: 0, Y: 0, Z: 0}
	t := pb.Transform{Position: &p, Rotation: &r}
	stream, err := c.ListTransform(ctx, &t)
	if err != nil {
		log.Fatalf("ListTransform failed: %v", err)
	}
	for {
		t, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatalf("ListTransform failed: %v", err)
		}
		log.Printf("Receive: %v", t)
	}
}

func clientStream(c pb.GameServiceClient) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	stream, err := c.RecordTransform(ctx)
	if err != nil {
		log.Fatalf("RecordTransform failed: %v", err)
	}
	for i := 0; i < 100; 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}
		if err := stream.Send(&t); err != nil {
			log.Fatalf("RecordTransform: stream.Send(%v) failed: %v", t, err)
		}
	}
	reply, err := stream.CloseAndRecv()
	if err != nil {
		log.Fatalf("RecordTransform failed: %v", err)
	}
	log.Printf("RecordReply: %v", reply)
}

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)
		}
	}()
	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}
		if err := stream.Send(&t); err != nil {
			log.Fatalf("BidirectTransform: stream.Send(%v) failed: %v", t, err)
		}
	}
	stream.CloseSend()
	<-waitc
}

func main() {
	flag.Parse()
	// 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)
	switch *mode {
	case "u":
		unaryRequest(c)
	case "s":
		serverStream(c)
	case "c":
		clientStream(c)
	case "b":
		bidirectStream(c)
	}
}

パケット解析

  • loopback インターフェイスを指定
  • 表示フィルタ tcp.port == 50051

として解析していきます。

Unary RPC

処理の流れ

  1. クライアントが SayHello に "world" を投げる
  2. サーバが "Hello world" を返す

  • No.2345 - 2347 c <=> s
    • TCP 3 way handshake
  • No.2348 - 2349 c => s
    • HTTP2 Magic
  • No.2350 - 2357 c <=> s
    • HTTP2 Settings
  • No.2358 - 2359 c => s
    • HTTP2 Header POST /grpc.GameService/SayHello
  • No.2360 - 2361 c => s
    • HTTP2 DATA grpc.HelloRequest
  • No.2362 - 2365 s => c
    • HTTP2 WINDOW_UPDATE
  • No.2366 - 2367 s => c
    • HTTP2 Headers 200 OK
    • HTTP2 DATA /grpc.GameService/SayHello, Response
  • No.2368 - 2369 s => c
    • HTTP2 Headers
  • No.2370 - 2373 c => s
    • HTTP2 WINDOW_UPDATE
  • No.2374
    • TCP rst

TCP・HTTP2のフロー制御のパケットが殆どですね・・・。

No.2358 - 2361 がクライアントがリクエストしているところ。

No.2358 - 2359 でヘッダ(163 Bytes)を送った後、No.2360 - 2361 でデータ(85 Bytes)を渡しています。
データ部の中を覗くと HelloRequest の中身として string "world" が 7 Bytes で送信されています。

No.2366 - 2367 がサーバがレスポンスしているところ。

ヘッダ、データを一緒に応答しています。(114 Bytes)
データの中を見ると grpc.HelloReply の中身として string "Hello world" を 13 bytes で送信しています。

というわけで Unary RPC の解析は以上です。

解析してみると gRPC 以前に自分の HTTP2 の理解が曖昧だったので勉強が必要でした・・・。

gRPC は TCP + HTTP2 といった層の上に実装されており、ベース部分の信頼性がとても高いというのが理解できました。
その上で HTTP2 で HTTP よりも効率的に通信でき、サーバ間通信用途で普及が広がるのも納得です。

一方でゲームの様な用途では、UDP 通信で極限まで最適化されたパケットと比較すると、オーバーヘッドが大きすぎて、Unary RPC だけを目的に使う場面はあまりないのではないかな、と改めて感じました。

次回は streaming 通信を解析してみます。

Discussion

ログインするとコメントできます