.NET 8のプロセス間通信には、gRPCのパイプ通信が優秀なようです：その2 具体的なコードの書き方

# 概要

[前回の記事](https://qiita.com/spc_ksudoh/items/760a41a73c825eb70e70)では、gRPCのパイプ通信のメリットなどを扱いました。この記事では、その具体的な実装方法やポイントなどを紹介しようと思います。

gRPC(TCP)でのプロセス間通信を行うコードがすでにある前提で、これをパイプ通信に変更するところが対象です。

# とにかくコード全体を見たい人へ

この記事で説明しているコードは、次の場所のソリューションの中の、ポイントとなる部分です。全体を見たい人はそちらをどうぞ。
https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/tree/master/gRPCWinServiceSample

# 具体的な実装方法やポイントの紹介

## クライアント側

サーバーへ接続する部分は、TCPならば1行で書けますが、パイプ通信の場合は少し複雑になります。

TCPの場合はこのように接続できます。

```csharp
var channel = GrpcChannel.ForAddress("http://localhost:50100/Connect1");
```

パイプ通信の場合は、次のように独自のConnectCallbackを作成する必要があります。

```csharp
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」は組み込みのクラスではなく、独自に実装する必要があります。次のようになります。ここで、パイプの細かい設定を変更することができます。

```csharp
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のコードで言うと、[このあたり](https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/blob/a8587edcc9ee2ea41a33c39778adb1f74200252c/gRPCWinServiceSample/gRPCCoreLib/UserSessionToServiceGrpcClient.cs#L69-L85)と[このクラス](https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/blob/master/gRPCWinServiceSample/gRPCCoreLib/NamedPipesConnectionFactory.cs)になります。

クライアント側についてはおおむね、次のMS Learnの記載通りであり、特に注意点はありません。
[gRPC と名前付きパイプを使ったプロセス間通信 | Microsoft Learn](https://learn.microsoft.com/ja-jp/aspnet/core/grpc/interprocess-namedpipes?view=aspnetcore-8.0)

## サーバー側

サーバー側は、GenericHostを使っていることを前提に説明します。[GenericHost](https://learn.microsoft.com/ja-jp/dotnet/core/extensions/generic-host)についてはこの記事では触れませんが、DI・ロガー・各種サービスホストなどをまとめて取り扱える.NETの仕組みで、Windowsサービスもこれを使って作っていくのがお勧めです。

その前提だと非常に簡単で、TCPだと次のようにするところを

```csharp
.ConfigureWebHostDefaults(webBuilder =>
{
    webBuilder.ConfigureKestrel(options =>
    {
        options.Listen(IPAddress.Loopback, 50100, listenOptions =>
        {
            listenOptions.Protocols = HttpProtocols.Http2;
        });
    });

```

次のようにListenNamedPipeへ置き換えるだけです。

```csharp
.ConfigureWebHostDefaults(webBuilder =>
{
    webBuilder.ConfigureKestrel(options =>
    {
        options.ListenNamedPipe("gRPCWinServiceSamplePipeName", listenOptions =>
        {
            listenOptions.Protocols = HttpProtocols.Http2;
        });
    });

```

置き換えずに両方を記載すると、TCPとパイプの両方を待ち受けることができます。サーバー側は、かなり簡単に実装可能だと思います。

GitHubのコードで言うと、[このあたり](https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/blob/a8587edcc9ee2ea41a33c39778adb1f74200252c/gRPCWinServiceSample/gRPCWinServiceSample/Program.cs#L29-L44)になります。

ただし、サーバー側には注意点があります。

### 注意点：パイプのACL

Windowsサービス(LocalSystem権限)からパイプを作ると、デフォルトではユーザー権限の書き込み不可になります。

それはそれで良いのですが、もしユーザーセッションからユーザー権限で操作したい場合は、権限を追加する必要があります。gRPC側からそれを設定できるインターフェースが有るといいのですが、見つかりませんでした。

※2025/6/11追記: 良さそうな方法があったので、次の記事にまとめました。

https://qiita.com/spc_ksudoh/items/45c2efbb4556a2cdb198

ここまでが、TCPのgRPC通信コードを、パイプ通信に変更する方法です。

## .NET8より前の既存プロジェクトからの移行は？

その前に、.NET 8へ移行しなければ、gRPCのパイプ通信は使用できません。幸い、既存プロジェクトを.NET 8に移行するのは、割と簡単です。そこも少し説明します。

### WPF・クラスライブラリなど(C#) 

プロジェクトの「ターゲットフレームワーク」で「.NET 8.0」を選択するだけで、たいていは動きます。もちろんファイルの`<TargetFramework>`を直接書き換えてもOKです。GitHubのコードでは[この部分](https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/blob/a8587edcc9ee2ea41a33c39778adb1f74200252c/gRPCWinServiceSample/gRPCCoreClient/gRPCCoreClient.csproj#L5)です。

### C++/CLI

プロジェクトの「.NETターゲットフレームワーク」で「.NET 8.0」を選択するだけ、と書きたいところですが・・・選択肢にありません。

GUIではコンボボックスになっていますが手入力可能なので、「net8.0」と書けばOKです。プロジェクトファイルの直接書き換えも可能で、GitHubのコードでは[この部分](https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/blob/a8587edcc9ee2ea41a33c39778adb1f74200252c/gRPCWinServiceSample/Win32APILib/Win32APILib.vcxproj#L28)です。

# まとめ

記事にしてみるとそこそこ長い気がしますが、ソースコードの視点ではさほど大きな変更をすることなく、gRPCをTCPからパイプ通信に変更できます。[前回の記事](https://qiita.com/spc_ksudoh/items/760a41a73c825eb70e70)でも移行価値ありという結論なので、興味が湧いたら触ってみると良いと思います。


# 古い情報

パイプのACLについて記載していた古い情報を、いちおう以下に残します。

この章は古い情報です。最新情報は[新規の記事](
https://qiita.com/spc_ksudoh/items/45c2efbb4556a2cdb198)を見てください。

Win32 APIを使って、パイプを開いてACLを設定すれば、実現できます。C#でも書けると思いますが、C++/CLIで書きました。記事の中に貼るにはWin32 APIを使ったコードは長すぎるので、ポイントを説明した後にGitHubのリンクを貼ります。

1. パイプを開く
1. 設定したいユーザーグループのSIDを文字列で指定
1. パイプのセキュリティ設定を取得して、SIDに対する許可を設定

以上のような処理をします。最初の「パイプを開く」処理は、GenericHost側でパイプが作成されるまで成功しません。別スレッドで成功するまでリトライするなどの工夫が必要になります。

GitHubのコードは、[このあたり](https://github.com/suusanex/sample_winservice_pipe_duplex_wcf_and_grpc/commit/a8587edcc9ee2ea41a33c39778adb1f74200252c#diff-c12c63cb8f8b615135a71f38409c72268a35ca0ea88d8b57f8a95cc2d6d84d53)になります。