20200314のGoに関する記事は7件です。

AOJ(Go)でよくわからんエラーが出た

Aizu online judgeのgo言語での提出において、
17 0 37 0.00user 0.00system 0:00.00elapsed 50%CPU (0avgtext+0avgdata 1120maxresident)k 0inputs+8outputs (0major+615minor)pagefaults 0swaps
のようなエラーが出た時の対処法を書いておきます。

1つ目(fmtパッケージを使わずそのままprint)

僕の環境ではそのままprintを書いてもコンパイルが通ってしまったので気付きにくかったのですが、
追記(訂正):print関数は標準のエラーに書き込まれるそうです。AOJでは標準出力を答えとして受け取るため、ビルドでエラーが発生されるのではないかと考えられます。
Print("hoge")などと書いてしまうと上記のようなエラーが出ることがあります。
代わりに、
fmt.Print("hoge")と書けば、上記のエラーを避けることができます。


まだ1つ目までしかかけていませんが、また違う原因で同じようなエラーが発生した場合は追記していこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語と螺旋本で学ぶデータ構造とアルゴリズム(その0)

はじめに

「プログラミングコンテスト攻略のためのアルゴリズムとデータ構造」(通称:螺旋本)をGo言語で解いていくシリーズです。個人の勉強が目的で、主な目標は以下のとおりです。

  • Go言語に慣れる
  • アルゴリズムとデータ構造の理解を深める
  • 上記を理解した上で、競技プログラミング用のライブラリを作成する

なお、Go言語の基本文法については特に細かく書かないつもりです。

標準入出力

競技プログラミングをやる上で避けては通れないのが標準入出力だと思うので、今回はここについてまとめたいと思います。

簡単な問題であれば前回の記事でご紹介したfmt.Scan()及びfmt.Println()で十分なのですが、入出力の数が多くなると問題が発生するようなので、今回は別の方法を紹介します。

標準入力

bufio.Scannerを使います。
使い方としては、まずScan()で1行を読み取ります。Scan()は標準入力があればtrue, なければfalseを返します。その後にText()で文字列を返すことができます。

package main

import (
    "bufio"
    "fmt"
    "os"
)

// Scannerを生成
var sc = bufio.NewScanner(os.Stdin)

func main() {
    var a string
    if sc.Scan() {
        a = sc.Text()
    }

    fmt.Println(a)
}

整数や小数で受け取る場合はstrconv.Atoi()strconv.ParseFloat()で変換する必要があります。

これらはよく使う処理なので、関数にしておきます。なお、競技プログラミング用なので、戻り値(エラー、真偽値)の確認は割愛しています。

func next() string {
    sc.Scan()
    return sc.Text()
}

func nextInt() int {
    i, _ := strconv.Atoi(next())
    return i
}

func nextFloat() float64 {
    f, _ := strconv.ParseFloat(next(), 64)
    return f
}

複数行から受け取る場合の関数も作成しておきます。戻り値はスライスにしておきます。

func nextInts(n int) []int {
    ret := make([]int, n)
    for i := 0; i < n; i++ {
        ret[i] = nextInt()
    }
    return ret
}

func nextFloats(n int) []float64 {
    ret := make([]float64, n)
    for i := 0; i < n; i++ {
        ret[i] = nextFloat()
    }
    return ret
}

func nextStrings(n int) []string {
    ret := make([]string, n)
    for i := 0; i < n; i++ {
        ret[i] = next()
    }
    return ret
}

今までは行区切りで受け取っていましたが、競技プログラミングではスペース区切りの入力が与えられることも多いです。bufio.Scanner.Split(bufio.ScanWords)を事前に実行しておくと、これに対応することができます(Scan()が改行までではなく、スペースまでを読み取るようになります)。また、bufio.Scanner.Split(bufio.ScanLines)を実行すると行区切りに戻すことができます。

いつでも切り替えができるように、これも関数にしておきます。

func splitSpace() {
    sc.Split(bufio.ScanWords)
}

func splitLine() {
    sc.Split(bufio.ScanLines)
}

標準出力

bufio.Writerを使います。
これはfmt.Fprintlnfmt.Fprintfと併せて使います。fmt.Printlnfmt.Printfと使い方はほとんど同じですが、ここでは第一引数にbufio.Writerを与えてから出力するデータやフォーマットを与えます。
また、最後に必ずbufio.Writer.Flush()を実行する必要があります。忘れないようにdeferで先に書いておくのがよいかもしれないです。

