grpc-elixirでGoと通信してみる #2

（この記事は、「fukuoka.ex Elixir／Phoenix Advent Calendar Advent Calendar 2018」の18日目です）

昨日は @artk さんの「GigalixirでSlackBotを動かしてみる」でした。本日は[「grpc-elixirでGoと通信してみる #1」](https://qiita.com/twinbee/items/644dcd6f3ed5924259cd)の続きです。

## Route Guide サンプル

前回はGoとElixirのgRPC通信でhelloworldがうまくいきました。今回は[route_guide](https://grpc.io/docs/tutorials/basic/go.html)というサンプルを試します。

このサンプルは、ストリーミング(アップロード、ダウンロード、全二重）が含まれます。

この記事では触れませんが、grpcでストリーミングが必用になった場合はこのサンプルコードをあたると良いでしょう。

Elixirのroute_guideサンプルは[こちら](https://github.com/tony612/grpc-elixir/tree/master/examples/route_guide)。

Goのサンプルは[こちら](https://github.com/grpc/grpc-go/tree/master/examples/route_guide)です。

## プログラムのポイント

 残念ながら現時点ではElixirのgRPCプログラミングガイドはないので、Goのものと見比べて実装を追う必要があります。

 以下、双方向ストリーミングRPCのクライアントプログラムで見比べてみます。runRouteChatは、RPCのクライアントプログラムです。

 Goではストリーミングにgoroutineを使って並列で処理をしています。

```go:
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で並列ストリーミングを行います。

```elixir
 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間の通信を行います。

コンテナから抜けて、２つのコンテナを終了しましょう。 `docker-comopose down`ではコンテナの終了と破棄を同時に行ってくれます。

```
# exit
$ docker-compose down
```

### Route Guide : Elixir -> Golang

Editorマークをクリックして`docker-compose.yml`を編集をします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/66748/98d4b200-e1eb-d7ed-5b17-03b081d71918.png)

`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サーバー起動準備ができました。
以下、コンテナを立ち上げます。

※ エラーが出る場合は、エディターで右端が切れてないかをチェックしてみて下さい。

```bash
$ docker-compose up -d
```

`docker ps`コマンドで、コンテナが２つ立ち上がってることを確認したら次へ進みます。コンテナが１つしかない場合は、`docker-compose down`でコンテナを落としてEditorでymlの編集内容を見直してください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/66748/ee78aab2-6451-b073-6a65-e65244672e3f.png)

以下elixir-nodeのコンテナに入ります。

```bash
$ docker-compose exec elixir-node ash
```

今度はroute_guideのフォルダに移動、依存関係の取得・コンパイル。

```

# cd ~/grpc-elixir/examples/route_guide
# mix deps.get && mix compile
```

サーバー接続先を書き替えます

```
# vi priv/client.exs
```

```elixir: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アプリを作った](https://qiita.com/piacere_ex/items/59bf9cb86d77a8857672)」です。お楽しみに！