UnityでgRPCを使いたい
自分でざっと調べて見た感じ辛そうだけど、Twitterで検索しても悲鳴ばかり聞こえる...
目標
- UnityをgRPCのClient (or Server)として利用したい
- RESTやWebSocketで実装するのに適さないWebAPIがあり、gRPCを使えるようにしたい
- 新規プロジェクトでもUPMでぽちっと使えるようにしたい
- 都度環境構築をするのはしんどい
C#におけるgRPCの実装は大きく2つある模様。
1. Grpc.Core
- かなり前からある.Net向けの実装を含むgRPC実装
- Unity×gRPCで一番有名なMagicOnionが内部で使用しているのはこちら
- ただし、2023/5でC#は非推奨になってサポートが切れていて、新規プロジェクトでの使用は推奨されていない
- おそらくHttp/2の実装も内部に含まれているので、.NETのバージョンが古いUnityでも動作するという原理のはず
2. grpc-dotnet
- .NET(C#)向けの新しいgRPC実装
- .NETでHttp/2が(.NET Core 3?以上?)標準で実装されたので、それを利用したよりシンプルな実装?
- Unityの対応している.NETのバージョンではHttp/2を標準でサポートしていないので、これを組み込んでもHttp/1.1で動く or 動かないらしい
今日現在で取りうる選択肢は2つありそう。
1. Grpc.Coreを頑張って利用する
- 前述のように公式のサポートが切れているので、将来性は低い
- ただMagicOnionでの実績があるので動かす分には問題ないはず
- それでも実例や情報が少ないので、ライブラリを自作することにはなるかも
2. grpc-dotnetを頑張って利用する
- grpc-dotnetはそのままでは使い物にならない
- BestHTTP/2という有料アセットを利用してgrpc-dotnetをUnityで動かすライブラリを発見した
- 有料アセットが必要というハードルはあるが、将来性はありそう
- もし万が一Unityが.NET 6/7などの対応をしたらすんなり移行できるかもしれない
- package.jsonが置かれてないので、Issueを立てるなりforkして改造するなりして使ったほうが良さそう
その他メモ
- .protoファイルからのソースコード自動生成~Clientの実装までのワークフローをちゃんと確認したい
- 動作確認は作成済みのRust/tonicサーバーで良さそう。
- Apple Silicon Macでも動くかどうかも調べておく
grpc-dotnetを使用する方針の調査
- 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を改造しながら触ってみたい
grpc-dotnet-unityの改造
forked Repository
変更点
-
package.json
の配置 - Assembly Definitionの配置
- DLLの
Auto Referenced = false
に変更- DLLの競合を防ぐため
これをUPMで参照して、AssetStoreで購入したBestHTTP/2をインポートした状態でコンパイルエラーが出ないところまで確認
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に変えて展開。
展開した tools
フォルダに
protoc.exe
grpc_csharp_plugin.exe
の2つが入っているので、今度こそこれを適当なところに配置して、フォルダのPathを通して、PCを再起動する。
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拡張を作っておきたい。
サーバー側の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の設定方法は言語やフレークワークによって異なるのでここでは言及しない。
Client側での実装
Client側の実装は下記の流れ。
- protocで生成したC#のソースコードをUnityに取り込む
- GrpcChannelを取得する(その際、Optionsで
GRPCBestHttpHandler
を指定する) - Clientインスタンスを取得する
- 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の例。
基本的には以上のセットアップをすれば動く。(動いた)
サーバー側は何を使ってもよいが、gRPCurlなどで単体でも動作確認をしておくとよい。
自分はRustのtonicというcrateを使って、裏側でChatGPTのAPIを叩く実装を試しに作っていたのでそれをそのまま使用してテストした。
クライアント側の実装の細かいメモ。
-
GrpcChannelOptions
にリトライ回数の設定があるので、リトライはライブラリ内部でやってくれる模様 -
GrpcChannelOptions
にLoggerの設定があるので、Microsoft.Extensions.Logging
のLoggerを使うことができる模様 - 認証情報などは
Metadata
に入れて、RPCを叩くときに一緒に渡す -
CancellationToken
も対応している - Timeoutに相当する設定は、おそらく
deadline
のオプション - 実行時エラーはtry-catchしてハンドリングする想定っぽい (
Grpc.Core.RpcException
)-
Grpc.Core.RpcException
は内部にStatus、StatusCodeを持っているので、エラーの判別などはできそう
-