ASP.NET Core で gRPC と gRPC-Web の両方に対応できるか試した

はじめに

Grpc.AspNetCore.Web を使えば、プロキシを別に立てる必要なく、ASP.NET Core のパイプラインに gRPC-Web を組み込める。

gRPC-Web を組み込んだ場合に、gRPC-Web だけではなく通常の gRPC も使えるのか、気になったので試してみた。

gRPC サーバー

ASP.NET Core gRPC サービスのプロジェクトを新規作成。ターゲットフレームワーク .NET 5 で。

gRPC-Web をサポートするため、gRPC サーバー側に、Grpc.AspNetCore.Web をインストールする。

www.nuget.org

greet.proto

syntax = "proto3";

option csharp_namespace = "HelloGrpcWeb";

package greet;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string firstName = 1;
  string lastName = 2;
}

message HelloReply {
  string message = 1;
}

GreeterService

using System.Threading.Tasks;
using Grpc.Core;

namespace HelloGrpcWeb
{
    public class GreeterService : Greeter.GreeterBase
    {
        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = $"Hello {request.FirstName} {request.LastName}",
            });
        }
    }
}

Startup

ASP.NET Core のパイプラインに gRPC-Web を組み込む。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace HelloGrpcWeb
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            // ASP.NET Core のパイプラインに gRPC-Web を組み込む
            app.UseGrpcWeb();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>()
                    .EnableGrpcWeb();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("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");
                });
            });
        }
    }
}

Program

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace HelloGrpcWeb
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

gRPC クライアント

コンソールアプリケーションのプロジェクトを新規作成する。ターゲットフレームワークはこちらも .NET 5。

[接続済みサービスの追加] - [サービス参照] - [gRPC] で、greeter.proto を選択すると、必要なパッケージがインストールされ、クライアントのコードが生成される。gRPC-Web を呼び出すために、Grpc.Net.Client.Web もインストールしておく。

www.nuget.org

通常の gRPC と gRPC-Web をそれぞれ呼び出すサンプルは次の通り。

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

namespace HelloGrpcWeb.Client
{
    class Program
    {
        const string Address = "https://localhost:5001";

        static async Task Main(string[] args)
        {
            await Grpc();
            await GrpcWeb();

            Console.ReadLine();
        }

        static async Task Grpc()
        {
            var channel = GrpcChannel.ForAddress(Address);

            var client = new Greeter.GreeterClient(channel);

            var reply = await client.SayHelloAsync(new HelloRequest
            {
                FirstName = "Takefusa",
                LastName = "Kubo",
            });

            Console.WriteLine($"gRPC:{reply.Message}");
        }

        static async Task GrpcWeb()
        {
            var channel = GrpcChannel.ForAddress(Address, new GrpcChannelOptions
            {
                HttpHandler = new GrpcWebHandler(
                    new HttpClientHandler()),
            });

            var client = new Greeter.GreeterClient(channel);

            var reply = await client.SayHelloAsync(new HelloRequest
            {
                FirstName = "Takehiro",
                LastName = "Tomiyasu",
            });

            Console.WriteLine($"gRPC Web:{reply.Message}");
        }
    }
}

実行結果

上のウィンドウに、gRPC と gRPC-Web がそれぞれ呼び出されているログが確認できた。

f:id:griefworker:20210527094848p:plain

おわりに

別にプロキシを立てることなく、1つの ASP.NET Core アプリで gRPC と gRPC-Web の両方に対応できるのは素晴らしい。Microsoft の本気を感じた。

かぐや様は告らせたい(22)

ヤングジャンプで連載しているけど、お色気など一切無い、ヤングジャンプらしからぬ健全なマンガなので、会長とかぐやが交際してもその路線は変わらないだろうなと思っていた。ましてや、セッ…なんて扱わないだろうなと思ってたので、まさかの展開に。

