RPC
ここではXML-RPCを試した。サーバ、クライアントでrequest/responseをしただけの通信。
シンプルなHTTP/1.1通信になっている
TCP
request
HTTP/1.1 の POST で 5, 33 の足し算を依頼している
response
HTTP/1.1 packets
テスト方法
from xmlrpc.server import SimpleXMLRPCServer
def add(x, y):
return x + y
server = SimpleXMLRPCServer(("localhost", 8000))
server.register_function(add, "add")
print("Serving XML-RPC on port 8000...")
server.serve_forever()
import xmlrpc.client
proxy = xmlrpc.client.ServerProxy("http://localhost:8000/")
result = proxy.add(5, 33)
print(f"The result of 5 + 3 is: {result}")
gRPC
HTTP/2 を使ってrequest/responseが行き来した。windows sizeの調整が頻繁に行われ、pingも飛び交っている
TCP
HTTP/2
HTTP/1.1に比べて多くのやり取りがある。
POST している TCP packet
POST 通信についていうと、HTTP/1.1は1つのパケットの中にheaderもbodyもあったけど、HTTP/2は分かれてい流みたい。まず HEADERS パケットを送り、ここで POST / に送ることを宣言する。直後の WINDOW_UPDATE を挟んで、DATA パケットで body を送信。その直後も WINDOW_UPDATE だ。windowの位置を細かにやりとりすることで効率的なネットワーク利用をしているらしい。
このTCP packet一つの中に、5つのHTTP/2 packetが入っている。
HyperText Transfer Protocol 2
Stream: SETTINGS, Stream ID: 0, Length 0
HyperText Transfer Protocol 2
Stream: HEADERS, Stream ID: 1, Length 204, POST /Greeter/SayHello
HyperText Transfer Protocol 2
Stream: WINDOW_UPDATE, Stream ID: 1, Length 4
HyperText Transfer Protocol 2
Stream: DATA, Stream ID: 1, Length 12
HyperText Transfer Protocol 2
Stream: WINDOW_UPDATE, Stream ID: 0, Length 4
そして Stream: DATA
の中にはGRPC Messageがあり、さらにその中には Protocol Buffers が入っている
GRPC Message: /Greeter/SayHello, Request
Message Data: 7 bytes
Protocol Buffers: /Greeter/SayHello,request
Value: 576f726c64
HTTP/2-DATA( GRPC Message ( Protocol Buffers ) )
という感じの構造だ
Value: 576f726c64
がbodyの実態、 World
という文字列だ。サーバはそれを使って Hello World
を返却する関数になってる。
gRPCのヘッダの前に入っている SETTINGS[0] とその後の WINDOW_UPDATE[1] が (HTTP/2 を知らない自分には) 特徴的だ。勉強が必要
gRPCが通信を削減するのか試す
Clientから User(name="John Doe", age=30)
というオブジェクトを送信する。
先にresponseから見ていく。Responseはそのままのtextデータが含まれている(実質binaryかもしれない)。User Name: John Doe, Age: 30
は28文字であり、value length も 28 bytesとなっている。
一方requestは、たった12 bytesしかない。切り詰めてname=John Doe
age=30
にしても19文字のはずだ。
Value: 0a084a6f686e20446f65101e
この内容はこうだ。
Value は次のように解釈できます:
0a
→ name フィールド
08
→ name の長さ
6a6f686e20446f65
→ John Doe
10
→ age フィールド
1e
→ age の値(30)
辞書のkeyが 0a
, 10
に短縮されている点が大きい。送受信側が共にobject構造を知っているためkeyを文字列でやり取りする必要がないのだ。
さらに、binary=hexになっている点も大きい。例えば 30
をascii文字で送る場合は2文字必要なので2byte必要だ。具体的には 0x33=3
, 0x30=0
となるため 30
を送るために 0x3330
の2bytesが必要になる。一方、0x1e
は数字の 30
を表す(16+14だね)。これなら1byteでいい。
これがprotobufの通信量削減だ。
how to test gRPC - text
grpcio-tools
syntax = "proto3";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. helloworld.proto
この2ファイルができる
$ ll *py
-rw-r--r--@ 1 user staff 1574 Oct 19 21:47 helloworld_pb2.py
-rw-r--r--@ 1 user staff 3340 Oct 19 21:47 helloworld_pb2_grpc.py
import grpc
from concurrent import futures
import time
# 生成されたファイルをインポート
import helloworld_pb2
import helloworld_pb2_grpc
# Greeterサービスを実装
class GreeterServicer(helloworld_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
response = helloworld_pb2.HelloReply()
response.message = f"Hello, {request.name}!"
return response
# サーバーを開始する関数
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
helloworld_pb2_grpc.add_GreeterServicer_to_server(GreeterServicer(), server)
server.add_insecure_port('[::]:50051')
print("Server is starting on port 50051...")
server.start()
try:
while True:
time.sleep(86400)
except KeyboardInterrupt:
server.stop(0)
print("Server stopped.")
if __name__ == '__main__':
serve()
import xmlrpc.client
proxy = xmlrpc.client.ServerProxy("http://localhost:8000/")
result = proxy.add(5, 33)
print(f"The result of 5 + 3 is: {result}")
how to test gRPC - Dictionary
grpcio-tools
syntax = "proto3";
package dictionary;
// User メッセージの定義
message User {
string name = 1;
int32 age = 2;
}
// リクエストとレスポンスメッセージの定義
message DictionaryRequest {
User user = 1;
}
message DictionaryResponse {
string message = 1;
}
// サービスの定義
service DictionaryService {
rpc GetUserInfo(DictionaryRequest) returns (DictionaryResponse);
}
$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. helloworld.proto
この2ファイルができる
$ ll *py
-rw-r--r--@ 1 user staff 1574 Oct 19 21:47 helloworld_pb2.py
-rw-r--r--@ 1 user staff 3340 Oct 19 21:47 helloworld_pb2_grpc.py
import grpc
from concurrent import futures
import time
import dictionary_pb2
import dictionary_pb2_grpc
class DictionaryService(dictionary_pb2_grpc.DictionaryServiceServicer):
def GetUserInfo(self, request, context):
user = request.user
message = f"User Name: {user.name}, Age: {user.age}"
return dictionary_pb2.DictionaryResponse(message=message)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
dictionary_pb2_grpc.add_DictionaryServiceServicer_to_server(DictionaryService(), server)
server.add_insecure_port('[::]:50051')
server.start()
print("Server is running on port 50051...")
try:
while True:
time.sleep(86400) # 1 day
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
serve()
import grpc
import dictionary_pb2
import dictionary_pb2_grpc
def run():
with grpc.insecure_channel('localhost:50051') as channel:
stub = dictionary_pb2_grpc.DictionaryServiceStub(channel)
user = dictionary_pb2.User(name="John Doe", age=30)
request = dictionary_pb2.DictionaryRequest(user=user)
response = stub.GetUserInfo(request)
print(response.message)
if __name__ == '__main__':
run()