package main

import (
    "bufio"
    "fmt"
    "os"
)

// Writerを生成
var wr = bufio.NewWriter(os.Stdout)

func main() {
    defer wr.Flush()
    fmt.Fprintln(wr, "Hello, World!")
}

参考

まとめ

今回は競技プログラミングの問題を解く上で必須と思われる標準入出力についてまとめました。
次回からは実際に問題を解いていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

grpc-gatewayでgRPCサーバをREST APIで疎通できるようにする

はじめに

今回はgrpc-gatewayを用いて、gRPCサーバをREST APIに対応させてみます。
成果物はこちらのgithubにおいてありますので合わせてご確認ください。

grpc-gatewayとは

grpc-gatewayのリポジトリには

The grpc-gateway is a plugin of the Google protocol buffers compiler protoc. It reads protobuf service definitions and generates a reverse-proxy server which 'translates a RESTful HTTP API into gRPC. This server is generated according to the google.api.http annotations in your service definitions.

https://github.com/grpc-ecosystem/grpc-gateway
つまり、gRPCサーバで定義しているprotobufを読み取り、RESTful HTTP APIをgRPCに変換するリバースプロキシサーバの役割を担うことができる、というものです。

image.png

gRPCサーバの作成

なにはともあれ、grpc-gatewayを試すにはgRPCサーバが必要ですので、まずはそれをサクッと作成します。
まずはprotoファイルの定義から

service.proto
syntax = "proto3";
package proto;

service SayHello {
  rpc Echo(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
  string userName = 1;
}

message HelloResponse {
  string message = 1;
}

protocを使って、.protoファイルをビルドして、.pb.goファイルを作成してください。

リクエストにuserNameを指定するとmessageを返すEchoメソッドを作成しました。

server/main.go
type helloService struct{}

func (hs *helloService) Echo(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    return &pb.HelloResponse{
        Message: "hello, " + req.UserName,
    }, nil
}

func Start(port string) {
    listen, err := net.Listen("tcp", ":"+port)
    if err != nil {
        log.Fatalln(err)
    }
    log.Printf("server listen: " + listen.Addr().String())
    server := grpc.NewServer()
    pb.RegisterSayHelloServer(server, &helloService{})

    if err := server.Serve(listen); err != nil {
        log.Fatalln(err)
    }
    return
}

このEchoメソッドはusenameに対して hello, username を返すように設定します。
Start関数でサーバ起動できます。

これでgRPCサーバは作成できました。
実際に疎通確認してみてもいいでしょう。それ用のクライアントサーバも作成します。
ここから先はgrpc-gatewayには必要ありません。

client/main.go
func Echo(conn *grpc.ClientConn, name string) {
    client := pb.NewSayHelloClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    resp, err := client.Echo(ctx, &pb.HelloRequest{UserName: name})
    if err != nil {
        log.Fatalln(err)
    }
    log.Printf("message: %s\n", resp.Message)
}

func Call(port string) {
    addr := fmt.Sprintf("localhost:" + port)

    conn, err := grpc.Dial(addr, grpc.WithInsecure())
    if err != nil {
        log.Fatalln(err)
    }
    Echo(conn, "sakas")
    defer conn.Close()
}

Call関数でEcho(conn, "sakas")をすることでusername=sakasでEchoメソッドにリクエストを投げます。

main.go
func main() {
    serverPort := "19003"
    go func() {
        server.Start(serverPort)
    }()
    client.Call(serverPort)
    return
}

試しに起動させると疎通できていることがわかると思います。
もちろん今はgrpc-gatewayを作成していませんので、REST APIでは疎通できません。

grpc-gatewayの作成

ここから本題です。
このgRPCサーバにgrpc-gatewayでREST API対応していきましょう。

protoファイルの修正

protoファイルを以下のように変更します。

service.proto
syntax = "proto3";
package proto;

import "google/api/annotations.proto";

service SayHello {
  rpc Echo(HelloRequest) returns (HelloResponse) {
    option (google.api.http) = {
        get: "/echo"
    };
  }
}

message HelloRequest {
  string userName = 1;
}

message HelloResponse {
  string message = 1;
}

gRPC stubの生成

その上で、pb.goファイルを生成していきます。

grpc-gateway対応したので、import "google/api/annotations.proto";がパスを認識できるように、grpc-gatewayのパスを追加でincludeさせます。

公式のREADMEには

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --ruby_out=. \
  path/to/your_service.proto

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --plugin=protoc-gen-grpc=grpc_ruby_plugin \
  --grpc-ruby_out=. \
  path/to/your_service.proto

と書かれていましたが、自分はGO111MODULE=onでgrpc-gatewayをimportしたので、gomoduleでインストールされるパスを指定しました。
また、自分はbrewでprotobufを入れたため、protobufをimportしたいときに追加でIPATHを指定しています。

 protoc -I/usr/local/include -I. \
  -I$GOPATH/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.12.2/third_party/googleapis \
  -I/usr/local/opt/protobuf/include \
   --go_out=plugins=grpc:. \
  ./proto/service.proto

grpc-gatewayの生成

リバースプロキシを生成します。
こちらも、公式READMEでは以下のようになっていましたが、

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=logtostderr=true:. \
  path/to/your_service.proto

--pluginでprotoc-gen-grpc-gatewayの明記
protobufのIPATH追加
を変更しました。

  protoc -I/usr/local/include -I. \
  -I/usr/local/opt/protobuf/include \
  -I$GOPATH/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.12.2/third_party/googleapis \
  --plugin=protoc-gen-grpc-gateway=$GOPATH/bin/protoc-gen-grpc-gateway \
--grpc-gateway_out=logtostderr=true:. \
./proto/service.proto

これでpb.gw.goファイルが生成できます。

gatewayサーバの作成

gateway/main.go
func run(serverPort string, gwPort string) error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}
    endpoint := fmt.Sprintf(":" + serverPort)
    err := gw.RegisterSayHelloHandlerFromEndpoint(ctx, mux, endpoint, opts)
    if err != nil {
        return err
    }
    log.Printf("gateway port:" + gwPort)
    log.Printf("server listen: " + serverPort)
    return http.ListenAndServe(":"+gwPort, mux)
}

