砂漠の旅人(たびと)

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

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

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

前回、Windows 版を作ったので、今回は Linux 版を作ります。 いつものように、WSL2/Ubuntu で作ってみたいと思います。

前回の Windows 版はこちらから。

sabakunotabito.hatenablog.com

この記事の対象者

  • gRPC を使って HTTP/2 通信 (現在 HTTP/3 はプレビュー機能) アプリを作ってみたい。
  • Windows ではなく、Linux 版を作ってみたい。

事前準備

前回も IP アドレスを確認しましたが、ここでの IP アドレスは Log2Console の出力用に使います。 よって、nlog.config の localhost を 172.18.149.227 に指定することになります。 当然ですが、各自の IP アドレスに置き換えてください。

注意:「イーサネット アダプター vEthernet (WSL)」を指定しても出力されません。

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

gRPC サーバ

WSL2/Ubuntu のコンソール上で、dotnet コマンドで GrpcGreeter を作成します。 dotnet new コマンドを実行するときにパスを指定しないと、直下のディレクトリ名がプロジェクト名になります。

$ mkdir GrpcGreeter
$ cd GrpcGreeter
$ dotnet new grpc
The template "ASP.NET Core gRPC Service" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on /home/tabito/GrpcGreeter/GrpcGreeter.csproj...
  Determining projects to restore...
  Restored /home/tabito/GrpcGreeter/GrpcGreeter.csproj (in 8.68 sec).
Restore succeeded.

NLog.Web.AspNetCore パッケージを追加します。

$ dotnet add package NLog.Web.AspNetCore
  Determining projects to restore...
  Writing /tmp/tmpKNtG2H.tmp
  以下省略

nlog.config ファイルを作成します。

$ touch nlog.config

ソースコード

Visual Studio Code をインストールしている場合、code コマンドで Windows 上で編集することができて便利です。

$ code .
Updating VS Code Server to version 6d9b74a70ca9c7733b29f0456fd8195364076dda
Removing previous installation...
Installing VS Code Server for x64 (6d9b74a70ca9c7733b29f0456fd8195364076dda)
Unpacking: 100%
Unpacked 2416 files and folders to /home/tabito/.vscode-server/bin/6d9b74a70ca9c7733b29f0456fd8195364076dda.

Visual Studio Code による編集

launchSettings.json を編集します。 IP アドレスを指定してアクセスできるように、applicationUrl の localhost を 0.0.0.0 に変更します。

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

GrpcGreeter.csproj を編集します。 ItemGroup を新たに追加し、nlog.config をプロジェクトに追加します。

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

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

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

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.40.0" />
    <PackageReference Include="NLog.Web.AspNetCore" Version="5.1.0" />
  </ItemGroup>

</Project>

nlog.config を編集します。NLogViewer の localhost は各自の IP アドレスを指定します。 tcpOutlet の address="tcp4://... の tcp4 を tcp に変更すると IPv6 の出力に切替わります。

<?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: WSLのアドレスではなく、通常のIPアドレスを指定すること! -->
        <target name="tcpOutlet" xsi:type="NLogViewer" address="tcp4://172.18.149.227: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>

Program.cs を編集します。NLog が有効になるように追加します。

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();
}

ビルド&実行

donet build コマンドでビルドします。

$ dotnet build
MSBuild version 17.3.0+92e077650 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
  GrpcGreeter -> /home/tabito/GrpcGreeter/bin/Debug/net6.0/GrpcGreeter.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:04.50

dotnet run コマンドで実行します。

$ dotnet run
Building...
DEBUG: init main
TRACE: Discovering gRPC methods for GrpcGreeter.Services.GreeterService.
TRACE: Added gRPC method 'SayHello' to service 'greet.Greeter'. Method type: 'Unary', route pattern: '/greet.Greeter/SayHello'.
DEBUG: Hosting starting
DEBUG: Using development certificate: CN=localhost (Thumbprint: 45FD30CC9749529649024AD1EAD628123590A96E)
INFO : Now listening on: http://0.0.0.0:5045
INFO : Now listening on: https://0.0.0.0:7045
DEBUG: Loaded hosting startup assembly GrpcGreeter
INFO : Application started. Press Ctrl+C to shut down.
INFO : Hosting environment: Development
INFO : Content root path: /home/tabito/GrpcGreeter/
DEBUG: Hosting started

Log2Console を起動しておくと、上記のログを確認することができます。 Log2Console の詳細は、前回 Windows 版の記事を参考にしてください。

Log2Console によるログの確認

gRPC クライアント

WSL2/Ubuntu のコンソールを新たに開き、dotnet コマンドで GrpcGreeterClient を作成します。

$ mkdir GrpcGreeterClient
$ cd GrpcGreeterClient
$ dotnet new console
The template "Console App" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on /home/tabito/GrpcGreeterClient/GrpcGreeterClient.csproj...
  Determining projects to restore...
  Restored /home/tabito/GrpcGreeterClient/GrpcGreeterClient.csproj (in 86 ms).
