発明のための再発明

内部動作のような、プログラムを書くときの参考になることを書きます

grpcの動き

grpcがどんな風に動いているかを覗いてみます。

grpcとは

下の画像のようにgoogle製のrpcフレームワーク言語をまたいで扱えるのが特徴です。

f:id:mrasu:20180407195152p:plain

クライアントは言語ごとに作られていて、grpcのリポジトリに直接入っているものと、別リポジトリに分けられているものが有ります。
基本的には、protocolbufferとhttp2を利用したRPCで、cncfの各プロジェクトやコンテナ系のミドルウェアでかなり使われています。

wiresharkで見るgrpcの動き

grpc-goのサンプルコードを基に、通信内容を見てみます。
動いている内容は、

  1. クライアントが、SayHello({Name: "world"})というメソッドを関数を実行する
  2. サーバーがResponse{Message: "Hello world"}と返す

というシンプルな動作で、最もシンプルな Unary RPC を使用しています。

wiresharkで除いて見ると、下のような流れを見ることが出来ます。 (クライアントのポートが42874、サーバーのポートが50051)

f:id:mrasu:20180407194834j:plain

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のコードを基におってみてみます。

クライアントから見ると、

  1. ユーザーがprotoファイルで定義した関数を呼び出す。
  2. 引数の値をprotocolbufferを基にシリアライズして、DATAにする
  3. pathに呼び出したい関数をセットする
  4. サーバーに送信
  5. 受信を待つ
  6. 受信したデータをデシリアライズして、ユーザーに伝える

という流れです。
逆にサーバーから見ると、

  1. サーバーを起動する
  2. 呼び出される関数を登録する
  3. リクエストが来たら、DATAをデシリアライズし、pathの値を基に登録した関数を呼び出す
  4. 実行した戻り値をクライアントへ送信する

以上が、grpcの簡単な流れとなっています。