func Start(serverPort string, gwPort string) {
    flag.Parse()
    defer glog.Flush()
    if err := run(serverPort, gwPort); err != nil {
        glog.Fatal(err)
    }
}

mux := runtime.NewServeMux() でhttpヘッダ←→gRPC contextの変換をしてくれます。
NewServeMux()の中身を追っていくと

func RegisterSayHelloHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SayHelloClient) error {

    mux.Handle("GET", pattern_SayHello_Echo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
        ...
        resp, md, err := request_SayHello_Echo_0(rctx, inboundMarshaler, client, req, pathParams)
        ...
        forward_SayHello_Echo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
        ...
    }
    return nil
}

この記述があり、GETリクエストをhandler登録していて、内部ではresponseを取得してhttpで返却していることがわかります。

これだけの変更でgrpc-gatewayが完成します。

疎通確認

実際にREST APIを投げてみましょう

main.go
func main() {
    serverPort := "19003"
    gwPort := "50000"
    go func() {
        server.Start(serverPort)
    }()
    gateway.Start(serverPort, gwPort)
    return
}

gRPCサーバのポートを19003番、gatewayのポートを50000番で起動させます。

❯ curl -XGET "localhost:50000/echo?userName=sakas1231"
{"message":"hello, sakas1231"}

無事、疎通確認ができました:clap:?:clap:?

まとめ

gRPCサーバさえあれば、導入しきい値は結構低いのではないかなと思っています。かなり簡単にgrpc-gatewayの疎通確認ができました。

再掲ですが、githubにコードを公開しています:bow:
https://github.com/KatsuyaAkasaka/grpc-gateway-sample

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【GO/DDD】レイヤードアーキテクチャの整理

もうすでに詳しい記事が色々ありますが、自分の知識の整理として書いていきます。

レイヤードアーキテクチャ

下記の図の構成になります。
Presentation層(handler)→Application層(usecase)→Domain層(domain)→Infrastructure層(infrastructure)の順番で、一つの方向に依存して構成になっています。
またそれぞれの層にinterface, structを定義しています。

image.png

Presentation層

http通信などを書きます。

Application層

CRUDを行う場所。
MVCでいうViewとControllerの間にいるやつ。

Domain層