いやでも、会長モンスター○貞で真摯だし、いざとなったら日和るでしょ。ほらやっぱり。……ええっ?!健全なマンガがセッ…を扱うと、こういう描写になるのかと納得。そういえば、健全王が主人公のマンガも本誌では連載中だったな。時代だなぁ。

さよなら私のクラマー(1)〜(14)

さよなら私のクラマー』全14巻を読んだ。タイトルだけ見たら、何の漫画かわからない人多そうだけど、サッカー漫画。しかも高校女子サッカー。サッカー漫画は数多くあれど、女子サッカーを題材にしたものを、自分は本作以外知らない。高校女子サッカーとなるとなおさら。

キャプテン翼みたいに必殺のシュートなんて無い。代わりに、マルセイユルーレットエラシコやシャペウといった現実世界の足技が出てくるので、ちょっとサッカー通になれる。描写は丁寧で、絵が綺麗なのも相まって、他の名サッカー漫画に負けず劣らず読んでいて熱くなれた。

主人公たちが所属する通称ワラビーズの選手たちと、対戦するライバルチームの選手たち、その多くにエピソードがあり、皆実力もさることながらキャラクターとしても個性的で魅力的に描かれていた。中でも、自分のお気に入りはクマこと深津監督。監督業を過去に挫折しやる気なしモードなんだけど、時折見せるキレに、名将の予感をヒシヒシと感じた。特に素人の越前をピッチに送り出すシーンと、新女王戦で劣勢のワラビーズをハーフタイムで焚きつけるシーンは、自分の中のベスト。

そのクマがせっかくやる気出して、新戦術ゲーゲンプレッシングをひっさげ、いよいよ埼玉王者にリベンジ!…というところで連載終了。マジで?ここで終わり?詩人と全国で再戦は?第二部とか始まらないの?盛り上がってきたところだったのに。

キャプテン翼が世のサッカー小僧に影響を与えまくったように、このマンガがサッカー少女に影響を与えるポテンシャルは秘めていた。ただ、少女コミックっていう柄じゃ無いし、月マガだったから人気出たのはあるだろうから、難しいか。続編出ないかな。

Trace に出力した内容を Application Insights に保存する

はじめに

セルフホストしてる WCF サービスで、例外が発生したら Application Insights に記録したい。ただ、WCF 用のパッケージはラボの段階で、しかもリポジトリアーカイブされている。開発されていない。

github.com

今から TelemetryClient を使って Application Insights に送信するコードを書いて回るほどの日程は無い。というか死にゆく WCF のためにそこまでリソースを使いたくない。

そういえばトレースを出力するコードは、いくらか仕込んでいたな。トレースを Application Insights に送信するパッケージはちゃんとある。

www.nuget.org

こいつを使えば良さそうだ。

サービスコントラクトを定義

using System.ServiceModel;

namespace WcfAppInsights.Shared
{
    [ServiceContract]
    public interface IEchoService
    {
        [OperationContract]
        string Echo(string message);
    }
}

WCF サービスを実装

NuGet で Microsoft.ApplicationInsights.TraceListener をインストールし、サービスクラスを実装する。引数で渡された文字列をエラーとしてトレースに出力するだけ。Application Insights にちゃんと送信されるよう、即フラッシュしておく。

using System;
using System.Diagnostics;
using System.ServiceModel;
using WcfAppInsights.Shared;

namespace WcfAppInsights.Server
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new ServiceHost(typeof(EchoService));
            host.AddServiceEndpoint(
                typeof(IEchoService),
                new NetTcpBinding(),
                "net.tcp://localhost:8081/EchoService");
            host.Open();
            Console.WriteLine("Enter で終了");
            Console.ReadLine();
            host.Close();
        }
    }

    public class EchoService : IEchoService
    {
        public string Echo(string message)
        {
            Trace.TraceError(message);
            Trace.Flush();

            return message;
        }
    }
}

App.config を記述

