今回は、最近よく使われている 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()
でやり取りするのは HelloRequest
と HelloReply
というメッセージ。
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
を指定する。
これで余計な内容が表示されなくて済む。
準備ができたら、先ほどの gRPC サーバを起動する。
$ python server.py
そして、gRPC クライアントを実行する。
$ python client.py
すると、次のように TCP の通信内容がキャプチャされる。
適当にフレームを選択して「Follow > TCP Stream」すると一連の TCP の通信内容が確認できる。
HTTP/2.0
という文字列から読み取れる通り、gRPC は通信部分のレイヤーに HTTP2 を採用している。
生の TCP だと読みにくいので、通信を HTTP/2 として解釈させてみよう。 先ほどと同じようにフレームを選択したら「Decode As...」を選択する。
上記のような画面が開いたら右端のカラムを「HTTP2」にする。 これで、通信プロトコルが HTTP/2 として解釈される。
HTTP/2 において HTTP のリクエストとレスポンスは Headers
と Data
のメッセージでやり取りされる。
以下のように、まずリクエストが出ている。
メッセージの内容に World
が含まれることが分かる。
また、呼び出す対象のリモートプロシジャは HTTP のパス部分に格納されるようだ。
同様に、上記に対するレスポンスが以下になる。
メッセージに Hello, World
という内容が含まれることが分かる。
なかなか分かりやすいね。