ぼくたちは勉強ができない(15)

祖母の真意を理解できたのと、成幸への想いを自覚してから、理珠はかなりキャラが変わったな。 見た目だけでなく言動も。 以前美容室でイメチェンしたときよりは断然良い。 思わせぶりな態度をとって成幸をからかう姿を見ると、人の気持ちが分からないと悩んでいた少女がよくぞここまで成長した、と感慨深くなる。

そして15巻でついに、受験の第1関門であるセンター試験へ。そのセンター試験でまさかの、成幸に最大のピンチが到来。 連載をリアルタイムで読んでいて、え?まじで?浪人しちゃうの?いやラブコメだし…。でも引き延ばしもありえるか。なんて考えがよぎってハラハラした。 そんな窮地の成幸を救ったのはうるか。 成幸に寄り添うその姿はまさにメインヒロインだった。

自分は真冬先生やあしゅみー先輩を気に入っているんだけど、さすがに成幸の相手に選ばれるのは三人娘の誰かなんだろうと思う。それなら、三人娘の中ではうるかを希望。5年越しの恋を実らせてほしい。

ぼくたちは勉強ができない 15 (ジャンプコミックスDIGITAL)

ぼくたちは勉強ができない 15 (ジャンプコミックスDIGITAL)

IssueHub – GitHub の Issues に特化した iOS/Android 用モバイルアプリ

IssueHub というモバイルアプリのソースコードGitHub で公開した。

github.com

IssueHub は、GitHub の Issues に特化したクライアントアプリ。 競合するのは CodeHub や GitHub 公式のモバイルアプリ(まだベータ版)辺りかな。 Issues 特化と Todo アプリっぽく操作できるのをコンセプトにし、差別化を試みている。

iOSAndroid の両方に対応。 ただし、App Store と Play Store で公開はしていない。

GitHub で無料ユーザーでもプライベートリポジトリが無制限に作れるようになってから、 Bitbucket にあった個人開発用のリポジトリを全部 GitHub に移行したけど、 GitHub のイシューを管理するちょうど良いアプリがなかった。 CodeHub は高機能過ぎてイシューを管理するだけにしてはオーバーキルなのと、 プライベートリポジトリはアプリ内課金しないと扱えなかったので、試したもののすぐに使うのを断念。 Bitbucket のときは自作の Bitissues という、これまた Bitbucket の Issues に特化したアプリを使っていて、 同じようなものが GitHub にも欲しいと思い、Bitissues 作ったときのノウハウがあるので作ることにした。 開発には Xamarin.Forms を採用。 技術的な話は長くなるので、別に記事を書こうと思う。

ソースコードを公開した理由は、GitHub 公式のモバイルアプリが出るから。 iOS 版がリリースできるくらいになって、Apple Developer の登録料を捻出するべきかどうしようかと迷っているタイミングで、 GitHub 公式モバイルアプリのニュースが飛び込んできて、そこでモチベーションが萎えてしまった。 毎年 Apple Developer の登録料を払うのは財布的に厳しいし、マネタイズもまぁ無理だろう。 この手の開発者向けアプリは、広告を貼ってもクリックされない。 GitHub のクライアントアプリなので、有料アプリとして売るのも気が引ける。 有料だったらみんな公式モバイルアプリの方を使うでしょ。 ユーザーは自分だけかもしれない。 ならば GitHubソースコードを公開して自分のポートフォリオに加えてしまえ、 という結論に至った。

ビルド手順は README に詳しく書くが、簡単に説明するなら次のようになる。

  1. GitHub の Developer settings の OAuth apps に登録し、Client ID と Client Secret を入手する。
    • リダイレクト URL は issuehub://oauth2redirect
  2. IssueHub のソースコードをクローンし、Visual Studio 2019 または Visual Studio for Mac で IssueHub.sln を開く。
  3. IssueHub.csproj があるディレクトリに下記の secrets.json を作成し、ビルドして実機にデプロイ。
{
    "GitHubClientId": "{Your GitHub Client Id HERE}",
    "GitHubClientSecret": "{Your GitHub Client Secret HERE}",
    "GitHubAuthorizationCallbackUrl": "issuehub://oauth2redirect",
     "AndroidDataScheme": "issuehub",
      "AndroidDataPath": "/oauth2redirect"
}

