proto ファイルを使用せずに実装する手順
gRPC アプリケーションの開発で proto ファイルを使用したくない/使用する必要がないケースもあります。
-
ProtocolBuffers
以外のデータフォーマットを使用したい- 「gRPC は任意のデータフォーマットに差し替えることができる」とうたわれていますが、Grpc.Tools で生成される C# のソースコードでは多くの要素が readonly で定義されており、実際には容易に差し替えることはできません。
- リクエスト/レスポンスの型は
ProtocolBuffers
で扱えるようにするための実装が組み込まれており、ProtocolBuffers
を使用しない場合にオーバーヘッドになったり、制限事項が発生したりします。
-
プラットフォーム間の相互運用性を考慮しない
- サーバーアプリケーションとクライアントアプリケーションを同一プラットフォームで開発し、将来的にも異なるプラットフォームとの相互運用性を考慮しなくてもよい場合、proto ファイルに頼らずプロジェクトに最適化された手法をとることができます。
MagicOnion
はその例です。
- サーバーアプリケーションとクライアントアプリケーションを同一プラットフォームで開発し、将来的にも異なるプラットフォームとの相互運用性を考慮しなくてもよい場合、proto ファイルに頼らずプロジェクトに最適化された手法をとることができます。
このドキュメントでは実装手順を説明します。★がついている章が proto ファイルを使用しないときに必要になる部分です。なお、Grpc.Tools で生成される C# のソースコードについては次のエントリで説明しています。
【Qiita】Grpc.Tools で生成される C# ソースコードの解説
【Qiita】C# Protocol Buffers メッセージモデルクラスの難点
サーバーサイドの実装
リクエスト/レスポンス★
RPCメソッドのリクエスト/レスポンスとして使用する型を定義します。サーバーアプリケーションとクライアントアプリケーションを同一プラットフォームで開発する場合、実装を共有できるようにするとよいです。
この例では ProtocolBuffers
ではなく MessagePack
を使用しています。サーバーアプリケーションとクライアントアプリケーションのプラットフォームが異なる場合、相互運用性があるデータフォーマットを使用する必要があります。
【GitHub】neuecc/MessagePack-CSharp
[MessagePack.MessagePackObject]
public sealed class TestRequest
{
[MessagePack.Key(0)]
public string Value { get; set; }
}
[MessagePack.MessagePackObject]
public sealed class TestResponse
{
[MessagePack.Key(0)]
public string Value { get; set; }
}
サービスクラス★
サービスクラスを実装します。
- 特定の基底クラスを継承する必要はありません。
- メソッドは gRPC の
Unary
ClientStreaming
ServerStreaming
DuplexStreaming
何れかのシグネチャに従う必要があります。
internal sealed class TestService
{
internal Task<TestResponse> GetResponse(TestRequest request, ServerCallContext context)
{
// リクエストされた値をおうむ返ししているだけです
return Task.FromResult(new TestResponse { Value = request.Value });
}
}
Marshaller<T>★
リクエスト/レスポンスオブジェクトのシリアライズとデシリアライズを行う Marshaller<T>
を生成します。リクエスト/レスポンスの型の数だけ生成することになりますので、通常は汎用ジェネリックメソッドとして実装します。
Marshaller は、この後の Method<TRequest, TResponse>
を生成するときに必要になります。
private Marshaller<T> CreateMarshaller<T>()
{
return new Marshaller<T>(
// バイト配列へのシリアライズ
obj => MessagePack.MessagePackSerializer.Serialize<T>(obj)
// バイト配列からのデシリアライズ
, bytes => MessagePack.MessagePackSerializer.Deserialize<T>(bytes)
);
}
Method<TRequest, TResponse>★
RPCメソッドのシグネチャにあたる Method<TRequest, TResponse>
を生成します。サーバーアプリケーションとクライアントアプリケーションを同一プラットフォームで開発する場合、実装を共有できるようにするとよいです。
- サービス名とメソッド名がマッピングのキーになります。必ずしも実装クラス名/メソッド名と一致させる必要はありませんが、一致するように実装したほうが分かりやすいです。マッピングはこの後の
ServerServiceDefinition
の定義で行います。 -
Marshaller<T>
は同じ型のリクエスト/レスポンスを使用する複数のメソッドで共有可能です。 - リフレクションを使ってサービスクラスの実装から生成できるようにすると生産性が高まります。
Marshaller<TestRequest> requestMarshaller = CreateMarshaller<TestRequest>();
Marshaller<TestResponse> responseMarshaller = CreateMarshaller<TestResponse>();
Method<TestRequest, TestResponse> GetResponse = new Method<TestRequest, TestResponse>(
// RPCメソッドの種類
MethodType.Unary
// サービス名
, "TestService"
// メソッド名
, "GetResponse"
// リクエスト/レスポンスのマーシャラー
, requestMarshaller
, responseMarshaller
);
ServerCredentials
資格証明を生成します。
特に何も使用しない場合は Grpc.Core に定義されている ServerCredentials.Insecure を使用します。
SslServerCredentials については、【Qiita】C# gRPC v1.16.0 SSLに関する仕様変更 を参照してください。
private ServerCredentials CreateServerCredentials()
{
string cacert = File.ReadAllText("testServer.crt");
string servercert = File.ReadAllText("testServer.crt");
string serverkey = File.ReadAllText("testServer.key");
KeyCertificatePair keypair = new KeyCertificatePair(servercert, serverkey);
SslServerCredentials credentials = new SslServerCredentials(
new List<KeyCertificatePair> { keypair }
, cacert
, SslClientCertificateRequestType.RequestAndVerify
);
return credentials;
}
ServerPort
RPC通信に使用するポートの定義を生成します。
private ServerPort CreateServerPort(string server, int port)
{
ServerCredentials credentials = CreateServerCredentials();
return new ServerPort(server, port, credentials)
}
ServerServiceDefinition★
RPCサービスメソッドの定義を生成します。
ServerServiceDefinition.Builder の AddMethod メソッドは、戻り値としてメソッド追加後の ServerServiceDefinition.Builder を返します。複数のメソッドを追加する場合、fluent スタイルで記述するのが一般的です。
private ServerServiceDefinition CreateServerServiceDefinition()
{
// サービスメソッドへの参照を必要とするため、サービスクラスのインスタンスを生成
TestService service = new TestService()
// RPCメソッドを追加
// TestRpcMethods はRPCメソッドの定義(Method<TRequest, TResponse>)をまとめたクラスです
ServerServiceDefinition.Builder builder = ServerServiceDefinition.CreateBuilder();
builder = builder
.AddMethod(TestRpcMethods.GetResponse, testService.GetResponse)
.AddMethod(TestRpcMethods.SendRequests, testService.SendRequests)
;
// ビルド
ServerServiceDefinition definition = builder.Build();
// インターセプターを設定するならここで設定する
definition = definition
.Intercept(CreateInterceptorA())
.Intercept(CreateInterceptorB())
;
return definition;
}
Server
サーバーオブジェクトを生成し、ServerPort
と ServerServiceDefinition
を登録します。
Server server = new Server();
server.Ports.Add(CreateServerPort("localhost", 55001, CreateServerCredentials()));
server.Services.Add(CreateServerServiceDefinition());
// 開始
server.Start();
クライアントサイドの実装
リクエスト/レスポンス★
サーバーサイドの実装の説明を参照してください。
サーバーアプリケーションとクライアントアプリケーションを同一プラットフォームで開発する場合、実装を共有できるようにするとよいです。
Marshaller<T>★
サーバーサイドの実装の説明を参照してください。
サーバーアプリケーションとクライアントアプリケーションを同一プラットフォームで開発する場合、実装を共有できるようにするとよいです。
Method<TRequest, TResponse>★
サーバーサイドの実装の説明を参照してください。
サーバーアプリケーションとクライアントアプリケーションを同一プラットフォームで開発する場合、実装を共有できるようにするとよいです。
ChannelCredentials
資格証明を生成します。
特に何も使用しない場合は Grpc.Core に定義されている ChannelCredentials.Insecure を使用します。
SslCredentials については、【Qiita】C# gRPC v1.16.0 SSLに関する仕様変更 を参照してください。
private ChannelCredentials CreateChannelCredentials()
{
string cacert = File.ReadAllText("testServer.crt");
string clientcert = File.ReadAllText("testClient.crt");
string clientkey = File.ReadAllText("testClient.key");
SslCredentials sslCredential = new SslCredentials(
cacert, new Grpc.Core.KeyCertificatePair(clientcert, clientkey)
);
return credentials;
}
Channel
チャネルを生成します。
private Channel CreateChannel(string server, int port)
{
return new Channel(server, port, CreateChannelCredentials());
}
CallInvoker
呼び出しオブジェクトを生成します。
private CallInvoker CreateCallInvoker(string server, int port)
{
CallInvoder invoker = new DefaultCallInvoker(CreateChannel(server, port));
// インターセプターを設定するならここで設定する
invoker = invoker
.Intercept(CreateInterceptorA())
.Intercept(CreateInterceptorB())
;
return invoker;
}
クライアントクラス★
RPCメソッドを呼び出すには、RPCメソッドの定義を引数で CallInvoker に渡す必要があります。アプリケーションコードからRPCメソッドを呼び出しやすくするため、ショートカットメソッドを持つクライアントクラスを実装します。
- 特定の基底クラスを継承する必要はありません。
- CallInvoker には 5 つの呼び出しメソッド
BlockingUnaryCall
AsyncUnaryCall
AsyncClientStreamingCall
AsyncServerStreamingCall
AsyncDuplexStreamingCall
があり、RPCメソッドの種類に応じてこれらを使い分けます。
internal sealed class TestClient
{
internal TestClient(CallInvoker invoker)
{
Invoker = invoker;
}
private CallInvoker Invoker { get; }
public TestResponse GetResponse(TestRequest request, CallOptions options)
{
// TestRpcMethods はRPCメソッドの定義(Method<TRequest, TResponse>)をまとめたクラスです
return Invoker.BlockingUnaryCall(TestRpcMethods.GetResponse, "", options, request);
}
public async Task<TestResponse> GetResponseAsync(TestRequest request, CallOptions options)
{
return await Invoker.AsyncUnaryCall(TestRpcMethods.GetResponse, "", options, request);
}
}
補足
gRPC サーバーやクライアントを XML ファイルの定義に従って構成する方法についても紹介しています。