Restore succeeded.

gRPC と NLog パッケージを追加します。

$ dotnet add package Google.Protobuf
$ dotnet add package Grpc.Net.Client
$ dotnet add package Grpc.Tools
$ dotnet add package NLog

サーバ用の greet.proo をコピーします。

$ mkdir Protos
$ cp ../GrpcGreeter/Protos/greet.proto  ./Protos

nlog.config ファイルを作成します。

$ touch nlog.config

ソースコード

greeto.proto を編集します。 csharp_namespace はサーバ用なので、 GrpcGreeterClient に変更します。

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.csproj を編集します。 gRPC と NLog を有効にするため、それぞれの ItemGroup を追加します。

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

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

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

  <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.48.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="NLog" Version="5.0.2" />
  </ItemGroup>

</Project>

nlog.config を編集します。 NLogViewer の localhost は各自の IP アドレスを指定します。

<?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: WSLのアドレスではなく、通常のIPアドレスを指定すること! -->
        <target name="tcpOutlet" xsi:type="NLogViewer" address="tcp4://172.18.149.227:4505"/>
    </targets>

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

WSL2/Ubuntu の IP アドレスを取得します。

$ ip a show dev eth0
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:88:c1:b8 brd ff:ff:ff:ff:ff:ff
    inet 172.18.126.178/20 brd 172.18.127.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fe88:c1b8/64 scope link
       valid_lft forever preferred_lft forever

Program.cs を編集します。サーバの IP アドレスは ip コマンドで取得した IP アドレスを指定します。

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.126.178:7045";
_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);
}

ビルド&実行

donet build コマンドでビルドします。

$ dotnet build
MSBuild version 17.3.0+92e077650 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
  GrpcGreeterClient -> /home/tabito/GrpcGreeterClient/bin/Debug/net6.0/GrpcGreeterClient.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.88

dotnet run コマンドで実行します。

$ dotnet run
INFO : https://172.18.126.178:7045
INFO : Hello World

サーバ側のコンソールには、以下のようにメッセージが出力されています。

DEBUG: Connection id "0HMK0ATIV0DCR" accepted.
DEBUG: Connection id "0HMK0ATIV0DCR" started.
DEBUG: Connection 0HMK0ATIV0DCR established using the following protocol: Tls13
TRACE: Connection id "0HMK0ATIV0DCR" sending SETTINGS frame for stream ID 0 with length 18 and flags NONE.
TRACE: Connection id "0HMK0ATIV0DCR" sending WINDOW_UPDATE frame for stream ID 0 with length 4 and flags 0x0.
TRACE: Connection id "0HMK0ATIV0DCR" received SETTINGS frame for stream ID 0 with length 12 and flags NONE.
TRACE: Connection id "0HMK0ATIV0DCR" sending SETTINGS frame for stream ID 0 with length 0 and flags ACK.
TRACE: Connection id "0HMK0ATIV0DCR" received WINDOW_UPDATE frame for stream ID 0 with length 4 and flags 0x0.
TRACE: Connection id "0HMK0ATIV0DCR" received SETTINGS frame for stream ID 0 with length 0 and flags ACK.
TRACE: Connection id "0HMK0ATIV0DCR" received HEADERS frame for stream ID 1 with length 189 and flags END_HEADERS.
TRACE: Connection id "0HMK0ATIV0DCR" received DATA frame for stream ID 1 with length 12 and flags NONE.
TRACE: Connection id "0HMK0ATIV0DCR" received DATA frame for stream ID 1 with length 0 and flags END_STREAM.
INFO : Request starting HTTP/2 POST https://172.18.126.178:7045/greet.Greeter/SayHello application/grpc -
DEBUG: Wildcard detected, all requests with hosts will be allowed.
TRACE: All hosts are allowed.
DEBUG: 3 candidate(s) found for the request path '/greet.Greeter/SayHello'
DEBUG: Endpoint 'gRPC - /greet.Greeter/SayHello' with route pattern '/greet.Greeter/SayHello' is valid for the request path '/greet.Greeter/SayHello'
DEBUG: Endpoint 'gRPC - Unimplemented method for greet.Greeter' with route pattern 'greet.Greeter/{unimplementedMethod}' is valid for the request path '/greet.Greeter/SayHello'
DEBUG: Endpoint 'gRPC - Unimplemented service' with route pattern '{unimplementedService}/{unimplementedMethod}' is valid for the request path '/greet.Greeter/SayHello'
DEBUG: Request matched endpoint 'gRPC - /greet.Greeter/SayHello'
INFO : Executing endpoint 'gRPC - /greet.Greeter/SayHello'
DEBUG: Reading message.
DEBUG: Connection id "0HMK0ATIV0DCR", Request id "0HMK0ATIV0DCR:00000001": started reading request body.
DEBUG: Connection id "0HMK0ATIV0DCR", Request id "0HMK0ATIV0DCR:00000001": done reading request body.
TRACE: Deserializing 7 byte message to 'GrpcGreeter.HelloRequest'.
TRACE: Received message.
TRACE: Connection id "0HMK0ATIV0DCR" sending HEADERS frame for stream ID 1 with length 59 and flags END_HEADERS.
DEBUG: Sending message.
TRACE: Serialized 'GrpcGreeter.HelloReply' to 13 byte message.
TRACE: Message sent.
INFO : Executed endpoint 'gRPC - /greet.Greeter/SayHello'
TRACE: Connection id "0HMK0ATIV0DCR" sending DATA frame for stream ID 1 with length 18 and flags NONE.
TRACE: Connection id "0HMK0ATIV0DCR" sending HEADERS frame for stream ID 1 with length 15 and flags END_STREAM, END_HEADERS.
INFO : Request finished HTTP/2 POST https://172.18.126.178:7045/greet.Greeter/SayHello application/grpc - - 200 - application/grpc 213.0363ms
TRACE: Connection id "0HMK0ATIV0DCR" received PING frame for stream ID 0 with length 8 and flags NONE.
TRACE: Connection id "0HMK0ATIV0DCR" sending PING frame for stream ID 0 with length 8 and flags ACK.
DEBUG: Connection id "0HMK0ATIV0DCR" received FIN.
DEBUG: Connection id "0HMK0ATIV0DCR" is closed. The last processed stream ID was 1.
DEBUG: Connection id "0HMK0ATIV0DCR" sending FIN because: "The client closed the connection."
DEBUG: Connection id "0HMK0ATIV0DCR" stopped.