Xamarin 製アプリを実機にデプロイするのは、自分でも新規にプロジェクトを作成すると毎回つまずく。 使うまでの道のりは険しい。 マネタイズできそうなアプリを他に作ることができて、 Apple Developer の登録料を払ったら、 IssueHub もついでに登録する、かもしれない。

WEB+DB PRESS Vol.114

特集1 AWS/GCP セキュア化計画

本業でクラウドサービスを開発することが増えてきて、 今までは Azure App Service や Azure SQL Database といった、SaaS やPaaSにのっかっていたが、 これからはガッツリと VPC を構築しそう。 セキュリティは最重要。 本格的な勉強を始めるところだったので、 できれば Microsoft Azure も扱って欲しかった。 Microsoft Azure ならこうだろうな、と読み替えながら参考にしよう。

特集2 作って学ぶ WebAuthn

WebAuthn の名前だけは mozaic.fm で聞いていたが、 どういった技術なのかまでは把握していなかった。 ちょうど本業の方で認証基盤を開発をすることになりそうで、 要件として多要素認証の対応は求められており、 2要素目として WebAuth は有力だな。 特集のサンプルは Java だったが、 WebAuthn のライブラリは C# にもあるみたいなので、 ASP.NET Core 製のWebアプリに組み込めるか調べてみよう。

特集3 Kyash 開発ノウハウ大公開

カード発行・決済サービスとなると、 最上級のセキュリティが求められるので、 その対応の大変さをうかがい知れた。

Kyash Direct の設計では、 クリーンアーキテクチャ・マイクロサービス・CQRS・Sagaパターン・イベントソーシングと、 盛りだくさんの攻めたアーキテクチャで、 かなりチャレンジングなことやっていて熱かった。 部分的に Go のサンプルコードも載っていて、 貴重な実例として参考になった。

過去の自分へコードレビュー

最近はコンパイラや OS といった低レイヤーにも興味出てきたので、 まずはコンパイラや CPU の気持ちを考えたコードが書けるようになりたい。 最近の C# もパフォーマンス志向だし。

コードレビューで強い言葉を使う人は自分の周りにいないけど、 言われた凹むのは容易に想像できる。 書いた人の批判ではなくコードの批判だとはいえ、 そのコードはいわば自分の作品なわけで、 分けて受け止めるのは難しい。 そこまでドライにはなれないかな。 だから、自分がレビューするときは強い言葉は避けている。 強い言葉を使わずとも、より良いコードに導けるようになりたい。

まるきん亭

木の葉モールの隣にある『まるきん亭』がラーメンウォーカーに載っていたので行ってみた。食べログや Retty の評価はまぁまぁなので、存在は認識していたけど、今までスルーしていた。まさかラーメンウォーカーに載るくらいの店だったとはね。

ラーメンとチャーハンと餃子ぜんぶ食べたい欲張り向けに、『スペシャルセット』というのがあったので注文。 ラーメンは博多ラーメン系のような、白濁が薄めのあっさりした豚骨ラーメンだった。

半チャーハンはパラパラに仕上がっていて、味付けも丁度いい塩梅。 ラーメンとチャーハンの相性の良さは間違い無いので、 一緒に注文しない理由があるだろうか。いや無い。

餃子は皮がしっかり焼かれてパリッっとしていて、 中は具材から滲み出たスープがたっぷりでジューシー。 これは白米が進みそうだ。 それでも白米よりチャーハンが食べたいので悩ましい。

ラーメンに半チャーハンに餃子と、食べたかったもの全部のセットが存在していて満足した。他の、チャーハンと餃子をメニューに載せている店は見習って欲しい。このような郊外にある店でもクオリティが高くて、福岡市の食のレベルの高さがうかがえる。

コードファースト ASP.NET Core gRPC ストリーミング編

はじめに

先日、WCF から gRPC に移行するときの手段として、.proto ファイルを書くのではなく、protobuf-net.Grpc を使って C# のコードから gRPC のクライアントとサーバーを動的に生成する方法を紹介した。コードファースト ASP.NET Core gRPC と命名

tnakamura.hatenablog.com

