Open13

UnityでgRPCを使いたい

mochinekomochineko

目標

  • UnityをgRPCのClient (or Server)として利用したい
    • RESTやWebSocketで実装するのに適さないWebAPIがあり、gRPCを使えるようにしたい
  • 新規プロジェクトでもUPMでぽちっと使えるようにしたい
    • 都度環境構築をするのはしんどい
mochinekomochineko

C#におけるgRPCの実装は大きく2つある模様。

1. Grpc.Core

https://github.com/grpc/grpc

  • かなり前からある.Net向けの実装を含むgRPC実装
  • Unity×gRPCで一番有名なMagicOnionが内部で使用しているのはこちら
  • ただし、2023/5でC#は非推奨になってサポートが切れていて、新規プロジェクトでの使用は推奨されていない
  • おそらくHttp/2の実装も内部に含まれているので、.NETのバージョンが古いUnityでも動作するという原理のはず

2. grpc-dotnet

https://github.com/grpc/grpc-dotnet

  • .NET(C#)向けの新しいgRPC実装
  • .NETでHttp/2が(.NET Core 3?以上?)標準で実装されたので、それを利用したよりシンプルな実装?
  • Unityの対応している.NETのバージョンではHttp/2を標準でサポートしていないので、これを組み込んでもHttp/1.1で動く or 動かないらしい
mochinekomochineko

今日現在で取りうる選択肢は2つありそう。

1. Grpc.Coreを頑張って利用する

  • 前述のように公式のサポートが切れているので、将来性は低い
  • ただMagicOnionでの実績があるので動かす分には問題ないはず
  • それでも実例や情報が少ないので、ライブラリを自作することにはなるかも

2. grpc-dotnetを頑張って利用する

  • grpc-dotnetはそのままでは使い物にならない
  • BestHTTP/2という有料アセットを利用してgrpc-dotnetをUnityで動かすライブラリを発見した
  • 有料アセットが必要というハードルはあるが、将来性はありそう
  • もし万が一Unityが.NET 6/7などの対応をしたらすんなり移行できるかもしれない
  • package.jsonが置かれてないので、Issueを立てるなりforkして改造するなりして使ったほうが良さそう
mochinekomochineko

その他メモ

  • .protoファイルからのソースコード自動生成~Clientの実装までのワークフローをちゃんと確認したい
  • 動作確認は作成済みのRust/tonicサーバーで良さそう。
  • Apple Silicon Macでも動くかどうかも調べておく
mochinekomochineko

grpc-dotnetを使用する方針の調査

https://github.com/doctorseus/grpc-dotnet-unity/tree/master

  • grpc-dotnetはManaged Library(= C#で書かれているライブラリ)のようなので、動作プラットフォームの心配は少なさそう
  • 原理としては、grpc-dotnetのHttpHandlerのオプションにBestHTTP/2を使って実装したHandlerを指定すれば動く、というものらしい
  • 現在はClientのみサポートしていて、Serverはサポートしていない
  • package.jsonは置かれていないが、Pluginsフォルダ、Scriptsフォルダの2つにpackage.jsonを配置して動かせるようにすればいけるはず
  • protoファイルからのソースコード生成はサポートしていなさそうなので、自分でコマンドを叩くか、Editor拡張を追加で用意する形になりそう
    • protocは別でインストールが必要

思っていたよりすんなりいけそうな感触なので、BestHTTP/2を購入して、grpc-dotnet-unityを改造しながら触ってみたい

mochinekomochineko

protocの導入 Grpc.Toolsの導入

macOSだとHomebrewでさくっとインストールできるらしいが、今はWindowsを使っているので手動でインストールをする必要がありそう。

https://github.com/protocolbuffers/protobuf/releases

リリースの現時点での最新版 v23.4 の protoc-23.4-win64.zip をダウンロードしてzipを展開、bin/protoc.exe が実行ファイルのよう。

これを適当なところに配置して、フォルダのPathを通して、PCを再起動する。

gRPCではprotocだけではだめで、Grpc.Tools(正確にはその内部に含まれているgRPC用のC#向け拡張Plugin)が必要のよう。

NuGetのパッケージをダウンロードし、拡張子を無理やり.zipに変えて展開。

https://www.nuget.org/packages/Grpc.Tools/

展開した tools フォルダに

  • protoc.exe
  • grpc_csharp_plugin.exe

の2つが入っているので、今度こそこれを適当なところに配置して、フォルダのPathを通して、PCを再起動する。

mochinekomochineko

C#のソースコードの自動生成

$ protoc --csharp_out {protoc_output} --grpc_out {protoc_output} --plugin=protoc-gen-grpc={grpc_csharp_plugin} {Protos/greet.proto}

パラメータ

  • {protoc_output} : 生成したソースコードの出力先
  • {grpc_csharp_plugin} : grpc_csharp_plugin.exeのPath
  • {Protos/greet.proto} : 生成元の.protoファイルのPath

例:

  • 出力先Path = generated
  • Plugin Path = %Proto%/grpc_csharp_plugin.exe
    • 環境変数「Proto」に通したPathと同じPathを入れている想定
  • 生成元Path = chat.proto

の場合のコマンドは下記。

$ protoc --csharp_out generated --grpc_out generated --plugin=protoc-gen-grpc=%Proto%/grpc_csharp_plugin.exe chat.proto

実行してエラーが出ずに終了したのを確認したのち、指定した出力先に.csが2つ生成されていることが確認できる。

例の場合だと下記。

  • Chat.cs
  • ChatGrpc.cs

通常のProtocol Buffersと、gRPCの分が2種類生成されているイメージ?

毎回自分でコマンドを叩くのは大変なので、後で簡易のEditor拡張を作っておきたい。

mochinekomochineko

サーバー側のTLS設定

FAQより

Are connections with ChannelCredentials.Insecure supported?
No, BestHTTP does not support plaintext http2 aka "h2c with prior knowledge" which is assumed when gRPC is used with insecure credentials. You will have to setup SSL on your gRPC server.

BestHTTP/2の都合でローカルで検証するだけでも http:// ではなく https:// を使う必要があるため、サーバー側でTLSの対応をしないとダメ。

テストするだけならOpenSSLで生成した自己証明書で十分。

サーバー側でのTLSの設定方法は言語やフレークワークによって異なるのでここでは言及しない。

mochinekomochineko

Client側での実装

Client側の実装は下記の流れ。

  1. protocで生成したC#のソースコードをUnityに取り込む
  2. GrpcChannelを取得する(その際、OptionsでGRPCBestHttpHandlerを指定する)
  3. Clientインスタンスを取得する
  4. RPCを呼び出す

下記、実装例。

#nullable enable
using System;
using System.Threading.Tasks;
using GRPC.NET;
using UnityEngine;
using Grpc.Net.Client;

namespace Chat.Client
{
    internal sealed class ChatClient : MonoBehaviour
    {
        private GRPCBestHttpHandler handler = new();
        private GrpcChannel? channel;

        private void Start()
        {
            channel = GrpcChannel.ForAddress("https://127.0.0.1:8000", new GrpcChannelOptions()
            {
                HttpHandler = handler
            });
        }

        private void OnDestroy()
        {
            handler.Dispose();
            channel?.Dispose();
        }

        [ContextMenu(nameof(CompleteChat))]
        public void CompleteChat()
        {
            CompleteChatAsync();
        }

        private async Task CompleteChatAsync()
        {
            try
            {
                var client = new Chat.ChatClient(channel);
                var response = await client.CompleteChatAsync(new ChatRequest
                    {
                        Message = "あなたのことを教えてください。"
                    });

                Debug.Log(response.Response);
            }
            catch (Exception e)
            {
                Debug.LogError(e);
            }
        }
    }
}

ただし、

 var client = new Chat.ChatClient(channel);
 var response = await client.CompleteChatAsync(new ChatRequest
 {
      Message = "あなたのことを教えてください。"
 });

の部分は具体的なRPCの実装に依存する部分、上記はUnaryの例。

mochinekomochineko

基本的には以上のセットアップをすれば動く。(動いた)

サーバー側は何を使ってもよいが、gRPCurlなどで単体でも動作確認をしておくとよい。

自分はRustのtonicというcrateを使って、裏側でChatGPTのAPIを叩く実装を試しに作っていたのでそれをそのまま使用してテストした。

mochinekomochineko

クライアント側の実装の細かいメモ。

  • GrpcChannelOptions にリトライ回数の設定があるので、リトライはライブラリ内部でやってくれる模様
  • GrpcChannelOptions にLoggerの設定があるので、Microsoft.Extensions.Logging のLoggerを使うことができる模様
  • 認証情報などは Metadataに入れて、RPCを叩くときに一緒に渡す
  • CancellationToken も対応している
  • Timeoutに相当する設定は、おそらく deadline のオプション
  • 実行時エラーはtry-catchしてハンドリングする想定っぽい ( Grpc.Core.RpcException )
    • Grpc.Core.RpcExceptionは内部にStatus、StatusCodeを持っているので、エラーの判別などはできそう