grpcがどんな風に動いているかを覗いてみます。
grpcとは
下の画像のようにgoogle製のrpcフレームワーク言語をまたいで扱えるのが特徴です。
クライアントは言語ごとに作られていて、grpcのリポジトリに直接入っているものと、別リポジトリに分けられているものが有ります。
基本的には、protocolbufferとhttp2を利用したRPCで、cncfの各プロジェクトやコンテナ系のミドルウェアでかなり使われています。
wiresharkで見るgrpcの動き
grpc-goのサンプルコードを基に、通信内容を見てみます。
動いている内容は、
- クライアントが、SayHello({Name: "world"})というメソッドを関数を実行する
- サーバーがResponse{Message: "Hello world"}と返す
というシンプルな動作で、最もシンプルな Unary RPC を使用しています。
wiresharkで除いて見ると、下のような流れを見ることが出来ます。 (クライアントのポートが42874、サーバーのポートが50051)
http2を使用していることがわかります。
詳細に見てみると、
クライアントからのDATAはHEADERSと一緒に送られていて、下のような内容が送信されています。
HEADERS内容: :method: POST :scheme: http :path: /helloworld.Greeter/SayHello content-type: application/grpc DATA内容: hexでの表記: 00000c00010000000100000000070a05776f726c64 最後に、"world"(77 6f 72 6c 64)があるのがわかります
対して、サーバーからのHEADERSとDATAの下のような内容です。
HEADERS内容: :status: 200 content-type: application/grpc DATA内容: hexでの表記: 000012000000000001000000000d0a0b48656c6c6f20776f726c64 最後に、"Hello world"(48 65 6c 6c 6f 20 77 6f 72 6c 64)があるのがわかります
つまり、
- 呼び出したいメソッドはHEADERS
- protocolbufferの内容はDATA
に書かれているとわかります。
つまり、http2で動いているだけなので、grpcのライブラリを使わずとも、リクエストを送るクライアントが(大変ですが)一応、書けます。
func main() { log.Println("start client") conn, err := net.Dial("tcp", "localhost:50051") if err != nil { log.Fatalf("did not connect: %v", err) } magic := "505249202a20485454502f322e300d0a0d0a534d0d0a0d0a" settings := "000000040000000000" headers := "00005c010400000001838645956272d141fc1eca245f15852a4b631b87eb1968a0ff418ba0e41d139d09b8d800d87f5f8b1d75d0620d263d4c4d65647a8a9acac8b4c7602bb2b83f40027465864d833505b11f40899acac8b24d494f6a7f867df7db7416ff" data := "00000c00010000000100000000070a05776f726c64" packets := []string{magic, settings, headers, data} for _, p := range packets { raw, err := hex.DecodeString(p) if err != nil { log.Fatalf("cannot decode: %v", p) } if _, err := conn.Write(raw); err != nil { log.Fatalf("cannot write: %v", p) } } buf := make([]byte, 1024) for { n, err := conn.Read(buf) if err != nil { log.Fatalf("cannot read from socket: %v", err) } log.Println(string(buf[:n])) log.Println(hex.EncodeToString(buf[:n])) } }
更に詳細な送信内容は、github内のドキュメントにまとめられています
コードから見る動き
grpc(Unary RPC)の動きをgoのコードを基におってみてみます。
クライアントから見ると、
- ユーザーがprotoファイルで定義した関数を呼び出す。
- 引数の値をprotocolbufferを基にシリアライズして、DATAにする
- pathに呼び出したい関数をセットする
- サーバーに送信
- 受信を待つ
- 受信したデータをデシリアライズして、ユーザーに伝える
という流れです。
逆にサーバーから見ると、
以上が、grpcの簡単な流れとなっています。