試したのは 1 リクエストで 1 レスポンスを返す、gRPC でいうところの Unary 方式。 WCF には使いにくいけど Streaming がある。 マジ使いにくいけど。 一方で、gRPC には Client Streaming、Server Streaming、Duplex Streaming がある。 コードファーストで gRPC をやる場合に、Streaming はどうやるのか試してみた。

サービスコントラクトとデータコントラクト

.NET Core ライブラリプロジェクトで、WCF のときみたいにサービスコントラクトとデータコントラクトを定義する。 Streaming は IAsyncEnumerable<T> で表現。 Client Streaming は引数、Server Streaming は返り値、Duplex Streaming はその両方で型が IAsyncEnumerable<T> になる。

using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Threading.Tasks;

namespace CodeFirstGrpc.Shared
{
    [ServiceContract]
    public interface IGreetingService
    {
        ValueTask<HelloReply> HelloAsync(HelloRequest request);

        IAsyncEnumerable<HelloReply> ServerStreamingHelloAsync(HelloRequest requests);

        IAsyncEnumerable<HelloReply> DuplexStreamingHelloAsync(IAsyncEnumerable<HelloRequest> requests);
    }

    [DataContract]
    public class HelloRequest
    {
        [DataMember(Order = 1)]
        public string Name { get; set; }
    }

    [DataContract]
    public class HelloReply
    {
        [DataMember(Order = 1)]
        public string Message { get; set; }
    }
}

gRPC サービスの実装

async なメソッド内での yield returnawait foreach を使って、Server Streaming と Duplex Streaming を実装。 C# らしい書き味で筋が良いと感じる。

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using CodeFirstGrpc.Shared;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ProtoBuf.Grpc.Server;

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

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCodeFirstGrpc();
        }

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

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreetingService>();
            });
        }
    }

    public class GreetingService : IGreetingService
    {

        public ValueTask<HelloReply> HelloAsync(HelloRequest request)
        {
            return new ValueTask<HelloReply>(new HelloReply
            {
                Message = $"Hello, {request.Name}.",
            });
        }

        public IAsyncEnumerable<HelloReply> ServerStreamingHelloAsync(HelloRequest request) =>
            ServerStreamHelloAsyncImpl(request, default);

        async IAsyncEnumerable<HelloReply> ServerStreamHelloAsyncImpl(
            HelloRequest request,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            for (var i = 0; i < 5; i++)
            {
                if (!cancellationToken.IsCancellationRequested)
                {
                    yield return new HelloReply
                    {
                        Message = $"Hello, {request.Name}.",
                    };
                    await Task.Delay(TimeSpan.FromSeconds(1));
                }
            }
        }

        public IAsyncEnumerable<HelloReply> DuplexStreamingHelloAsync(IAsyncEnumerable<HelloRequest> requests) =>
            DuplexStreamHelloAsyncImpl(requests, default);

        async IAsyncEnumerable<HelloReply> DuplexStreamHelloAsyncImpl(
            IAsyncEnumerable<HelloRequest> requests,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await foreach (var request in requests)
            {
                await Task.Delay(TimeSpan.FromSeconds(1));
                if (!cancellationToken.IsCancellationRequested)
                {
                    yield return new HelloReply
                    {
                        Message = $"Hello, {request.Name}.",
                    };
                }
            }
        }
    }
}

gRPC クライアントの実装

Server Streaming を await foreach で処理できるのが実に自然。 Duplex Streaming の Client から Server に送る方、つまりは Client Streaming だけど、 IAsyncEnumerable<T> を作って渡すところが馴れない。 自分が IAsyncEnumerable<T> を触り始めたばかりなのもあるが、 もうちょっと上手く書けないか試行錯誤の余地がありそう。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CodeFirstGrpc.Shared;
using Grpc.Net.Client;
using ProtoBuf.Grpc.Client;