ビジネスロジックを書きます。
具体的には下記の例がわかりやすい。
ドメインモデルで実行されるべきロジックですね。

・限定共有投稿、公開を切り替えられる(ただし、一度公開した記事を非公開(=限定共有)にはできない)
・最終更新日時から1年以上経過すると「この記事は最終更新日から○年以上が経過しています。」と表示される
ドメインモデル、ドメインロジックとは何かをコードを交えて考えてみる - Qiitaより引用

ビジネスロジック (business logic)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

Infrastructure層

DB、メモリ操作を書きます。
具体的には、SQLとかですね。

MVCでいうと

Railsを例にとると、下記のようになるようです。

名前
ユーザインタフェース層 (View) views view_objects
アプリケーション層 (Controller) controllers observers decorators parameters
ドメイン層 (Model) models mailers callbacks validators policies queries services value_objects factories

中規模Web開発のためのMVC分割とレイヤアーキテクチャ - Qiitaより引用

これとかもめちゃくちゃ参考になった。
てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ! - Qiita

フォルダ構成

現状のフォルダ構成をtreeコマンドで出力してみました。

├── app
│   ├── application
│   │   └── usecase
│   │       ├── user.go
│   ├── domain
│   │   ├── model
│   │   │   ├── user.go
│   │   ├── repository
│   │   │   ├── user.go
│   │   └── service
│   │       ├── user.go
│   ├── infrastructure
│   │   ├── database.go
│   │   └── persistence
│   │       ├── user.go
│   └── presentation
│       ├── handler
│       │   ├── request.go
│       │   └── response.go

用語の整理

ドメイン

ドメインモデルとは、今あなたが扱っている業務ドメインに特化した、ソフトウェアモデルのことだ。
ドメインを反映したモデルとは、ドメインを構成する物・概念・振る舞い・関係性を表現 したものです。
わかる!ドメイン駆動設計 ~もちこちゃんの大冒険~【C91新刊】 - TechBooster - BOOTHより引用

そしてこのレイヤードアーキテクチャは、DDDの中では戦術的設計に該当します。

依存関係

上記のレイヤードアーキテクチャのような構成を取り入れると何が嬉しいのか。
それは依存関係が単一の方向になり、どちらか一方の変更でもう一方が壊れるリスクを無くしてくれます。
つまり変更に強くなるということに集約されると思います。

理想

image.png
プログラムの依存関係とモジュール構成のこと - Qiitaより引用

個人的に注意すること

  • 依存性の大きい要素は使われる場所を局所的にする
  • 依存性の小さい要素は多くの場所から使われるようにする
  • 各層でも依存関係が単一になっていること

プログラムの依存関係とモジュール構成のこと - Qiitaより引用

interface

機能を約束してくれて、異なる構造体(中身が全く異なっていたとしても)でも問題なく使えます。
Goのinterfaceがわからない人へ - Qiita
インタフェースの実装パターン #golang - Qiita

その他参考になる記事

Goのpackage構成と開発のベタープラクティス - Tech Blog - Recruit Lifestyle Engineer
レイヤードアーキテクチャの視点 - Qiita
Golang APIレイヤードアーキテクチャで組んでみた。 - Qiita
MVC、3 層アーキテクチャから設計を学び始めるための基礎知識 - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gRPC-Web + React Hooks + Go でリアルタイムチャットをつくる

概要

分散システムを学ぶうちにgRPCに興味を持った。きくところによると、gRPC-Webというものもあるらしい。

この記事では、gRPC-Web + React Hooks + Goを用いてリアルタイムチャットを作りながら、実装の流れを書いてみようと思う。

コードだけ見たいという方は↓へ、

gRPC-Webってなんやねん?という方は↓へどうぞ!

全体像

サービスの全体像は以下のようになる。

Untitled Diagram.png

タイトルのとおり、ReactクライアントからgRPC-WebでGoサーバーと通信するチャットサービスだ。

デモはこんな感じ↓
(リアルタイムですべてのクライアントにメッセージが配信される)

Image from Gyazo

開発

Protocol Buffersの定義

まずはProtocol Buffersのインターフェイスを定義する。

前述のチャットサービスをつくるにあたって、以下のようなインターフェイスを作成する。

syntax = "proto3";

import "google/protobuf/empty.proto";

package messenger;