トレースを Application Insights に送信するために、Microsoft.ApplicationInsights.TraceListener.ApplicationInsightsTraceListener を使うように構成する。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.diagnostics>
    <trace>
      <listeners>
        <add name="myAppInsightsListener" type="Microsoft.ApplicationInsights.TraceListener.ApplicationInsightsTraceListener, Microsoft.ApplicationInsights.TraceListener" />
      </listeners>
    </trace>
  </system.diagnostics>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
  </startup>
</configuration>

ApplicationInsights.config を追加

ApplicationInsightsTraceListener というか TelemetryClient は、コードで InstrumentationKey を指定しなかったら、ApplicationInsights.config に記述してあるものを使うようだ。

<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  <InstrumentationKey>your-instrumentation-key</InstrumentationKey>
</ApplicationInsights>

ビルド時にちゃんと出力先にコピーされるようにしておく。うっかり忘れて気づくまで時間かかった。

WCF クライアントを実装

using System;
using System.ServiceModel;
using WcfAppInsights.Shared;

namespace WcfAppInsights.Client
{
    class Program
    {
        static void Main(string[] args)
        {
            var channel = ChannelFactory<IEchoService>.CreateChannel(
                new NetTcpBinding(),
                new EndpointAddress("net.tcp://localhost:8081/EchoService"));

            var reply = channel.Echo("Hello Application Insights");

            Console.WriteLine(reply);

            Console.WriteLine("Enter で終了");
            Console.ReadLine();
            ((IClientChannel)channel).Close();
        }
    }
}

実行結果

WCF サーバーと WCF クライアントを実行して、少し待つと、トレースに出力したメッセージが Application Insights に送信されていることを確認できた。

f:id:griefworker:20210510151249p:plain

おわりに

Application Insights の使い方を詳しく解説してくれる書籍が欲しい。技術同人誌でもいい。

Xenoblade Definitive Edition

Rebuild.fm で評判を聞いて、いつかプレイしたいと思っていた『Xenoblade』が、Definitive Edition として Nintendo Switch で発売されていたので購入した。ゼノシリーズはゼノギアスゼノサーガをプレイしたことあるけど、どれも途中までしかやっていないので、クリアした最初のゼノシリーズになる。世界観的には、ゼノサーガよりはゼノギアスに近いかも。

アーツにスキルツリー、ジェムクラフトとシステムてんこ盛りで、慣れるまでだいぶ時間かかった。詰め込みすぎでは?と思わないでもない。ゼルダはあれだけ自由度あったけど、まだシンプルだったな。先にプレイしたのもあり、どうしてもゼルダ基準になってしまう。

物語は機神兵への復讐から始まり、巨神界と機神界の対決、最後には神との対決と、進むにつれ世界中を巻き込んでスケールアップしていく。本質的には、神というか、たった1人の人物に振り回された話だったな。メタな視点のシーンがあったので、上位世界とかあるのかもしれない。続編で触れられるだろうか。

そんなメインストーリーを進めつつも、プレイの大半は街やフィールドで片っ端からクエストの依頼を受けてクリアしていた。クエストの中には報酬で経験値が入るものがあり、物語終盤までは、しらみつぶしにクエストこなしていけば、自ずとレベル上がっていった。意識的にレベル上げしたのは最終章ぐらいだ。ゼルダほどどこでも行けるわけではないけど、ストーリー放置して、寄り道したり、今まで訪れた場所に再び行ける自由度はあった。

登場人物はシュルクやフィオルン、ラインら若者だけでなく、ダンバンでさえもまだ青く感じてしまうのは、自分の趣味嗜好から離れている故か、はたまた年か。皆真っ直ぐで眩しい。個人的には、フィオルンよりメリア派。マルチエンディングだったら、メリアの評価をひたすら上げただろうな。ただ、シュルクやフィオルン、ダンバンの攻撃性能が高くて、ラスボスまでほとんど力押しで倒せてしまったので、この3人は外せないか。

最初はシステムの複雑さに面食らって、自分に合わないと思ったものだけど、クリアした今では、名作の評判も納得。ゼノブレイド2やってもいいかなと思えた。

WCF の BasicHttpBinding を使ったセルフホストで HTTPS を構成してみた

