Mac で Unity と Go 使って gRPC のサンプルを試してみた

# はじめに

Unity で gRPC といえば [MagicOnion](https://github.com/Cysharp/MagicOnion) だと思うのですが、
自分でも Unity で gRPC 環境構築して開発出来るようになるために、
試しにサーバ側を Go で クライアント側を Unity でサンプルを動かしてみました。

# gRPC の環境構築

homebrew を使用して gRPC をインストールします。

```shell
brew tap grpc/grpc
brew install grpc
```

次に Protocol Buffers をインストールします。

```shell
brew install protobuf
```

また Protocol Buffers のファイル (.proto) から Go のソースコードを出力出来るようにするため Go 用の Protocol Buffers の gRPC プラグインをインストールします。

```shell
go get -u -v github.com/golang/protobuf/protoc-gen-go

```

gRPC 用の proto ファイルを生成することが出来る環境が整いました。

# gRPC のサンプルを動かすための準備

今回はサーバ側を Go で検証進めていくため、
Go の gRPC プロジェクトを git clone してきます。

```shell
git clone git@github.com:grpc/grpc-go.git
```

サーバ側には git clone したプロジェクトの `grpc-go/examples/helloworld/greeter_server/main.go` を使用します。

```go:grpc-go/examples/helloworld/greeter_server/main.go
/*
 *
 * Copyright 2015 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

//go:generate protoc -I ../helloworld --go_out=plugins=grpc:../helloworld ../helloworld/helloworld.proto

// Package main implements a server for Greeter service.
package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
	port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.Name)
    // 5. gRPC サーバに SayHello の実行要求が来たら
    // Message に in.Name の先頭に Hello を付けて
    // helloworld.proto で定義した HelloReply を返却する
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    // 1. 50051 ポートの TCP リスナーを作成する
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
    // 2. gRPC サーバを起動する
	s := grpc.NewServer()
    // 3. helloworld.proto ファイルで定義したメソッドを gRPC サーバに関連付ける
	pb.RegisterGreeterServer(s, &server{})
    // 4. gRPC サーバの待受を 1. で作成した TCP リスナーで行う
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

```

クライアント側は Unity の公式サンプルプロジェクトを改修して動作検証します。
そのため、まずは gRPC プロジェクトを git clone します。

```shell
git clone git@github.com:grpc/grpc.git
```

Unity のサンプルプロジェクトは git clone したプロジェクトの `grpc/examples/csharp/HelloworldUnity` に存在します。

# Unity のサンプルプロジェクトのセットアップ

Unity のサンプルプロジェクトはそのままでは動かすことが出来ないため、
公式に記載されている手順に従って、gRPC プラグインのセットアップを行います。
https://github.com/grpc/grpc/tree/master/examples/csharp/HelloworldUnity#build

まずは gRPC のプラグインのダウンロードを行います。
https://packages.grpc.io/ に遷移した後、画面下部に `Build ID` という欄が出てくるので、一番上にあるリンクをクリックします。
（添付画像で言うと [24968d94-8256-4572-a2dd-27b91a8265da](https://packages.grpc.io/archive/2019/05/e553590a0ac50a91b32fb5cbd0b229ed62f6564e-24968d94-8256-4572-a2dd-27b91a8265da/index.xml) というリンクです）

<img width="1144" alt="スクリーンショット 2019-05-29 1.31.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/6f598385-0629-73b3-4d00-b3de87588375.png">

すると画面遷移して gRPC の unity プラグインが zip でダウンロード出来るリンクが出現するのでクリックしてダウンロードします。
(添付画像でいうと [grpc_unity_package.1.22.0-dev.zip](https://packages.grpc.io/archive/2019/05/e553590a0ac50a91b32fb5cbd0b229ed62f6564e-24968d94-8256-4572-a2dd-27b91a8265da/csharp/grpc_unity_package.1.22.0-dev.zip) というリンクです)
<img width="1285" alt="スクリーンショット 2019-05-29 1.34.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/5de2c73f-e4fa-d989-c17d-058c6c449593.png">

ダウンロード完了後、zip を解凍すると Plugins フォルダが出てきます。
それを Unity サンプルプロジェクトの Asset フォルダ配下に設置します。 
`grpc/examples/csharp/HelloworldUnity/Assets` の下です。 


これで `grpc/examples/csharp/HelloworldUnity` が Unity で正常に開けるようになっているはずです。

# サーバとクライアントで gRPC 通信を行う

まずはサーバ側の Go を動かします。`grpc-go/examples/helloworld/greeter_server` で下記コマンドを実行して、Go の gRPC サーバを起動します。

```shell
go run main.go 
```

Unity 側では `Scripts/HelloWorldTest.cs` を少し変更します。
デフォルトのサンプルでは Unity 側でもサーバとして動作するようになっているため、
Unity 側でサーバとして振る舞わないよう関連コードをコメントアウトしています。

```csharp:grpc/examples/csharp/HelloworldUnity/Scripts/HelloWorldTest.cs
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using UnityEngine;
using System.Threading.Tasks;
using System;
using Grpc.Core;
using Helloworld;

class HelloWorldTest
{
  // Can be run from commandline.
  // Example command:
  // "/Applications/Unity/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -executeMethod HelloWorldTest.RunHelloWorld -logfile"
  public static void RunHelloWorld()
  {
    Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None);

    Debug.Log("==============================================================");
    Debug.Log("Starting tests");
    Debug.Log("==============================================================");

    Debug.Log("Application.platform: " + Application.platform);
    Debug.Log("Environment.OSVersion: " + Environment.OSVersion);

    var reply = Greet("Unity");
    Debug.Log("Greeting: " + reply.Message);

    Debug.Log("==============================================================");
    Debug.Log("Tests finished successfully.");
    Debug.Log("==============================================================");
  }

  public static HelloReply Greet(string greeting)
  {
    const int Port = 50051;

    //MEMO: gRPC クライアントとして振る舞わせるためコメントアウト
    //Server server = new Server
    //{
    //  Services = { Greeter.BindService(new GreeterImpl()) },
    //  Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) }
    //};
    //server.Start();

    // 1. 127.0.0.1:50051 への gRPC チャネルを確立する
    // (Go 側の gRPC サーバへリクエストを行うため) 
    Channel channel = new Channel("127.0.0.1:50051", ChannelCredentials.Insecure);

    // 2. helloworld.proto 内の Greeter 内で定義したメソッドを関連付ける
    var client = new Greeter.GreeterClient(channel);

    // 3. gRPC で SayHello メソッドを実行する。
    // メソッド変数には helloworld.proto で定義した HelloRequest を使用する
    // SayHello メソッドは引数で指定した文字列の接頭字に Hello を付けて
    // そのまま文字列として返却する
    var reply = client.SayHello(new HelloRequest { Name = greeting });

    // 4. ｇRPCチャネルを閉じる
    channel.ShutdownAsync().Wait();

    //server.ShutdownAsync().Wait();

    return reply;
  }

  class GreeterImpl : Greeter.GreeterBase
  {
    // Server side handler of the SayHello RPC
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
      return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
    }
  }
}

```

`Scripts/HelloWorldTest.cs` 内の Greet メソッドは `Scripts/HelloWorldScript.cs` から使用されています。

```csharp:Scripts/HelloWorldScript.cs
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using UnityEngine;
using UnityEngine.UI;

public class HelloWorldScript : MonoBehaviour {
  int counter = 1;

  // Use this for initialization
  void Start () {
    HelloWorldTest.RunHelloWorld();
    }

  // Update is called once per frame
  void Update() {}

  // Ran when button is clicked
  // 1. Run したときに画面中央に表示されるボタンがクリックされる度に呼び出される
  public void RunHelloWorld(Text text)
  {
    // 2. HelloWorldTest 内の Greet メソッドを呼び出し gRPC サーバに
    // SayHello メソッドの実行を要求し、レスポンス内容を受け取る
    var reply = HelloWorldTest.Greet("Unity " + counter);
    // 3. 受け取ったレスポンス内容の Message をボタンの表記に設定する
    text.text = "Greeting: " + reply.Message;
    // 4. ボタンをクリックする度に counter を増やし SayHello メソッドの変数の内容を変更する
    counter++;
  }
}
```

この状態で Unity プロジェクトを Editor 上で Run すると画面上に `Hello gRPC!!!` ボタンが表示されるのでクリックしてみます。そうすると Go で動かしている gRPC サーバ側の標準出力に Unity 側から送信されてきた文字列が表示されます。

```shell
go run greeter_server/main.go
2019/05/29 01:49:51 Received: Unity 1
2019/05/29 01:49:51 Received: Unity 2
```

また Unity 側の画面では、ボタンを 2回クリックしていれば、ボタンの文字表記が `Greeting: Hello Unity 2` になっているはずです。
<img width="1792" alt="スクリーンショット 2019-05-29 1.52.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/88f496ce-4fa8-f63d-8e2e-8c32c53fd72c.png">

これでサーバ及びクライアント側の通信は確認出来ました。
次回は gRPC サーバ経由で Unity 同士相互に通信を行う仕組みを作ってみる予定です。

# 参考リンク
https://budougumi0617.github.io/2018/01/01/hello-grpc-go/
https://blog.fenrir-inc.com/jp/2016/10/grpc-go.html
https://qiita.com/shiena/items/6c3b34a8d8f1bb938470
https://qiita.com/muroon/items/2115c2c72be8b0c3f5f6