service Messenger {
  rpc GetMessages (google.protobuf.Empty) returns (stream MessageResponse) {}
  rpc CreateMessage (MessageRequest) returns (MessageResponse) {}
}

message MessageRequest {
  string message = 1;
}

message MessageResponse {
  string message = 1;
}
  • CreateMessageはメッセージの投稿で、リクエストとレスポンスの型を定義している。
  • GetMessagesでメッセージの受信をする。returns (stream MessageResponse)とすることでストリームを返すコードを生成できる。

gRPCのコードを自動生成

ここからgRPCのコードを生成する。

GoバックエンドとTypeScriptフロントエンドのコードを生成するために、protocに加え、protoc-gen-goprotoc-gen-grpc-webをインストールする。

もちろんローカル環境には入れたくないのでコンテナをつくっていく。

FROM golang:1.14.0

ENV DEBIAN_FRONTEND=noninteractive

ARG PROTO_VERSION=3.11.4
ARG GRPCWEB_VERSION=1.0.7

WORKDIR /proto

RUN apt-get -qq update && apt-get -qq install -y \
  unzip

RUN curl -sSL https://github.com/protocolbuffers/protobuf/releases/download/v${PROTO_VERSION}/\
  protoc-${PROTO_VERSION}-linux-x86_64.zip -o protoc.zip && \
  unzip -qq protoc.zip && \
  cp ./bin/protoc /usr/local/bin/protoc && \
  cp -r ./include /usr/local

RUN curl -sSL https://github.com/grpc/grpc-web/releases/download/${GRPCWEB_VERSION}/\
  protoc-gen-grpc-web-${GRPCWEB_VERSION}-linux-x86_64 -o /usr/local/bin/protoc-gen-grpc-web && \
  chmod +x /usr/local/bin/protoc-gen-grpc-web

RUN go get -u github.com/golang/protobuf/protoc-gen-go
docker-compose.yml
version: '3'
services:
  proto:
    command: ./proto/scripts/protoc.sh
    build:
      context: .
      dockerfile: DockerfileProto
    volumes:
      - .:/proto
protoc.sh
#!/bin/sh

set -xe

SERVER_OUTPUT_DIR=server/messenger
CLIENT_OUTPUT_DIR=client/src/messenger

protoc --version
protoc --proto_path=proto messenger.proto \
  --go_out=plugins="grpc:${SERVER_OUTPUT_DIR}" \
  --js_out=import_style=commonjs:${CLIENT_OUTPUT_DIR} \
  --grpc-web_out=import_style=typescript,mode=grpcwebtext:${CLIENT_OUTPUT_DIR}

これでdocker-compose upするとコードが自動生成される。

バックエンドの実装

バックエンドの実装をする。

ひとつ前のステップで、以下のようなインターフェイスが自動生成されているので、これを組み合わせて実装をしてゆく。

// MessengerServer is the server API for Messenger service.
type MessengerServer interface {
    GetMessages(*empty.Empty, Messenger_GetMessagesServer) error
    CreateMessage(context.Context, *MessageRequest) (*MessageResponse, error)
}

まずはサーバーの雛形を書いてみる。下記のTODOを埋めていくような流れだ。

package main