はじめに

WCFHTTPS を構成できるか試すことになった。 しかも IIS を使わず、セルフホストで。

サービスコントラクトの定義

using System.ServiceModel;

namespace WcfHttpsSample.Shared
{
    [ServiceContract]
    public interface ISampleService
    {
        [OperationContract]
        string Echo(string message);
    }
}

WCF サービスの実装

using System;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using WcfHttpsSample.Shared;

namespace WcfHttpsSample.Server
{
    class Program
    {
        static void Main(string[] args)
        {
            var baseAddress = new Uri($"https://{Dns.GetHostEntry("").HostName}:8080");
            var host = new ServiceHost(
                typeof(SampleService),
                baseAddress);

            // https で待ち受けられるようにする
            var binding = new BasicHttpBinding();
            binding.Security.Mode = BasicHttpSecurityMode.Transport;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;

            host.AddServiceEndpoint(
                typeof(ISampleService),
                binding,
                "Sample");

            host.Open();

            Console.WriteLine("Enter で終了");
            Console.ReadLine();

            host.Close();
        }
    }

    public class SampleService : ISampleService
    {
        public string Echo(string message) => message;
    }
}

WCF クライアントの実装

using System;
using System.Net;
using System.Net.Security;
using System.ServiceModel;
using System.ServiceModel.Channels;
using WcfHttpsSample.Shared;

namespace WcfHttpsSample.Client
{
    class Program
    {
        static void Main(string[] args)
        {
            // 自己署名証明書を使って試す場合は証明書の検証をしない
            ServicePointManager.ServerCertificateValidationCallback =
                new RemoteCertificateValidationCallback((a, b, c, d) => true);

            var endpointAddress = new EndpointAddress(
                $"https://{Dns.GetHostEntry("").HostName}:8080/Sample");

            // https で通信できるようにする
            var binding = new BasicHttpBinding();
            binding.Security.Mode = BasicHttpSecurityMode.Transport;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;

            var channel = ChannelFactory<ISampleService>.CreateChannel(
                binding,
                endpointAddress);

            var reply = channel.Echo("Hello World!");
            Console.WriteLine(reply);

            ((IChannel)channel).Close();
            Console.WriteLine("Enter で終了");
            Console.ReadLine();
        }
    }
}

SSL 証明書をインポート

HTTP.sys から見られるよう、ローカルマシンの個人にインポートする。 ローカルマシンにインストールしておかないと、SSL 証明書をポート番号にバインドできない。 そのせいで数時間ハマった。

SSL 証明書をポート番号にバインド

このままだと、クライアントからアクセスした際に下記のような例外が発生する。

System.ServiceModel.CommunicationException: 'https://ホスト名:8080/Sample に対する HTTP 要求の発行中にエラーが発生しました。 この原因としては、HTTPS ケースの HTTP.SYS でサーバー証明書が正しく構成されていないこと、 またはクライアントとサーバーの間でセキュリティ バインドが整合していないことが考えられます。

下記のコマンドを実行して、SSL 証明書をポート番号にバインドする必要がある。

netsh http add sslcert ipport=0.0.0.0:8080 certhash=<証明書のサムプリント> appid={<適当な Guid>}

appid は適当な GUID で良いみたい。

実行結果

f:id:griefworker:20210423103301p:plain

おわりに

早く gRPC に移行したい。

Node のバージョン管理を nvs に移行した

Node のバージョン管理には nvm を使っていたけど、クロスプラットフォームnvs の存在を知り乗り換えた。

github.com

Windows では msi をダウンロードしてインストールするだけ。

Windows では PowerShell Core を使っているので、<ユーザー>\Documents\PowerShell\Microsoft.PowerShell_profile.ps1nvs のバージョン自動切り替え機能を有効にする設定を記述しておく。

nvs auto on

あとは、プロジェクトのディレクトリ内に .node-version を配置しておけば、シェルで移動したとき自動で Node のバージョンを切り替えてくれる。便利。