(この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018」の18日目です)
昨日は @artk さんの「GigalixirでSlackBotを動かしてみる」でした。本日は「grpc-elixirでGoと通信してみる #1」の続きです。
Route Guide サンプル
前回はGoとElixirのgRPC通信でhelloworldがうまくいきました。今回はroute_guideというサンプルを試します。
このサンプルは、ストリーミング(アップロード、ダウンロード、全二重)が含まれます。
この記事では触れませんが、grpcでストリーミングが必用になった場合はこのサンプルコードをあたると良いでしょう。
Elixirのroute_guideサンプルはこちら。
Goのサンプルはこちらです。
プログラムのポイント
残念ながら現時点ではElixirのgRPCプログラミングガイドはないので、Goのものと見比べて実装を追う必要があります。
以下、双方向ストリーミングRPCのクライアントプログラムで見比べてみます。runRouteChatは、RPCのクライアントプログラムです。
Goではストリーミングにgoroutineを使って並列で処理をしています。
func runRouteChat(client pb.RouteGuideClient) {
notes := []*pb.RouteNote{
{Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "First message"},
{Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Second message"},
{Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Third message"},
{Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "Fourth message"},
{Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Fifth message"},
{Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Sixth message"},
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.RouteChat(ctx)
if err != nil {
log.Fatalf("%v.RouteChat(_) = _, %v", client, err)
}
waitc := make(chan struct{})
go func() {
for {
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("Failed to receive a note : %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
for _, note := range notes {
if err := stream.Send(note); err != nil {
log.Fatalf("Failed to send a note: %v", err)
}
}
stream.CloseSend()
<-waitc
}
一方Elixir側の対応する関数です。Task.async/Task.awaitで並列ストリーミングを行います。
def run_route_chat(channel) do
data = [
%{lat: 0, long: 1, msg: "First message"},
%{lat: 0, long: 2, msg: "Second message"},
%{lat: 0, long: 3, msg: "Third message"},
%{lat: 0, long: 1, msg: "Fourth message"},
%{lat: 0, long: 2, msg: "Fifth message"},
%{lat: 0, long: 3, msg: "Sixth message"}
]
stream = channel |> Routeguide.RouteGuide.Stub.route_chat()
notes =
Enum.map(data, fn %{lat: lat, long: long, msg: msg} ->
point = Routeguide.Point.new(latitude: lat, longitude: long)
Routeguide.RouteNote.new(location: point, message: msg)
end)
task =
Task.async(fn ->
Enum.reduce(notes, notes, fn _, [note | tail] ->
opts = if length(tail) == 0, do: [end_stream: true], else: []
GRPC.Stub.send_request(stream, note, opts)
tail
end)
end)
{:ok, result_enum} = GRPC.Stub.recv(stream)
Task.await(task)
Enum.each(result_enum, fn {:ok, note} ->
IO.puts(
"Got message #{note.message} at point(#{note.location.latitude}, #{
note.location.longitude
})"
)
end)
end
見比べてみてどうしたでしょうか? 関数名はGoの実装を踏襲してるため、一目でわかるのではないかと思います。(構造体の記述が多い分、Elixirのコードが長めになっていますね。)
前回の続き
さて、これまで説明してきたroute_guideサンプルでElixir-Go間の通信を行います。
コンテナから抜けて、2つのコンテナを終了しましょう。 docker-comopose down
ではコンテナの終了と破棄を同時に行ってくれます。
# exit
$ docker-compose down
Route Guide : Elixir -> Golang
Editorマークをクリックしてdocker-compose.yml
を編集をします。
docker-compose.yml
のcommandの#
を外して、Saveボタンを忘れずに押してください。
(注記:command:の字下げは、build:のカラムに合わせてください)
go-node:
build:
context: .
dockerfile: Dockerfile-go
command: sh -c "cd /go/src/google.golang.org/grpc/examples/route_guide/ && server/server"
これでGolang側のRoute Guideサーバー起動準備ができました。
以下、コンテナを立ち上げます。
※ エラーが出る場合は、エディターで右端が切れてないかをチェックしてみて下さい。
$ docker-compose up -d
docker ps
コマンドで、コンテナが2つ立ち上がってることを確認したら次へ進みます。コンテナが1つしかない場合は、docker-compose down
でコンテナを落としてEditorでymlの編集内容を見直してください。
以下elixir-nodeのコンテナに入ります。
$ docker-compose exec elixir-node ash
今度はroute_guideのフォルダに移動、依存関係の取得・コンパイル。
# cd ~/grpc-elixir/examples/route_guide
# mix deps.get && mix compile
サーバー接続先を書き替えます
# vi priv/client.exs
# 9行目
{:ok, channel} = GRPC.Stub.connect("localhost:10000", opts)
↓
{:ok, channel} = GRPC.Stub.connect("go-node:10000", opts)
# mix run priv/client.exs
実行すると、以下から始まる大量のテキストが流れれば接続は成功です。
ストリーミングを含む接続がうまく行ってるのが確認できました。
Getting feature for point (409146138, -746188906)
<name: "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", location: <latitude: 409146138, longitude: -746188906>>
Getting feature for point (0, 0)
<name: nil, location: <latitude: 0, longitude: 0>>
Looking for features within %Routeguide.Rectangle{hi: <latitude: 420000000, longitude: -730000000>, lo: <latitude: 400000000, longitude: -750000000>}
<name: "Patriots Path, Mendham, NJ 07945, USA", location: <latitude: 407838351, longitude: -746143763>>
~ 略 ~
Route Guide : Golang -> Elixir
今度は逆方向をやってみます。
# mix grpc.server
これで、elixir-nodeのgRPCサーバーが起動するので、Ctrl+p Ctrl+qでコンテナからデタッチします。
go-nodeコンテナに入ります
$ docker-compose exec go-node bash
route_guideフォルダーに移動して
# cd /go/src/google.golang.org/grpc/examples/route_guide
route_guideクライアントをelixir-nodeに向けて実行します。こちらはhelloworldと違ってサーバーのオプション起動が付いているので楽です。
elixir-nodeのport=10000に対して、リクエストしてみます。
# client/client -server_addr elixir-node:10000
先ほどのように以下から始まるテキストが流れたら成功です。(Elixirと若干フォーマットが違いますね。)
2018/12/10 03:42:56 Getting feature for point (409146138, -746188906)
2018/12/10 03:42:56 name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
2018/12/10 03:42:56 Getting feature for point (0, 0)
・・・・
以上で、公式サンプルのgRPC双方向通信が確認できました。
終了
以下でコンテナを抜けることができます。
# exit
作業が終了したら、左上の「CLOSE SESSION」ボタンを押すと、コンテナホストごと消去されます。
お疲れ様でした。
明日は@piacere_exさんの「BASIC以来、35年間プログラミングしてないIT企業社長が、ElixirでWebアプリを作った」です。お楽しみに!