import (
    "context"
    "log"
    "net"

    "github.com/golang/protobuf/ptypes/empty"
    pb "github.com/okmttdhr/grpc-web-react-hooks/messenger"

    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

const (
    port = ":9090"
)

type server struct {
    pb.UnimplementedMessengerServer
    requests []*pb.MessageRequest
}

func (s *server) GetMessages(_ *empty.Empty, stream pb.Messenger_GetMessagesServer) error {
  // TODO: 実装
}

func (s *server) CreateMessage(ctx context.Context, r *pb.MessageRequest) (*pb.MessageResponse, error) {
  // TODO: 実装
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterMessengerServer(s, &server{})
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

まずはメッセージの投稿だが、シンプルに、配列に時刻付きのメッセージを詰め込んでゆく形にした。

func (s *server) CreateMessage(ctx context.Context, r *pb.MessageRequest) (*pb.MessageResponse, error) {
    log.Printf("Received: %v", r.GetMessage())
    newR := &pb.MessageRequest{Message: r.GetMessage() + ": " + time.Now().Format("2006-01-02 15:04:05")}
    s.requests = append(s.requests, newR)
    return &pb.MessageResponse{Message: r.GetMessage()}, nil
}

次にメッセージの取得だ。一度目のアクセスで保持しているメッセージを流し、それ以降は、新しいメッセージを検知したときのみデータを送るようにしている。

func (s *server) GetMessages(_ *empty.Empty, stream pb.Messenger_GetMessagesServer) error {
    for _, r := range s.requests {
        if err := stream.Send(&pb.MessageResponse{Message: r.GetMessage()}); err != nil {
            return err
        }
    }

    previousCount := len(s.requests)

    for {
        currentCount := len(s.requests)
        if previousCount < currentCount {
            r := s.requests[currentCount-1]
            log.Printf("Sent: %v", r.GetMessage())
            if err := stream.Send(&pb.MessageResponse{Message: r.GetMessage()}); err != nil {
                return err
            }
        }
        previousCount = currentCount
    }
}

これでバックエンドの実装ができた。

フロントエンドの実装

次に、Web側の実装を行う。

まずはgRPCと通信を行うためのクライアントをつくる。MessengerClientが自動生成されているので、以下のように使うことができる。(messenger/*が自動生成)。

import { MessengerClient } from "messenger/MessengerServiceClientPb";

export type GRPCClients = {
  messengerClient: MessengerClient;
};

export const gRPCClients = {
  messengerClient: new MessengerClient(`http://localhost:8080`)
};

これを以下のように使うと、メッセージの受信ができる。

import { Empty } from "google-protobuf/google/protobuf/empty_pb";

const stream$ = client.getMessages(new Empty());
// イベントは`data`以外にも、`error`、`status`、`end`が生成される。
stream$.on("data", m => {
  console.log(m)
});

実際はhooksの中で使うので、以下のようなコードとなる。

import { Empty } from "google-protobuf/google/protobuf/empty_pb";
import { useState, useEffect } from "react";
import { MessengerClient } from "messenger/MessengerServiceClientPb";

export const useMessages = (client: MessengerClient) => {
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    const stream$ = client.getMessages(new Empty());
    stream$.on("data", m => {
      setMessages(state => [...state, m.getMessage()]);
    });
  }, [client]);

  return {
    messages
  };
};

messagesをstateとして持ち、ストリームからデータを受信するたびにmessagesを更新している。

これを表示するコンポーネントは以下のようになる。

import React from "react";

type Props = {
  messages: string[];
};

export const Messages: React.FC<Props> = ({ messages }) => {
  return (
    <div>
      {messages.map(m => (
        <div key={m}>{m}</div>
      ))}
    </div>
  );
};

コンポーネントからはmessagesだけを見ることで、gRPCのロジックを切り離すことができる。(hooksを呼び出す箇所は後述)。

メッセージの投稿は以下のようにgRPCのコードを利用できる。

import { MessageRequest } from "messenger/messenger_pb";

const req = new MessageRequest();
req.setMessage(message);
client.createMessage(req, null, res => console.log(res));

同じようにhooksで使ってゆく。

import { MessageRequest } from "messenger/messenger_pb";
import { useState, useCallback, SyntheticEvent } from "react";
import { MessengerClient } from "messenger/MessengerServiceClientPb";

export const useMessageForm = (client: MessengerClient) => {
  const [message, setMessage] = useState<string>("");

  // メッセージ入力欄
  const onChange = useCallback(
    (event: SyntheticEvent) => {
      const target = event.target as HTMLInputElement;
      setMessage(target.value);
    },
    [setMessage]
  );

  // メッセージ投稿
  const onSubmit = useCallback(
    (event: SyntheticEvent) => {
      event.preventDefault();
      const req = new MessageRequest();
      req.setMessage(message);
      client.createMessage(req, null, res => console.log(res));
      setMessage("");
    },
    [client, message]
  );

  return {
    message,
    onChange,
    onSubmit
  };
};

フォームのコンポーネント

import React from "react";
import { useMessageForm } from "containers/Messages/hooks/useMessageForm";

type Props = ReturnType<typeof useMessageForm>;

export const MessageForm: React.FC<Props> = ({
  message,
  onChange,
  onSubmit
}) => {
  return (
    <form onSubmit={onSubmit}>
      <input type="text" value={message} onChange={onChange} />
    </form>
  );
};

hooksを使う側は以下のようになる。

import React from "react";
import { Messages } from "components/Messages";
import { MessageForm } from "components/MessageForm";
import { GRPCClients } from "gRPCClients";
import { useMessages } from "./hooks/useMessages";
import { useMessageForm } from "./hooks/useMessageForm";

type Props = {
  clients: GRPCClients;
};

export const MessagesContainer: React.FC<Props> = ({ clients }) => {
  const messengerClient = clients.messengerClient;
  const messagesState = useMessages(messengerClient);
  const messageFormState = useMessageForm(messengerClient);
  return (
    <div>
      <MessageForm {...messageFormState} />
      <Messages {...messagesState} />
    </div>
  );
};

プロキシの設定

現時点でgRPC-Webを使うには、プロトコル間の微調整を行うためのプロキシが必要で、公式ではEnvoyを推奨していたりする。

Dockerイメージがいい感じに用意されているので、フロントエンドからはプロキシにリクエスト、プロキシコンテナはサーバーコンテナにlinkするだけである。

詳しく見たい方は以下へどうぞ。

これで一通りの実装が完了し、docker-compose upでアプリケーションが起動できるようになった。

コードの全貌はGitHubに。

おわりに

gRPC-Web + React Hooks + Goを用いてリアルタイムチャットを作成してみた。

まだ制限もあるが、少なくともRESTの置き換えとしては十分候補に入れてよいのではないだろうか。また、領域を問わずコンテナベースでの開発がスタンダードになっていることを改めて実感できた。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

3分でわかるgRPC-Web

gRPCとは

gRPCの概要を簡単にまとめる。

  • HTTP/2による高速な通信
  • IDL(Protocol Buffers)でデータ構造とRPCを定義
  • 多言語対応のソースコード自動生成
  • Streamingを用いた双方向通信が可能

詳細は以下へ。

gRPC-Webとは

gRPC-WebによってgRPC通信をWebでも使うことができる。以上!

といえればいいのだが、実際は、ブラウザの制限にあわせたプロトコルを定義している。

そのため、現時点だと、プロトコル間の微調整を行うためのプロキシが必要で、公式ではEnvoyを推奨していたりする。

ブラウザの制限

前述したブラウザの制限とは、例えば以下のようなものだ。

  • HTTP/2のフレーミングレイヤーはブラウザに露出されない
  • ブラウザからのStreamingがまだ不十分 (WHATWG Streams)
  • クライアントにHTTP/2の使用を強制できない
  • クロスブラウザで動くbase64のようなテキストエンコーディングの必要性

上記により、以下のようなことがgRPC-Webでは不可能である。

  • gRPCでサーバーとの「直接」通信 (Proxyを用意する必要がある)
  • Client-side & Bi-directional streaming

少なくともBi-directional streamingがでできるようになればgRPC-Webの立場はかなり上がると思うので残念だ。

メリット

現時点でのgRPC-Webのメリットは以下のようなものがある。

  • クライアントからサーバーまで、一気通貫でgRPCの開発パイプラインに載せられる
  • バックエンド・フロントエンド間でタイトな連携ができる
  • クライアント向けの「gRPCライブラリ」を容易に生成できる

例えば、バックエンドのサービス群がgRPCで構築されている時、HTTPのレイヤーでBFFを用意する必要がなくなり、不要なAPI設計やコミュニケーションをへらすことができるのがメリットになりそうだ。

下の2つは、IDLベースなこととコードの自動生成により、RESTなどで「仕様書ベース」で合意を行うよりも、スムーズな開発ができるということだと理解した。アプローチや思想は異なるが、GraphQLとも一部ゴールを共有しそうだ。

gRPC APIの設計

API設計のガイドラインをGoogleが用意していたりする。

使ってみた

実際にgRPC-Webをつかって簡単なチャットを作ってみた↓

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

zmq4でFizzBuzzする

初めに

Goのすごい人がついったーでgophernotes の pure go zeromq 対応が入った。とツイートしてた。
すごい人が言ってるんだから使っておいて損無いハズ。

5分ぐらい考えた結果、FizzBuzzすることに決めた。
プログラマならFizzBuzzとか数列とか使いたくなるんだからしょうがない。

できたもの

dekita.png

斬新なFizzBuzz結果

zeromqとは

ブローカーなしのメッセージング。ブローカー不要が何より嬉しい。
どうせ通信相手そんなにいないのに、いちいちサーバ立てたくない。
詳しくはZeroMQ参照。

FizzBuzzとは

よくやる奴。割愛。

【非検証】gophernotesを使おうとしたけど使えなかったので手順だけまとめる

gophernotesとは、Jupyter NotebookのGo言語版的な奴。詳しくはgophernotesを参照。

諸般の事情(Windowsしか持ってない)があるので、Dockerを利用する必要がある。
が、今現在Dockerが利用できる端末がない。
なので環境構築の順番だけでもまとめる。

簡単に利用するためにgopherdata/gophernotesをベースとして自分の使いやすいイメージを作る。

Dockerfile
FROM gopherdata/gophernotes:latest

RUN go get github.com/go-zeromq/zmq4 \
  && mkdir -p /etc/jupyter

COPY ./entrypoint.sh /entrypoint.sh
COPY ./jupyter_notebook_config.py /etc/jupyter/jupyter_notebook_config.py

ENTRYPOINT [ "/entrypoint.sh" ]

entrypoint.sh
jupyter notebook --no-browser --allow-root --ip=0.0.0.0  --config=/etc/jupyter/jupyter_notebook_config.py

configはgopherdata/gophernotesのイメージをいったん走らせてから、コンテナに吐いてもらう。

# コンテナ走らせて中に入る
docker run -it -d gopherdata/gophernotes
docker exec -it ${gophernotesのid} /bin/sh
# config吐く
jupyter noteboopk --generate-config
mv /path/to/jupyter_notebook_config.py /tmp/
exit
# ローカルに落とす
docker cp ${gophernotesのid}:/tmp/jupyter_notebook_config.py ./

手元で試すだけなら、jupyter_notebook_config.pyのtokenを書き変える。
後はコンテナをビルドして実行するだけ

docker build -t miyatamagophernotes:1.0.0 .
docker run -it -p 8888:8888 miyatamagophernotes:1.0.0

zmq4

まずはpub側から

publish側

1秒おきに100までのFizzBuzzを垂れ流す。

package main

import (
    "fmt"
    "time"
    "context"
    "sync"
    "github.com/go-zeromq/zmq4"
)

func main() {
    err := startPublisher()
    if err != nil {
        fmt.Printf("%v", err)
    }
}

func startPublisher() error {
    pub := zmq4.NewPub(context.Background())
    defer pub.Close()

    err := pub.Listen("tcp://*:5563")
    if err != nil {
        return err
    }
    for {
        publishFizzBuzz(pub)
        if err != nil {
            return err
        }
        time.Sleep(time.Second)
    }
    return nil
}

func publishFizzBuzz(publisher zmq4.Socket) {
    getFizzBuzzText := func(num int) string {
        if (num % 15) == 0  {
            return "FizzBuzz"
        }
        if (num % 3) == 0 {
            return "Fizz"
        }
        if (num % 5) == 0 {
            return "Buzz"
        }
        return fmt.Sprintf("%d", num)
    }

    wg := &sync.WaitGroup{}
    for i := 0; i < 100 ; i++ {
        wg.Add(1)
        go func(number int) {
            defer wg.Done()
            msg := zmq4.NewMsgFrom(
                []byte("FizzBuzz"),
                []byte(getFizzBuzzText(number)),
            )
            publisher.Send(msg)
        }(i + 1)
    }
    wg.Wait()
}

続いてsub

Subscribe側

受け取って表示するだけ

package main

import (
    "context"
    "fmt"
    "github.com/go-zeromq/zmq4"
)

func main() {

    err := startSubscribe()
    if err != nil {
        fmt.Printf("%v", err)
    }
}

func startSubscribe() error {
    sub := zmq4.NewSub(context.Background())
    defer sub.Close()

    err := sub.Dial("tcp://localhost:5563")
    if err != nil {
        return err
    }

    err = sub.SetOption(zmq4.OptionSubscribe, "FizzBuzz")
    if err != nil {
        return err
    }

    for {
        msg, err := sub.Recv()
        if err != nil {
            return err
        }
        fmt.Printf("%s\n", msg.Frames[1])
    }
}

ふりかえり

今回も様々な知見を得られた

  • FizzBuzzをgophernotes上のZeroMQでやる必要は全くない
  • gophernotesが利用できなかったので単純にzmq4でFizzBuzzしただけになった
  • 並列処理したおかげでFizzBuzzの結果が正しいかよく分からなくなった
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む