namespace CodeFirstGrpc.Client
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = channel.CreateGrpcService<IGreetingService>();

            Console.WriteLine("=== Unary Test ===");
            var reply = await client.HelloAsync(new HelloRequest
            {
                Name = "Kubo"
            });
            Console.WriteLine(reply.Message);

            Console.WriteLine("=== Server Streaming Test ===");
            try
            {
                await foreach (var x in client.ServerStreamingHelloAsync(new HelloRequest { Name = "Minamino" }))
                {
                    Console.WriteLine(x.Message);
                }

            }
            catch (TaskCanceledException) { }

            Console.WriteLine("=== Duplex Streaming Test ===");
            try
            {
                await foreach (var x in client.DuplexStreamingHelloAsync(GenerateRequests()))
                {
                    Console.WriteLine(x.Message);
                }

            }
            catch (TaskCanceledException) { }

            Console.ReadLine();
        }

        static async IAsyncEnumerable<HelloRequest> GenerateRequests()
        {
            yield return new HelloRequest { Name = "Douan" };
            await Task.Delay(TimeSpan.FromSeconds(1));
            yield return new HelloRequest { Name = "Osako" };
            await Task.Delay(TimeSpan.FromSeconds(1));
            yield return new HelloRequest { Name = "Nakajima" };
            await Task.Delay(TimeSpan.FromSeconds(1));
            yield return new HelloRequest { Name = "Sibasaki" };
            await Task.Delay(TimeSpan.FromSeconds(1));
            yield return new HelloRequest { Name = "Tomiyasu" };
        }
    }
}

実行

Streaming の様子がわかるように、アニメーション GIF を作成してみた。

f:id:griefworker:20191217115009g:plain

まとめ

protobuf-net.Grpc は、gRPC の Client Streaming・Server Streaming・Duplex Streaming を、 IAsyncEnumerable<T> を使って表現していて、実に C# らしくて筋が良いと感じた。

Unary だけでなく Streaming が思いのほか簡単に実装できてしまい、 WCF から gRPC に移行するための道筋がだいぶ見えてきた。 あとはヘッダーで認証に関する情報をやり取りできるかどうかを試せば、 調査終了にできそうだ。

3月のライオン(15)

学園祭フィナーレのファイヤーパーティで、零がひなたに気持ちを伝える場面は、もう眩しすぎて直視出来ない。ていうか、今まで好きって伝えてなかったんかい。零の気持ちがひなたに伝わって、肝心なところのシーンは意図的に省かれていたけど、その後 2 人で離れたところからファイヤーパーティを眺める姿から、おそらくそういうことなんだろうな。

零が戦うことが増えてきたA級は、人間性を捧げなきゃ生きていけないような、 天才や怪物の住む世界。 棋戦でのアウトプットに対し、棋譜や研究のインプットが追いつかない、黒い部屋と自身で表現していた状態に陥りやすくなっている零だが、現状から抜け出すには、リソースのほとんどを将棋に費やす必要があるんだろう。

ただ、生きていくために将棋を選んだ零が、ようやく手にした居場所を犠牲にしてタイトルを獲得しても、はたしてハッピーエンドと言えるのかどうか。 そんな零に、ようやく手にした居場所を手放してはいけないと訴えた先生は教師の鑑だな。 零の数少ない理解者。 あかりの相手ということは零の家族になるかもしれないわけなので、 案外、 相応しいのかもしれないと思えた。 総合力的な意味で。

3月のライオン 15 (ヤングアニマルコミックス)

3月のライオン 15 (ヤングアニマルコミックス)

謹賀新年

今年の抱負として、またOKRを設定したいと思う。OKR は次の通り。

  • 個人開発
    • Write code every day
    • プロダクトを 2 つ以上リリースする
  • 英語
    • Study english every day
  • コミュニティ
    • Fukuoka.NET
    • JXUG
    • ふくあず

個人開発は引き続き Write code every day を続ける。 プロダクトは今年こそ 2 つ以上リリースしたい。 1 つは 1 月中に OSS として公開予定で、 それとは別にもう 1 つ作っているやつがあるので、 それはリリースする。絶対に。

英語は何か教材を見つけて、勉強する習慣をつけたいと思う。 Write code every day ならぬ、Study english every day。 Duolingo とか良さそうだ。

コミュニティ活動も今年こそ再開したい。 子供が大きくなってきて、 自分が子供の面倒見て奥さんがライブとか飲み会に出かけられるくらいには余裕出来てきたので、 自分の方も勉強会に参加していこうかなと思う。 自分の主戦場が C# とか Azure とか Microsoft テクノロジーだから、 Fukuoka.NET や JXUG やふくあずに参加しようかなと考えている。 ひとまず月1回くらいのペースで。

という感じで 2020 年もやっていくぞ。