砂漠の旅人(たびと)

UNIX / MS-DOS 時代から電脳砂漠を旅しています

【Windows編】.NET 6 で gRPC を作って、IP アドレスで通信する

こんにちは、たびとです。

.NET Framework の頃は WCF(Windows Communication Foundation) を使っていましたが、 .NET 6 では使えず、gRPC が推奨されいるようです。

今回は、Windows .NET 6 で gRPC アプリを作成し、 localhost ではなく IP アドレスを指定してアクセスしてみます。

この記事の対象者

  • gRPC により HTTP/2 通信 (現在 HTTP/3 はプレビュー機能) アプリを作ってみたい。
  • WCF アプリを作ったことがあるが、gRPC アプリはまだ作ったことがない。

gRPC サーバ

Microsoftチュートリアルをベースに作成していきます。 gRPC のサーバ部は ASP.NET Core をベースにしています。

プロジェクトの作成

Visual Studio 2022 を起動し、プロジェクトを作成します。 テンプレートから gRPC (ASP.NET Core gRPC サービス) を選択します。

gRPC (ASP.NET Core gRPC サービス) を選択する

プロジェクト名を GrpcGreeter と入力します。

GrpcGreeter を入力する

初期値のまま、.NET 6 を選択します。これで Microsoftチュートリアルと同様の内容になります。

フレームワーク .NET 6 を選択する

次に、「ツール(T) - NuGet パッケージマネージャ(N) - ソリューションの NuGet パッケージの管理(N)... 」を選択します。 以下のように、ASP.NET Core 用の NLog を追加します。

NLog を追加する

プロジェクトに nlog.config を追加します。この後、プロパティの出力ディレクトリにコピーを「新しい場合はコピーする」に変更します。

nlog.config のプロパティを変更する

ソースコード

Program.cs を NLog が出力できるように変更します。 builder.Logging.ClearProviders(); を削除すると、コンソールに初期値の出力を表示することができます。

using GrpcGreeter.Services;
using NLog;
using NLog.Web;

var _logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
_logger.Debug("init main");

try
{
    var builder = WebApplication.CreateBuilder(args);

    // Additional configuration is required to successfully run gRPC on macOS.
    // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

    // NLog: Setup NLog for Dependency injection
    builder.Logging.ClearProviders();
    builder.Host.UseNLog();

    // Add services to the container.
    builder.Services.AddGrpc();

    var app = builder.Build();

    // Configure the HTTP request pipeline.
    app.MapGrpcService<GreeterService>();
    app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

    app.Run();
}
catch (Exception ex)
{
    // NLog: catch setup errors
    _logger.Error(ex, "Stopped program because of exception");
    throw;
}
finally
{
    // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
    NLog.LogManager.Shutdown();
}

nlog.config の内容を編集します。 ログは「ファイル・コンソール・ビューワ(Log2Console)」の3種類としています。 builder.Logging.ClearProviders(); を削除した場合、NLog のコンソール出力は不要となります。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info"
      internalLogFile="./logs/internal-nlog-gRPC.log">
    <extensions>
        <add assembly="NLog.Web.AspNetCore"/>
    </extensions>
    <targets>
        <!-- File -->
        <target name="logFile"
                xsi:type="File"
                encoding="UTF-8"
                writeBom="true"
                lineEnding="LF"
                layout="${longdate} ${level:uppercase=true:padding=-5} [${threadid}] ${logger} - ${message} ${exception:format=tostring}"
                fileName="${basedir}/logs/grpcgreeter.log"
                archiveFileName="${basedir}/logs/backup/grpcgreeter_{###}.log"
                archiveEvery="Day"
                archiveNumbering="Sequence"
                maxArchiveFiles="10" />
        <!-- Console -->
        <target name="console" xsi:type="ColoredConsole" layout="${level:uppercase=true:padding=-5}: ${message}" />
        <!-- Log2Console -->
        <target name="tcpOutlet" xsi:type="NLogViewer" address="tcp4://localhost:4505"/>
    </targets>
    <!-- rules to map from logger name to target -->
    <rules>
        <logger name="*" minlevel="Trace" writeTo="logFile" />
        <logger name="*" minlevel="Trace" writeTo="console" />
        <logger name="*" minlevel="Trace" writeTo="tcpOutlet" />
    </rules>
