1.はじめに
ElixirでgRPCを使う機会があったので、基本的な手順をまとめてみました。
今回は、Elixir対Python間での通信を試してみます。
2.準備
$ cd (各自の作業ディレクトリに移動)
$ mkdir grpchello
$ cd !$
※今回の試行では、下記のようなディレクトリ構成を想定しています。
<working dir>
+ grpchello
+ hello.proto protoファイル
+ exgrpc Elixir版プロジェクト
+ lib Elixir版コード
+ pygrpc Python版プロジェクト
+ pygrpc Python版コード
protobuf-compilerをインストールしておきます。
$ sudo apt install protobuf-compiler -y
3.プロトコルバッファ
下記の構造体でメッセージをやり取りします。
順番 | 型 | 変数名 |
---|---|---|
1 | string | message |
2 | int32 | value |
protoファイルを作成します。
$ touch helloworld.proto
syntax = "proto3";
package hello;
// エンドポイント定義
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
// 型定義・リクエスト
message HelloRequest {
// 文字列
string message = 1;
// 整数
int32 value = 2;
}
// 型定義・レスポンス
message HelloResponse {
string message = 1;
int32 value = 2;
}
4.Elixir→Python間
本章では、Elixir側:クライアント、Python側:サーバーの挙動を試行します。
役割 | Elixir | Python | TCP/IPポート |
---|---|---|---|
クライアント | 〇 | 5001 | |
サーバー | 〇 | (同上) |
(1)クライアント・Elixir側
プロジェクトを作成します。
(あとでsupervisorも使うので、--sup
オプションも付加します)
$ mix new exgrpc --sup && cd exgrpc
ライブラリを追加します。
・・・(省略)・・・
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
# ↓追記
{:grpc, "~> 0.7.0"},
{:protobuf, "~> 0.12.0"}
]
end
・・・(省略)・・・
ライブラリの依存関係を解決します。
$ mix deps.get
protoファイルを解析してElixirのコードを生成するツールをインストールします。
$ mix escript.install hex protobuf
Resolving Hex dependencies...
・・・(省略)・・・
* creating /home/username/.asdf/installs/elixir/1.15.5-otp-25/.mix/escripts/protoc-gen-elixir
protoファイルを解析してElixirのコードを生成します。
# 先ほどのツールが使えるようにパスを通す
$ export PATH=/home/username/.asdf/installs/elixir/1.15.5-otp-25/.mix/escripts/:$PATH
# ひとつ上のディレクトリのprotoファイルから、Elixir用のgRPCコードを生成
$ protoc --elixir_out=plugins=grpc:./lib -I../ ../hello.proto
$ ls ./lib/
exgrpc.ex hello.pb.ex
protoc
コマンド実行時のエラー例
パスの通し忘れ
protoc-gen-elixir: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--elixir_out: protoc-gen-elixir: Plugin failed with status code 1.
オプション-I
の指定忘れ
../pygrpc/proto/helloworld.proto: File does not reside within any path specified using --proto_path (or -I). You must specify a --proto_path which encompasses this file. Note that the proto_path must be an exact prefix of the .proto file names -- protoc is too dumb to figure out when two paths (e.g. absolute and relative) are equivalent (it's harder than you think).
Elixir側のコードを書きます。
# 空のファイルを作成
$ touch lib/exgrpc/client.ex
defmodule Exgrpc.Client do
@moduledoc """
クライアント側・Elixir版
"""
@doc """
クライアント
## Parameters
- message: 送信する文字列
- value: 送信する数値
## Examples
"""
def request(message \\ "grpc-elixir", value \\ 1) do
# 指定のホストにリクエストを送信
ipport = "localhost:5001"
{:ok, channel} = GRPC.Stub.connect(ipport)
request = Hello.HelloRequest.new(message: message, value: value)
{:ok, reply} = channel |> Hello.HelloService.Stub.say_hello(request)
IO.puts("> response from server")
IO.puts("---")
IO.inspect(reply)
IO.puts("---")
{:ok, _} = GRPC.Stub.disconnect(channel)
:ok
end
end
ビルド、通信テストします。(相手側がいないので、数秒後に通信エラーにはなります)
# ビルド
$ iex -S mix
iex(1)> Exgrpc.request
** (MatchError) no match of right hand side value: {:error, :timeout}
(exgrpc 0.1.0) lib/exgrpc.ex:24: Exgrpc.request/1
iex:1: (file)
(2)サーバー・Python側
poetryでPython環境を作ります。
$ poetry new pygrpc
$ cd pygrpc/
# 必要なライブラリをインストール
$ poetry add grpcio
$ poetry add grpcio-tools
# 空のファイルを作成
$ touch pygrpc/server.py
# 実行権限を付与(poetry runするときに実行権限が必要)
$ chmod 755 pygrpc/server.py
protoファイルを解析してPythonのコードを生成します。
# ひとつ上のディレクトリのprotoファイルから、Python用のgRPCコードを生成
$ poetry run python -m grpc_tools.protoc \
-I../ \
--python_out=./ \
--pyi_out=./ \
--grpc_python_out=./ \
../hello.proto
Python側のコードを書きます。
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""サーバー側・Python版
"""
import pprint
from concurrent import futures
import grpc
import hello_pb2
import hello_pb2_grpc
class Greeter(hello_pb2_grpc.HelloServiceServicer):
"""レスポンスの処理"""
def SayHello(self, request, context):
"""クライアントからのリクエストに対するレスポンスの処理"""
# リクエストされた内容を表示
print("> request from client")
print("---")
pprint.pprint(request)
print("---")
# 返り値としてリクエストの内容を文字列に組み立てる。クライアントにはこの文字列が表示される
retval1 = f"hello: {request.message} / {request.value}"
retval2 = request.value * 10
return hello_pb2.HelloResponse(message=retval1, value=retval2)
def server():
"""サーバー"""
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
hello_pb2_grpc.add_HelloServiceServicer_to_server(Greeter(), server)
# 受付ポートを指定してサーバー起動
ipport = "[::]:5001"
server.add_insecure_port(ipport)
server.start()
print("running...")
server.wait_for_termination()
if __name__ == "__main__":
try:
print("-- gRPC Server [Python] --")
# サーバー起動
server()
except KeyboardInterrupt:
# [Ctrl-C]が押されたとき
print("SIGINT - Exit")
except:
# 例外発生時にメッセージ
import traceback
traceback.print_exc()
finally:
print("done.")
(3)試行
ターミナルを2つ開いて、それぞれ実行します。
$ poetry run pygrpc/server.py
-- gRPC Server [Python] --
running...
# ↓ここからがクライアントのリクエストに対する応答
> request from client
---
message: "grpc-elixir"
value: 1
---
> request from client
---
message: "fuga"
value: 2
---
$ cd grpchello/exgrpc
$ iex -S mix
Erlang/OTP 25 [erts-13.2.2.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit:ns]
Interactive Elixir (1.15.5) - press Ctrl+C to exit (type h() ENTER for help)
# ↓1つ目のリクエストを送信
iex(1)> Exgrpc.Client.request
> response from server
---
%Hello.HelloResponse{
message: "hello: grpc-elixir / 1",
value: 10,
__unknown_fields__: []
}
---
:ok
# ↓2つ目のリクエストを送信
iex(2)> Exgrpc.Client.request("fuga", 2)
> response from server
---
%Hello.HelloResponse{
message: "hello: fuga / 2",
value: 20,
__unknown_fields__: []
}
---
:ok
このように、簡単に異言語間のGRPCを作る事ができました。
poetory
コマンド実行時のエラー例
pyファイルに実行権限がない、あるいは、コードの先頭に#!python3
が書かれていない
PermissionError
[Errno 13] Permission denied: '/home/username/gitwork/python/grpchello/pygrpc/pygrpc/server.py'
カレントディレクトリの間違い・プロジェクトのディレクトリ以外でrunした
RuntimeError
Poetry could not find a pyproject.toml file in /home/username/gitwork/python/grpchello or its parents
5.Python→Elixir間
つづいて本章では、Python側:クライアント、Elixir側:サーバーの挙動を試行します。
役割 | Elixir | Python | TCP/IPポート |
---|---|---|---|
クライアント | 〇 | 5002 | |
サーバー | 〇 | (同上) |
(1)サーバー・Elixir側
# 空のファイルを作成
$ touch lib/exgrpc/server.ex
defmodule Exgrpc.Server do
@moduledoc """
サーバー側・Elixir版
"""
use GRPC.Server, service: Hello.HelloService.Service
@doc """
クライアントからのリクエストに対するレスポンスの処理
"""
@spec say_hello(Hello.HelloRequest.t(), GRPC.Server.Stream.t()) ::
Hello.HelloReply.t()
def say_hello(request, _stream) do
# リクエストされた内容を表示
IO.puts("> request from client")
IO.puts("---")
IO.inspect(request)
IO.puts("---")
# 返り値としてリクエストの内容を文字列に組み立てる。クライアントにはこの文字列が表示される
retval1 = "hello: #{request.message} / #{request.value * 10}"
retval2 = request.value * 10
Hello.HelloResponse.new(message: retval1, value: retval2)
end
end
さらに、既存のファイルのコードも修正します。
defmodule Exgrpc.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: Exgrpc.Worker.start_link(arg)
# {Exgrpc.Worker, arg}
# ↓ここを追記
# ↓コメントを外したら、GRPC.Server.Supervisorを自動起動。ポートは5002とする
# {GRPC.Server.Supervisor, endpoint: Exgrpc.Endpoint, port: 5002, start_server: true}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Exgrpc.Supervisor]
Supervisor.start_link(children, opts)
end
end
# ↓このモジュールを追記
defmodule Exgrpc.Endpoint do
@moduledoc """
エンドポイントを定義
"""
use GRPC.Endpoint
intercept(GRPC.Server.Interceptors.Logger)
run(Exgrpc.Server)
end
上記ソースコードの、↓ここを追記
の部分
コメントを外してビルドすると、後述の$ iex -S mix
でiexシェルを立ち上げた時に、自動的にサーバーを起動します。
このとき、後述の試行で、手順通りにGRPC.Server.Supervisor.start_link
を実行すると、下記のエラーが表示されます。
iex(1)> {:ok, pid} = GRPC.Server.Supervisor.start_link([endpoint: Exgrpc.Endpoint, port: 5002, start_server: true])
23:31:21.244 [error] Failed to start Ranch listener "Exgrpc.Endpoint" in :ranch_tcp:listen([cacerts: :..., key: :..., cert: :..., port: 5002]) for reason :eaddrinuse (address already in use)
** (EXIT from #PID<0.260.0>) shell process exited with reason: shutdown: failed to start child: {:ranch_listener_sup, "Exgrpc.Endpoint"}
** (EXIT) shutdown: failed to start child: :ranch_acceptors_sup
** (EXIT) {:listen_error, "Exgrpc.Endpoint", :eaddrinuse}
Interactive Elixir (1.15.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
(2)クライアント・Python側
# 空のファイルを作成
$ touch pygrpc/pygrpc/client.py
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""クライアント側・Python版
ref:
- https://grpc.io/docs/languages/python/quickstart/
"""
import time
import pprint
import grpc
import hello_pb2
import hello_pb2_grpc
def run(message: str = "hoge", value: int = 1):
"""クライアント
Args:
message (str, optional): 送信する文字列. Defaults to "hoge".
value (int, optional): 送信する数値. Defaults to 1.
"""
try:
# 指定のホストにリクエストを送信
ipport = "localhost:5002"
with grpc.insecure_channel(ipport) as channel:
stub = hello_pb2_grpc.HelloServiceStub(channel)
response = stub.SayHello(
hello_pb2.HelloRequest(message=message, value=value)
)
print("> response from server")
print("---")
pprint.pprint(response)
print("---")
except grpc.RpcError as rpc_error:
# 例外発生の際の処理
if rpc_error.code() == grpc.StatusCode.CANCELLED:
print("CANCELLED")
elif rpc_error.code() == grpc.StatusCode.UNAVAILABLE:
print("UNAVAILABLE")
else:
print(
f"Received unknown RPC error: code={rpc_error.code()} message={rpc_error.details()}"
)
if __name__ == "__main__":
try:
print("-- gRPC Client [Python] --")
# サーバーに2件送信
run()
time.sleep(1)
run("fuga", 2)
except KeyboardInterrupt:
# [Ctrl-C]が押されたとき
print("SIGINT - Exit")
except:
# 例外発生時にメッセージ
import traceback
traceback.print_exc()
finally:
print("done.")
(3)試行
ターミナルを2つ開いて、それぞれ実行します。
$ cd grpchello/exgrpc
$ iex -S mix
# GRPC.Server.Supervisorを起動。ポートは5002とする
iex(1)> {:ok, pid} = GRPC.Server.Supervisor.start_link([endpoint: Exgrpc.Endpoint, port: 5002, start_server: true])
22:46:31.418 [info] Running Exgrpc.Endpoint with Cowboy using http://0.0.0.0:5002
{:ok, #PID<0.243.0>}
# ↓ここからがクライアントのリクエストに対する応答
> request from client
---
iex(2)>
22:46:38.780 [info] Handled by Exgrpc.Server.say_hello
%Hello.HelloRequest{message: "hoge", value: 1, __unknown_fields__: []}
---
iex(2)>
22:46:38.783 [info] Response :ok in 3ms
> request from client
iex(2)>
22:46:39.788 [info] Handled by Exgrpc.Server.say_hello
---
%Hello.HelloRequest{message: "fuga", value: 2, __unknown_fields__: []}
---
iex(2)>
22:46:39.789 [info] Response :ok in 113µs
$ cd grpchello/pygrpc
# ↓2つのリクエストを送信
$ poetry run pygrpc/client.py
-- gRPC Client [Python] --
> response from server
---
message: "hello: hoge / 10"
value: 10
---
> response from server
---
message: "hello: fuga / 20"
value: 20
---
done.
6.まとめ
過去にも試したことがあったのですが、いろいろはまってしまって、汎用的な内容にまとまらなかったので、「未解決」記事になっていました。
といったところで、ようやくElixir~Python間でのgRPC通信ができました。
7.参考資料
-
Elixir
-
Python