【Unity】gRPC + Protocol BuffersをUnityで実装する

# 環境
Mac Mojave 10.14.6
Unity 2019.3.9f1
Rider 2019.3.4

# gRPC is 何？
そもそもgRPCとは「初めまして」の状態だったので、[こちら](https://knowledge.sakura.ad.jp/24059/)の記事で概要を把握。

# 環境構築

### gRPCのインストール

[公式ページ](https://packages.grpc.io/)からgRPC.ToolsをUnityのプロジェクトに追加する方法もあるようだが、homebrew経由の方が良さそう。

```
$ brew tap grpc/grpc
$ brew install grpc
```

### protobufのインストール
こちらもhomebrewからインストール。

```
$ brew install protobuf
```

## プロジェクトにgRPCパッケージを追加
Unityプロジェクトを作成する。
[こちら](https://packages.grpc.io/)のDaily Builds of master BranchのBuild IDの最新をクリック。
<img width="1226" alt="スクリーンショット 2020-06-07 11.29.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85509/53cc2a71-086a-e359-4446-93c3ac6bff08.png">

C#の一番上に`grpc_unity_package.v.v.v-dev.zip`があるのでダウンロードする。
<img width="1176" alt="スクリーンショット 2020-06-07 11.29.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85509/44628583-dd08-a78b-354e-8f6e1a57d663.png">

zipを解凍すると`Plugins`フォルダが生成されるので、これをプロジェクトの`Assets`配下に追加する。

## protobufのバージョンを下げる

最新(2020/06/05時点)のgRPCパッケージを追加すると、警告+エラーが発生する。
<img width="1261" alt="スクリーンショット 2020-06-07 11.36.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85509/e2d3998b-6b1d-6bf5-8fe2-7640559381e5.png">

[こちらの記事](https://note.com/npaka/n/n050b1aba7772)を参考にProtobufのバージョンを[3.9.1](https://www.nuget.org/packages/Google.Protobuf/3.9.1)にする。Download Packageをクリックすると
`google.protobuf.3.9.1.nupkg`がダウンロードされるので、`.nupkg`を`.zip`に変更して解凍する。`Assets/Plugins/Google.Protobuflib/net45/`を解凍後の`lib/net45/`に置き換える。

# .protoの作成

[公式ページ](https://developers.google.com/protocol-buffers/docs/proto3)を参考にしてこんな感じの.protoを作成。
gRPCにより`HelloService`のサーバー用、クライアント用メソッドがそれぞれ書き出される。

```hello.proto
syntax = "proto3";

message Request{
    string name = 1;
}

message Response{
    string hello = 1;
}

// Requestを引数に渡してSayHelloを呼び出すと、Responseが返ってくる
service HelloService {
    rpc SayHello(Request) returns (Response) {}
}
```

## .protoをコンパイル

以下のコマンドを実行すると、指定したディレクトリに`Hello.cs`と`HelloGrpc.cs`が生成される。

```
$ protoc -I .protoファイルがあるディレクトリ --csharp_out=Hello.csの出力先 --grpc_out=HelloGrpc.csの出力先 --plugin=protoc-gen-grpc=/usr/local/bin/grpc_csharp_plugin .protoファイル名
```

# クライアントの実装

クライアント用の`SayHello`メソッドが用意されている。これにリクエストを渡すことで、サーバーからのレスポンスを受け取れるようになっている。
ちなみに非同期メソッド`SayHelloAsync`も用意されている。

```TestClient.cs
using Grpc.Core;
using UnityEngine;

public class TestClient : MonoBehaviour
{
    void Start()
    {
        var channel = new Channel("localhost:50051", ChannelCredentials.Insecure);
        var client = new HelloService.HelloServiceClient(channel);
        
        // リクエストを送信してレスポンスをもらう
        var response = client.SayHello(new Request() { Name = "TOM"});
        Debug.Log(response.Hello);
    }
}
```

詳しい事は[公式](https://www.grpc.io/docs/languages/csharp/basics/#client)を参照。

# サーバーの実装

### HelloServiceBaseを継承したクラス
コマンドで生成された`HelloGrpc.cs`にある`HelloServiceBase`クラスを継承したクラスを作成する。この`HelloServiceBase`クラスにはvirtualな`SayHello`メソッドが用意されているのでオーバーライドして、サーバーとしての処理を実装する。


```TestServer.cs
using System.Threading.Tasks;
using UnityEngine;
using Grpc.Core;

public class TestServer : MonoBehaviour
{
    private void Start()
    {
        var server = new Server()
        {
            Services = { HelloService.BindService(new TestServerService())},
            Ports = { new ServerPort("localhost", 50051, ServerCredentials.Insecure) }
        };
        server.Start();
    }
}
// HelloServiceBaseを継承したクラス
public class TestServerService : HelloService.HelloServiceBase
{
    public override Task<Response> SayHello(Request request, ServerCallContext context)
    {
        // リクエストを受けて値を返却する
        var hello = $"Hello {request.Name}";
        var response = new Response{Hello = hello};
        return Task.FromResult(response);
    }
}
```

詳しくは[公式](https://www.grpc.io/docs/languages/csharp/basics/#server)を参照。

# 試してみる

### サーバーサイドのアプリをビルド

`TestServer.cs`を適当なゲームオブジェクトにアタッチしたシーンをMacのアプリとしてビルドする。ビルド出来たら起動しておく。
 
### クライアントの実行

`TestClient.cs`を適当なゲームオブジェクトのアタッチしてUnityEditorを再生する。

# 結果

クライアントから送った値がサーバーで加工されて返ってくることが確認できた。

<img width="361" alt="スクリーンショット 2020-06-07 22.53.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85509/0a6fde9e-6ac8-1a3c-b859-3d602949d025.png">

# ハマった点

`brew install protobuf`のタイミングによっては、コンパイルされたコードで`GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE`という定義が無いためにエラーが発生した。
homebrewでインストールするprotobuf、サイトからダウンロードするgrpc_unity_package.v.v.v-dev.zipのバージョンにズレが生じたっぽい。
今回は下記を参考にして古いバージョンのprotobufをインストールすることで解決した。
[Homebrewで過去バージョンのパッケージをインストールする方法](https://www.meganii.com/blog/2019/12/03/how-to-install-an-older-version-of-a-homebrew-package/)


# 参考
https://note.com/npaka/n/n050b1aba7772
https://qiita.com/muroon/items/4e12dde47b9e8b1e94d3
https://qiita.com/nikaera/items/fdcad59bd9a0ebf6a792
https://github.com/grpc/grpc/tree/master/examples/csharp/HelloworldUnity#build