PythonでgRPCで双方向通信を試す。

[gRPC-Webも正式リリース](https://www.publickey1.jp/blog/18/grpc-webwebgrpc.html)され、ますます便利になりそうな予感のgRPC。
HTTP/2ベースの双方向通信もできるということで面白そうなので試してみました。

# 試したこと

* Protocol Buffersの定義からPythonでClient・Server実装
* Client→Serverにコネクション
* Clientからメッセージ送付
* Serverから同じコネクションを通してメッセージを複数返す

チャットの通信のやり取りみたいなイメージのものをgRPCで実装してみます。


# サービス定義を実施

Protocol Bufferの定義を作成。
sample.protoファイルの作成。

```proto:sample.proto

syntax = "proto3";

package sample;

message HelloMessage{
  string name = 1;
  string msg = 2;
}

message ReplyMessage{
  string reply_msg = 1;
}

service SampleService{
  rpc HelloServer (stream HelloMessage) returns (stream ReplyMessage) {}
}
```

ここでポイントは、bidirectional(双方向)通信にするためにHelloServerのrpcの引数、戻りにstreamをつけている点。

# grpcio-toolsのインストール

Pythonでgrpcを実装するために、まずはライブラリインストール。

```
$ pip install grpcio-tools
```

# コード生成用スクリプト作成

protocコマンドで直接実行でも良さそうだが、pythonのコード化しておきます。


```python:codegen.py

from grpc.tools import protoc


protoc.main(
    (
        '',
        '-I.',
        '--python_out=.',
        '--grpc_python_out=.',
        './sample.proto',
    )
)
```

実行します。

```shell
$ python3 ./codegen.py
```

自動的に以下のファイルが生成されます。

* sample_pb2.py
* sample_pb2_grpc.py

sample_pb2_grpcには、ProtocolBufferで定義したサービスの実装内容が含まれます。


# Server側の実装

sample_server.pyを新たに作成してサーバ側の処理を実装します。

Server側には、自動生成されたSampleServiceServicerクラスを継承したクラスを実装し、その中に定義した各メソッドを実装すればOKです。

```python:sample_server.py
import time
from concurrent import futures
import grpc
import sample_pb2
import sample_pb2_grpc

class SampleServiceServicer(sample_pb2_grpc.SampleServiceServicer):

    def __init__(self):
        pass

    def HelloServer(self, request_iterator, context):
        for new_msg in request_iterator:
            reply_msgs = []
            print('Receive new message! [name: {}, msg: {}]'.format(new_msg.name, new_msg.msg))
            reply_msgs.append(sample_pb2.ReplyMessage(reply_msg='{} {}'.format(new_msg.msg, new_msg.name)))
            reply_msgs.append(sample_pb2.ReplyMessage(reply_msg='Nice to meet you!!!'))
            for message in reply_msgs:
                yield message

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    sample_pb2_grpc.add_SampleServiceServicer_to_server(SampleServiceServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    print('Starting gRPC sample server...')
    try:
        while True:
            time.sleep(3600)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()
```

HelloServerのメソッド内でyieldで一旦停止してリターンすることで、複数のメッセージを返すことができます。
戻す内容はProtocol Buffersの定義で記載したReplyMessageの形式に合わせて置くことでその内容がそのままClient側に送信されます。

上記の例だと、送信されてきたHelloMessage型のmsgの内容にnameの内容を付与したメッセージと、'Nice to meet you!!'のメッセージの2件のデータがClient側に送付されます。

# Client側の実装

Client側はServerに対してHelloServerメソッドを呼び出すコードを記述します。

```python:sample_client.py
import grpc
import sample_pb2
import sample_pb2_grpc

def hello_server(stub, name):
    messages = []
    messages.append(sample_pb2.HelloMessage(name=name, msg='Hello'))
    responses = stub.HelloServer(iter(messages))
    for response in responses:
        print('Received message {}'.format(response.reply_msg))

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = sample_pb2_grpc.SampleServiceStub(channel)
        print('--Please input your name--')
        while True:
            name = input("What's your name? > ")
            hello_server(stub, name)

if __name__ == '__main__':
    run()
```

この例では、標準入力からの入力内容をnameに格納してHelloというメッセージを元にHelloServerを呼び出しています。
上記の例では、1メッセージのみを送付する例ですが、複数のメッセージをまとめて送付するといったことも可能です。

# 実行

sample_server.pyとsample_client.pyを起動します。

client側から適当に以下のように名前を入力してみると、server側ではそのリクエストを受信し、2件のメッセージを返していることがわかります。

![gRPC_Client.png](https://qiita-image-store.s3.amazonaws.com/0/45851/ee64d430-f069-304c-cb2f-c1bfea9a1778.png)


![gRPC_Server.png](https://qiita-image-store.s3.amazonaws.com/0/45851/2eb7f53a-f8c6-5ff1-30fb-c053f1ec523f.png)


# まとめ

定義に基づいて自動でライブラリが生成されるのは非常に便利です。今回はServerもClientも両方Pythonで実装しましたが、Client側はGolangでとかも同じ定義ファイルから生成できるので管理が楽になりそうな感じです。双方向通信の部分はまだ細かくは把握できていないですが、簡単に試すことはできたので使い所はありそうな予感です。