CUBE SUGAR CONTAINER

技術系のこと書きます。

gRPC の通信を Wireshark でキャプチャしてみる

今回は、最近よく使われている gRPC の通信を Wireshark でキャプチャしてみる。 ちなみに、現行の Wireshark だと gRPC をちゃんと解釈できるみたい。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V                    
Python 3.7.3
$ wireshark --version | head -n 1
Wireshark 3.0.2 (v3.0.2-0-g621ed351d5c9)

Python で gRPC サーバ・クライアントを書く

通信をキャプチャするためには、まず gRPC のサーバとクライアントを用意する必要がある。

まずは gRPC を使う上で必要なツールキットをインストールしておく。

$ pip install grpcio-tools

gRPC では様々なプログラミング言語を用いてサーバとクライアントを記述できる。 そのために、どの言語を使う場合にも共通のスキーマを定義した上で、それを各言語用にコンパイルする。 共通のスキーマを定義するには Protocol Buffers というフォーマットを用いる。

以下は Protocol Buffers のスキーマを定義するファイルとなっている。 この中では HelloWorld というサービス上で greet() という RPC が定義されている。 greet() でやり取りするのは HelloRequestHelloReply というメッセージ。

syntax = "proto3";

service HelloWorld {
  rpc greet (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

上記を Python 用にコンパイルする。

$ python -m grpc_tools.protoc \
  --proto_path=. \
  --grpc_python_out=. \
  --python_out=. \
  helloworld.proto

すると、次のように pb2 という名前を含むファイルが二つ生成される。

$ ls | grep pb2
helloworld_pb2.py
helloworld_pb2_grpc.py

これらは、先ほどのスキーマ定義から生成された Python のモジュールになっている。

$ file helloworld_pb2.py 
helloworld_pb2.py: Python script text executable, ASCII text
$ file helloworld_pb2_grpc.py 
helloworld_pb2_grpc.py: Python script text executable, ASCII text

生成されたモジュールを使って gRPC のサーバを書いてみよう。 Protocol Buffers の定義にはインターフェースしか記述されていない。 そのため、内部で何をやるか実装を書いてやる必要がある。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from concurrent import futures
import time

import grpc

import helloworld_pb2
import helloworld_pb2_grpc


class HelloWorldService(helloworld_pb2_grpc.HelloWorldServicer):
    """サービスを定義する"""

    def greet(self, request, context):
        """RPC の中身"""
        # 受け取った内容を使って返信するメッセージを組み立てる
        message = 'Hello, {name}'.format(name=request.name)
        reply = helloworld_pb2.HelloReply(message=message)
        return reply


def main():
    # gRPC のサーバを用意する
    executor = futures.ThreadPoolExecutor(max_workers=10)
    server = grpc.server(executor)
    service = HelloWorldService()
    helloworld_pb2_grpc.add_HelloWorldServicer_to_server(service, server)

    # サーバを 37564 ポートで動作させる
    server.add_insecure_port('localhost:37564')
    server.start()

    try:
        while True:
            time.sleep(1)
    finally:
        server.stop(0)


if __name__ == '__main__':
    main()

サーバを起動してみよう。

$ python server.py

別のターミナルを開いてポートの状態を確認してみよう。 TCP で localhost:37564 を Listen していれば上手くいっている。

$ lsof -i | grep 37564
python3.7 11805 amedama    5u  IPv6 0xbd776a50dc8d08b1      0t0  TCP localhost:37564 (LISTEN)
python3.7 11805 amedama    6u  IPv6 0xbd776a50dc8d4231      0t0  TCP localhost:37564 (LISTEN)

続いてクライアントを記述しよう。 サーバが稼働している localhost:37564 ポートに接続させる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import grpc

import helloworld_pb2
import helloworld_pb2_grpc


def main():
    # 'localhost:37564' に接続する
    with grpc.insecure_channel('localhost:37564') as channel:
        # メッセージと共にリモートプロシジャを呼び出す
        stub = helloworld_pb2_grpc.HelloWorldStub(channel)
        reply = stub.greet(helloworld_pb2.HelloRequest(name='World'))
        # 返ってきた内容を表示する
        print('Reply:', reply.message)


if __name__ == '__main__':
    main()

上記を実行してみよう。 次のようにメッセージが表示されれば上手くいった。

$ python client.py 
Reply: Hello, World

通信を Wireshark でキャプチャする

さて、これだけだとあまり面白くないので、続いては上記の通信をキャプチャしてみよう。

パケットキャプチャをするために Wireshark をインストールする。

$ brew cask install wireshark

インストールできたら Wireshark を起動する。

$ wireshark

起動したら Loopback インターフェースのキャプチャを開始する。 また、ディスプレイフィルタのバーに tcp.port == 37564 を指定する。 これで余計な内容が表示されなくて済む。

f:id:momijiame:20190630152902p:plain

準備ができたら、先ほどの gRPC サーバを起動する。

$ python server.py

そして、gRPC クライアントを実行する。

$ python client.py

すると、次のように TCP の通信内容がキャプチャされる。

f:id:momijiame:20190630153107p:plain

適当にフレームを選択して「Follow > TCP Stream」すると一連の TCP の通信内容が確認できる。

f:id:momijiame:20190630153245p:plain

HTTP/2.0 という文字列から読み取れる通り、gRPC は通信部分のレイヤーに HTTP2 を採用している。

生の TCP だと読みにくいので、通信を HTTP/2 として解釈させてみよう。 先ほどと同じようにフレームを選択したら「Decode As...」を選択する。

f:id:momijiame:20190630153445p:plain

上記のような画面が開いたら右端のカラムを「HTTP2」にする。 これで、通信プロトコルが HTTP/2 として解釈される。

HTTP/2 において HTTP のリクエストとレスポンスは HeadersData のメッセージでやり取りされる。 以下のように、まずリクエストが出ている。 メッセージの内容に World が含まれることが分かる。 また、呼び出す対象のリモートプロシジャは HTTP のパス部分に格納されるようだ。

f:id:momijiame:20190630153533p:plain

同様に、上記に対するレスポンスが以下になる。 メッセージに Hello, World という内容が含まれることが分かる。

f:id:momijiame:20190630153821p:plain

なかなか分かりやすいね。