はじめに
注意
今回の記事は前回の記事の続きになります。
用意しているコードはすべて前回の続きからとなりますのでご注意ください。
あらためて、はじめに
前回はWindowsServiceを.NET5で作成する方法を記載しました。
ところで、サービスに対して何かリクエストをしたいとき、プロセス間通信を使用したくなると思います。
しかしながら、プロセス間通信を実施するIpcServerChannelクラスは.NET5ではサポートされていません。
そこで、別の方法を用いてプロセス間通信を実現する方法を記載します。
実行環境
- VisualStudio2019
- Windows10Pro 20H2 (Build: 19042.867)
- .NET5 SDK 5.0.1
参考
今回の記事の元ネタになった記事はこちらです。
良記事というか、日本語で.NETCoreでのIPCについて記述されているページがここしかありませんでした。
ぜひチェックしてください。
https://mseeeen.msen.jp/first-grpc-with-wpf-on-dotnet-core-3/
実装
事前準備
まず、プロセス間通信を行うということは、プロセス間通信を行うクライアントのアプリの作成が必要となりますので、まず、こちらを作成します。
そして、やはりそのままでは実装できませんので、事前準備としてNuGetパッケージのインストールを行います。
今回必要なのは、以下の3パッケージです
- Google.Protobuf
- Grpc
- Grpc.Tools
順を追ってやっていきましょう
プロジェクトの作成
プロセス間通信用のコンソールアプリの用意
ソリューションエクスプローラーから、新しいプロジェクトを追加します。
プロセス間通信で使用する、サービス/クライアント共有のライブラリの用意
同様に、ソリューションエクスプローラーから、新しいプロジェクトを追加します。
-
クラス ライブラリ (.NET Core)
を選択します。
- 任意の名前を決めて、そのまま作成します。
- 先ほどと同様に、作成したプロジェクトのプロパティを開き、対象のフレームワークは
.NET 5.0
を選択します。
NuGetパッケージのインストール
ソリューションエクスプローラーからソリューションを右クリックし、ソリューションのNuGetパッケージの管理
を選択します。
-
Google.Protobuf
を すべてのプロジェクトに対して インストールします。
-
Grpc
を すべてのプロジェクトに対して インストールします。
-
Grpc.Tools
を 共通ライブラリ(今回はCommon)に インストールします。
Commonプロジェクト (サービス/クライアント両方から参照する共通ライブラリプロジェクト) への参照の追加
先ほど作成したCommonプロジェクトに対して、サービス/クライアント両方からプロジェクト参照を追加してあげてください。
これで準備は完了です。
Grpcの .proto
ファイルの作成
Common
プロジェクトを右クリックし、任意の名前の .proto
ファイルを作成してあげてください。
今回の例だと commander.proto
という名前にしています。
// Common/commander.proto
syntax = "proto3";
// 名前空間の定義
option csharp_namespace = "Common";
// google.protobuf.Empty を使用するためにインポート
import "google/protobuf/empty.proto";
// google.protobuf.Timestamp を使用するためにインポート
import "google/protobuf/timestamp.proto";
// プロセス間通信に使用するインターフェースの定義
service Commander {
// インターフェース: 気象庁から取得していた1時間以内の気象予報情報
rpc FetchWeather (google.protobuf.Empty) returns (Response);
// インターフェース: 気象庁から取得していた任意の前の時間の気象予報情報
rpc FindWeather (Request) returns (Response);
}
// リクエスト電文の定義
message Request {
int32 HourAgo = 1;
}
// レスポンス電文の定義
message Response {
google.protobuf.Timestamp ReportedAt = 1;
string Weather = 2;
}
また、 .csprojを修正し、以下の追加行を追加することを必ず忘れないでください。これが無いとコンパイルできません。(5敗くらい)
<!-- Common.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.15.6" />
<PackageReference Include="Grpc" Version="2.36.4" />
<PackageReference Include="Grpc.Tools" Version="2.36.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
+ <ItemGroup>
+ <Protobuf Include="commander.proto" />
+ </ItemGroup>
</Project>
この状態でビルドすると、 プロジェクトルート/obj/Debug/net5.0
配下あたりに以下のファイルができていることが確認できると思います。
- Commander.cs
- CommanderGrpc.cs
これが確認できていればOKです。
.proto
の定義を利用するサービスクラスを作成する
任意の名前のクラスを作成します。
この時、 Commander.CommanderBase
を継承することを忘れないでください。
事前に先ほどまでの状態でビルドしていたならば、 Commander.CommanderBase
は IntelliSence によってサジェストされます。
// Common/CommanderService.cs
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using System;
using System.Threading.Tasks;
namespace Common
{
public class CommanderService : Commander.CommanderBase
{
}
}
overrideキーワードをクラス内で使用すると、先ほど定義したインターフェースをこのクラスの中に生やすことができます。
// Common/CommanderService.cs
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using System;
using System.Threading.Tasks;
namespace Common
{
public class CommanderService : Commander.CommanderBase
{
+ public override Task<Response> FetchWeather(Empty request, ServerCallContext context)
+ {
+ return base.FetchWeather(request, context);
+ }
+ public override Task<Response> FindWeather(Request request, ServerCallContext context)
+ {
+ return base.FindWeather(request, context);
+ }
}
}
ついでにdelegateあたりも定義しておいて、IPCでやってきたリクエストに対してサービスがレスポンスを返せるようにしておきましょう。
// Common/CommanderService.cs
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using System;
using System.Threading.Tasks;
namespace Common
{
+ public delegate Response FetchWeather(int hourAgo);
public class CommanderService : Commander.CommanderBase
{
+ public FetchWeather commanderServiceDelegate;
public override Task<Response> FetchWeather(Empty request, ServerCallContext context)
{
- return base.FetchWeather(request, context);
+ return Task.Run(() => commanderServiceDelegate.Invoke(0));
}
public override Task<Response> FindWeather(Request request, ServerCallContext context)
{
- return base.FindWeather(request, context);
+ return Task.Run(() => commanderServiceDelegate.Invoke(request.HourAgo));
}
}
}
WindowsService側をCommanderServiceと連携させる
連携させます
// WindowsServiceTest/Worker.cs
+using Common;
+using Grpc.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace WindowsServiceTest
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
+ /// <summary>
+ /// GRPCのサービス
+ /// </summary>
+ private CommanderService _grpcService;
+ /// <summary>
+ /// GRPCのサーバ
+ /// </summary>
+ private Server _grpcServer;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
+ // grpcServerを立ち上げる
+ _grpcService = new();
+ _grpcService.commanderServiceDelegate = FetchWeather;
+ _grpcServer = new()
+ {
+ Services = { Commander.BindService(_grpcService) },
+ // 第一引数はローカルループバックアドレス、第二引数にはポート番号を入力すること / Commonで定義しておくとクライアント側でも使えるのでなお良し!
+ Ports = { new("127.0.0.1", 50000, ServerCredentials.Insecure) }
+ };
+ _grpcServer.Start();
}
public override Task StartAsync(CancellationToken cancellationToken)
{
return base.StartAsync(cancellationToken);
}
public override Task StopAsync(CancellationToken cancellationToken)
{
return base.StopAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var client = new HttpClient();
var response = await client.SendAsync(new(HttpMethod.Get, @"https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"));
var body = await response?.Content.ReadAsStringAsync() ?? "";
_logger.LogInformation("response content: " + body);
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(360000, stoppingToken);
}
}
+ public Response FetchWeather(int hourAgo)
+ {
+ return null;
+ }
}
}
ついでに FetchWeather()
の中身も実装しておきましょう
// WindowsServiceTest/Worker.cs
using Common;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace WindowsServiceTest
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
/// <summary>
/// GRPCのサービス
/// </summary>
private CommanderService _grpcService;
/// <summary>
/// GRPCのサーバ
/// </summary>
private Server _grpcServer;
+ private Dictionary<DateTime, string> _weathers;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
// grpcServiceとgrpcServerを立ち上げる
_grpcService = new();
_grpcService.commanderServiceDelegate = FetchWeather;
_grpcServer = new()
{
Services = { Commander.BindService(_grpcService) },
Ports = { new("127.0.0.1", 50000, ServerCredentials.Insecure) }
};
_grpcServer.Start();
}
public override Task StartAsync(CancellationToken cancellationToken)
{
return base.StartAsync(cancellationToken);
}
public override Task StopAsync(CancellationToken cancellationToken)
{
return base.StopAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var client = new HttpClient();
var response = await client.SendAsync(new(HttpMethod.Get, @"https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"));
var body = await response?.Content.ReadAsStringAsync() ?? "";
+ _weathers.Add(DateTime.Now, body);
_logger.LogInformation("response content: " + body);
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(360000, stoppingToken);
}
}
public Response FetchWeather(int hourAgo)
{
- return null;
+ var orderedKeys = _weathers.Keys.OrderByDescending(x => x);
+ if (orderedKeys.Count() < hourAgo)
+ {
+ return new()
+ {
+ ReportedAt = new(),
+ Weather = string.Empty
+ };
+ }
+
+ var findKey = orderedKeys.ElementAt(hourAgo);
+ return new()
+ {
+ ReportedAt = Timestamp.FromDateTime(findKey.ToUniversalTime()),
+ Weather = _weathers[findKey]
+ };
}
}
}
クライアント側の実装
クライアント側で実施する実装は単純です。
チャンネルを作成し、それを元にクライアントを作成してインターフェースを呼び出してやればそれでよいです。
// WindowsServerTestClient/Program.cs
using Common;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using System;
namespace WindowsServiceTestClient
{
class Program
{
static void Main(string[] args)
{
var channel = new Channel("127.0.0.1", 50000, ChannelCredentials.Insecure);
var client = new Commander.CommanderClient(channel);
Console.WriteLine("plz input empty or number key");
var read = Console.ReadLine();
if (string.IsNullOrWhiteSpace(read))
{
var fetchWeatherResponse = client.FetchWeather(new Empty());
Console.WriteLine(fetchWeatherResponse.ReportedAt);
Console.WriteLine(fetchWeatherResponse.Weather);
return;
}
int hour = 0;
if (!int.TryParse(read, out hour))
{
Console.WriteLine("invalid string");
return;
}
var findWeatherResponse = client.FindWeather(new Request()
{
HourAgo = hour
});
Console.WriteLine(findWeatherResponse.ReportedAt);
Console.WriteLine(findWeatherResponse.Weather);
}
}
}
このように、通信した結果が返ってくるのを確認できればOKです。
おわりに
他の方の記事を参考には致しましたが、このように、.NET5で、WindowsServiceに対してプロセス間通信を実施するプログラムを作成することができました。
また、このプロセス間通信は、サーバ側が上位権限で動いている場合(Administratorなど)でも、適切にプロセス間通信ができることが確認ができています。
(まぁ、ほぼServer/Clientの実装みたいなものなので、当然と言えば当然かもしれませんが。。。)
次回こそ、インストーラーの作成を実施する記事を作成します。(この記事の続きとなります。)
同じ開発者の助けになる記事になることを望みます。
また、何か誤っている個所がありましたらご指摘ください。
ここまで読んでくださり、ありがとうございました。