</nlog>

launchSettings.json を編集します。 ここでは、localhost を 0.0.0.0 に変更すること が重要で、 localhost のままだと、クライアントから IP アドレスでアクセスすることができません。

ちなみに、ポート番号はランダムになるようなので、割り振られた番号を使ってください。

{
  "profiles": {
    "GrpcGreeter": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": false,
      "applicationUrl": "http://0.0.0.0:5217;https://0.0.0.0:7217",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

GrpcGreeter の実行

GrpcGreeter を実行すると初回に限り、SSL 証明書のダイアログが表示されるので、「はい」を選択します。

IIS Express SSL 証明書の信頼

証明書のインストール ダイアログが表示されるので、「はい」を選択します。

証明書のインストール

launchSettings.jsonlocalhost を 0.0.0.0 に変更すると、Windows Defender が起動することがあります。 この画面が表示された場合、「アクセスを許可する(A)」を選択します。

Windows Defender の警告

GrpcGreeter を実行すると、NLog を使った場合、以下のように表示されます。 ただし、builder.Logging.ClearProviders(); を削除した場合、通常の表示になります。

GrpcGreeter の実行後

gRPC クライアント

次に通信を確認するために gRPC クライアントを作成します。

プロジェクトの作成

新たに Visual Studio 2022 を起動し、コンソールアプリを選択します。

コンソールアプリを選択する

新しいプロジェクトの構成で「GrpcGreeterClinet」の名前を入力します。

新しいプロジェクトの構成

追加情報は、.NET 6 を選択し、プロジェクトを作成します。

追加情報

次に「ツール(T) - NuGet パッケージマネージャ(N) - ソリューションの NuGet パッケージの管理(N)... 」を選択します。 以下のパッケージを追加します。

  • Google.Protobuf
  • Grpc.Net.Client
  • Grpc.Tools
  • NLog

NuGet によるパッケージの追加

GrpcGreeter と同様に nlog.config を追加し、プロパティの出力ディレクトリにコピーを「新しい場合はコピーする」に変更します。

さらに、GrpcGreeter 同様に Protos フォルダを作成し、greet.proto をコピーします。 ただし、コピー後は編集が必要になります。

IPアドレス取得

ipconfig コマンドで、IP アドレスを取得します。以降の Program.cs に 172.18.149.227 を指定します。

PS C:\> ipconfig

Windows IP 構成

イーサネット アダプター イーサネット:

   接続固有の DNS サフィックス . . . . .: mshome.net
   リンクローカル IPv6 アドレス. . . . .: fe80::c441:9249:4a11:3714%7
   IPv4 アドレス . . . . . . . . . . . .: 172.18.149.227
   サブネット マスク . . . . . . . . . .: 255.255.240.0
   デフォルト ゲートウェイ . . . . . . .: 172.18.144.1

ソースコード

Program.cs を編集します。IP アドレスは、前述の 172.18.149.227 を使います。 ここは、各自の IP アドレスに置き換えてください。

また、localhost から IP アドレスに変更すると エラーになるため、 ServerCertificateCustomValidationCallback を指定して、SSL検証を常に Ture になるように変更します。

https の確認後、var server = "http://172.18.149.227:5217"; のように http に変更し、http も確認してください。 ここでは、http は割愛します。

using System.Net.Http;
using System.Threading.Tasks;
using Grpc.Net.Client;
using GrpcGreeterClient;
using NLog;

var _logger = LogManager.GetCurrentClassLogger();

var server = "https://172.18.149.227:7217";
_logger.Info(server);

try
{
    var option = new GrpcChannelOptions()
    {
        HttpClient = new HttpClient(new HttpClientHandler
        {
            // SSL証明書の検証で常に True を返す
            ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
        })
    };
    var channel = GrpcChannel.ForAddress(server, option);
    var client = new Greeter.GreeterClient(channel);

    var response = await client.SayHelloAsync(new HelloRequest { Name = "World" });

    _logger.Info(response.Message);
}
catch (Exception ex)
{
    _logger.Error(ex);
}

GrpcGreeterClinet.csproj を編集します。 gRPC クライアントを指定する必要があるため、 ItemGroup で囲まれた GrpcServices="Client" を追加します。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.21.5" />
    <PackageReference Include="Grpc.Net.Client" Version="2.47.0" />
    <PackageReference Include="Grpc.Tools" Version="2.47.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="NLog" Version="5.0.2" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="Protos\" />
  </ItemGroup>

  <ItemGroup>
    <None Update="nlog.config">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

nlog.config を編集します。サーバ版とは異なるため、注意してください。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Info"
      internalLogFile="../logs/internal-nlog-Console.log">
    <targets>
        <!-- File -->
        <target name="logFile"
                xsi:type="File"
                encoding="UTF-8"
                writeBom="true"
                lineEnding="CRLF"
                layout="${longdate} ${level:uppercase=true:padding=-5} [${threadid}] ${logger} - ${message} ${exception:format=tostring}"
                fileName="../logs/${processname}.log"
                archiveFileName="../logs/backup/${processname}_{###}.log"
                archiveEvery="Day"
                archiveNumbering="Sequence"
                maxArchiveFiles="10" />

        <!-- Console -->
        <target name="console" xsi:type="ColoredConsole" layout="${level:uppercase=true:padding=-5}: ${message}" />

        <!-- Log2Console -->
        <target name="tcpOutlet" xsi:type="NLogViewer" address="tcp4://localhost:4505"/>
    </targets>

    <rules>
        <logger name="*" minlevel="Debug" writeTo="logFile" />
        <logger name="*" minlevel="Trace" writeTo="console" />
        <logger name="*" minlevel="Trace" writeTo="tcpOutlet" />
    </rules>
</nlog>

greet.proto を編集します。option csharp_namespace = "GrpcGreeterClient"; と gRPC クライアントのネームスペースに変更します。 ここを間違えるとソースコードが出力されないので注意してください。

syntax = "proto3";

option csharp_namespace = "GrpcGreeterClient";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

GrpcGreeterClient の実行

GrpcGreeterClinet を実行すると以下のように出力されます。

GrpcGreeterClient の実行結果

このとき、GrpcGreeter には以下のように出力されます。

GrpcGreeter の実行結果

Log2Console を使った確認

Log2Console の起動後に NLog からログを受け取ることができるように設定します。 メニューの「Receives...」ボタンをクリックします。

Log2Console の初期設定

Add ボタンを押して、「TCP (IP v4 and v6)」をクリックします。

Receives を追加する

まずは、IPv4 で受け取るように設定します。そのまま何も編集しないでください。

IPv4 でログを受け取る

再度、Add ボタンで同様に「TCP (IP v4 and v6)」をクリックし、 IPv6 で受け取るように設定します。二段目は、「Use IPv6 Addresses」を Ture に変更します。

IPv6 でログを受け取る

これで設定完了です。 GrpcGreeter を起動し、GrpcGreeterClinet を起動すると、以下のように両方のログを確認することができます。

Log2Console によるログの確認

Log2Console の日本語版は、下記サイトからダウンロードしてください。 オリジナルだと詳細が文字化けするので注意してください。

github.com

まとめ

以前、.NET 5 のときに、gRPC による動画ファイルの送受信アプリを作ったのですが、 そのときは、localhost から 0.0.0.0 に変更することなく、IP アドレスを指定できていたような気がします。 いつの間にか仕様が変更されたのか、はたまた何かを忘れているのか、 原因を追究するのに時間が掛かってしまいました。

今回の続きで、Linux 版を掲載しました。WSL2/Ubuntu で同じアプリを作成します。

sabakunotabito.hatenablog.com

最後に参考サイトを掲載しておきます。 では、皆さん、よい旅を。

参考サイト