概要
前回の記事では、gRPCのパイプ通信のメリットなどを扱いました。この記事では、その具体的な実装方法やポイントなどを紹介しようと思います。
gRPC(TCP)でのプロセス間通信を行うコードがすでにある前提で、これをパイプ通信に変更するところが対象です。
とにかくコード全体を見たい人へ
この記事で説明しているコードは、次の場所のソリューションの中の、ポイントとなる部分です。全体を見たい人はそちらをどうぞ。
https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/tree/master/gRPCWinServiceSample
具体的な実装方法やポイントの紹介
クライアント側
サーバーへ接続する部分は、TCPならば1行で書けますが、パイプ通信の場合は少し複雑になります。
TCPの場合はこのように接続できます。
var channel = GrpcChannel.ForAddress("http://localhost:50100/Connect1");
パイプ通信の場合は、次のように独自のConnectCallbackを作成する必要があります。
var connectionFactory = new NamedPipesConnectionFactory("gRPCWinServiceSamplePipeName");
var socketsHttpHandler = new SocketsHttpHandler
{
ConnectCallback = connectionFactory.ConnectAsync
};
m_Channel = GrpcChannel.ForAddress("http://localhost/Connect1", new GrpcChannelOptions
{
HttpHandler = socketsHttpHandler
});
ConnectCallbackを実装している「NamedPipesConnectionFactory」は組み込みのクラスではなく、独自に実装する必要があります。次のようになります。ここで、パイプの細かい設定を変更することができます。
public class NamedPipesConnectionFactory(string m_PipeName)
{
public async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext _,
CancellationToken cancellationToken = default)
{
var clientStream = new NamedPipeClientStream(
serverName: ".",
pipeName: m_PipeName,
direction: PipeDirection.InOut,
options: PipeOptions.WriteThrough | PipeOptions.Asynchronous,
impersonationLevel: TokenImpersonationLevel.Anonymous);
try
{
await clientStream.ConnectAsync(cancellationToken).ConfigureAwait(false);
return clientStream;
}
catch
{
await clientStream.DisposeAsync();
throw;
}
}
}
実装する量は少々多いですが、つまりはGrpcChannel.ForAddressで接続をしている部分のコードだけ置き換えれば、TCP通信をパイプ通信に変えることが出来ます。
GitHubのコードで言うと、このあたりとこのクラスになります。
クライアント側についてはおおむね、次のMS Learnの記載通りであり、特に注意点はありません。
gRPC と名前付きパイプを使ったプロセス間通信 | Microsoft Learn
サーバー側
サーバー側は、GenericHostを使っていることを前提に説明します。GenericHostについてはこの記事では触れませんが、DI・ロガー・各種サービスホストなどをまとめて取り扱える.NETの仕組みで、Windowsサービスもこれを使って作っていくのがお勧めです。
その前提だと非常に簡単で、TCPだと次のようにするところを
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.Listen(IPAddress.Loopback, 50100, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
});
});
次のようにListenNamedPipeへ置き換えるだけです。
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.ListenNamedPipe("gRPCWinServiceSamplePipeName", listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
});
});
置き換えずに両方を記載すると、TCPとパイプの両方を待ち受けることができます。サーバー側は、かなり簡単に実装可能だと思います。
GitHubのコードで言うと、このあたりになります。
ただし、サーバー側には注意点があります。
注意点:パイプのACL
Windowsサービス(LocalSystem権限)からパイプを作ると、デフォルトではユーザー権限の書き込み不可になります。
それはそれで良いのですが、もしユーザーセッションからユーザー権限で操作したい場合は、権限を追加する必要があります。gRPC側からそれを設定できるインターフェースが有るといいのですが、見つかりませんでした。
Win32 APIを使って、パイプを開いてACLを設定すれば、実現できます。C#でも書けると思いますが、C++/CLIで書きました。記事の中に貼るにはWin32 APIを使ったコードは長すぎるので、ポイントを説明した後にGitHubのリンクを貼ります。
- パイプを開く
- 設定したいユーザーグループのSIDを文字列で指定
- パイプのセキュリティ設定を取得して、SIDに対する許可を設定
以上のような処理をします。最初の「パイプを開く」処理は、GenericHost側でパイプが作成されるまで成功しません。別スレッドで成功するまでリトライするなどの工夫が必要になります。
GitHubのコードは、このあたりになります。
ここまでが、TCPのgRPC通信コードを、パイプ通信に変更する方法です。
.NET8より前の既存プロジェクトからの移行は?
その前に、.NET 8へ移行しなければ、gRPCのパイプ通信は使用できません。幸い、既存プロジェクトを.NET 8に移行するのは、割と簡単です。そこも少し説明します。
WPF・クラスライブラリなど(C#)
プロジェクトの「ターゲットフレームワーク」で「.NET 8.0」を選択するだけで、たいていは動きます。もちろんファイルの<TargetFramework>
を直接書き換えてもOKです。GitHubのコードではこの部分です。
C++/CLI
プロジェクトの「.NETターゲットフレームワーク」で「.NET 8.0」を選択するだけ、と書きたいところですが・・・選択肢にありません。
GUIではコンボボックスになっていますが手入力可能なので、「net8.0」と書けばOKです。プロジェクトファイルの直接書き換えも可能で、GitHubのコードではこの部分です。
まとめ
記事にしてみるとそこそこ長い気がしますが、ソースコードの視点ではさほど大きな変更をすることなく、gRPCをTCPからパイプ通信に変更できます。前回の記事でも移行価値ありという結論なので、興味が湧いたら触ってみると良いと思います。