Log2Console は、以下のようにサーバとクライアントの両方のログが出力されます。

サーバとクライアントのログを確認する

SSL 証明書はどうなるの?

何事もなく動作したので忘れていましたが、Linux の場合は SSL 証明書はどうなるのでしょう。 Windows と同様に裏側でデフォルトの証明書が作成されるのでしょうか。

最初から WSL2/Ubuntu を作って確認すると、初回作成時に開発用の証明書がインストールされてました。 この表示は初回のみに表示されるため、2回目以降は表示されないようです。

$ dotnet new grpc

.NET 6.0 へようこそ!
---------------------
SDK バージョン: 6.0.108

----------------
ASP.NET Core の HTTPS 開発証明書をインストールしました。
証明書を信頼するには、'dotnet dev-certs https --trust' (Windows および macOS のみ) を実行します。
HTTPS の詳細については、https://aka.ms/dotnet-https を参照してください
----------------
最初のアプリを作成するには、https://aka.ms/dotnet-hello-world を参照してください
最新情報については、https://aka.ms/dotnet-whats-new を参照してください
ドキュメントを探索するには、https://aka.ms/dotnet-docs を参照してください
GitHub で問題の報告とソースの検索を行うには、https://github.com/dotnet/core を参照してください
'dotnet --help' を使用して使用可能なコマンドを確認するか、https://aka.ms/dotnet-cli にアクセスしてください
--------------------------------------------------------------------------------------
テンプレート "ASP.NET Core gRPC Service" が正常に作成されました。

作成後の操作を処理しています...
/home/tabito/grpc_greeter/src/GrpcGreeter/GrpcGreeter.csproj で ' dotnet restore ' を実行しています...
  Determining projects to restore...
  Restored /home/tabito/grpc_greeter/src/GrpcGreeter/GrpcGreeter.csproj (in 10.52 sec).
正常に復元されました。

数年前、.NET 5 で作ったときは、SSL 証明書を openssl コマンドで作成して利用したのを思い出しました。 とりあえず、次回への課題としましょう。

まとめ

今回は、WSL2/Ubuntu 上の .NET 6 で gRPC アプリを作成してみました。 サーバ用途は Linux (docker)で、GUI を作るなら Windows が現時点でのベストだと思うので、 どちらでも作れる .NET Core は便利だと思います。 ただ、まだまだ情報が少ないため、やりたいことを実現するための調査に苦労することが多いです。

以前、.NET Core の Windows 版と Linux 版を説明したときは、Windows 版を Linux へコピーしましたが、 今回はコマンドで作成してみました。

ちなみに、dotnet new sln コマンドを使うと、ソリューションファイル(sln) を作成することができます。 このソリューションファイルにプロジェクトファイルを追加することで Windows と同様の構成にすることができます。 そんな面倒なことをするより、Windows でプロジェクトを作成して、Linux にコピーした方が早いと思います。

今回の続きで、Docker 版を掲載しました。ここでは、今回使わなかった証明書も作成しています。

sabakunotabito.hatenablog.com

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

参考サイト