- 投稿日:2020-03-14T21:50:07+09:00
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つ目までしかかけていませんが、また違う原因で同じようなエラーが発生した場合は追記していこうと思います。
- 投稿日:2020-03-14T20:02:16+09:00
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.Fprintln
やfmt.Fprintf
と併せて使います。fmt.Println
やfmt.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!") }参考
まとめ
今回は競技プログラミングの問題を解く上で必須と思われる標準入出力についてまとめました。
次回からは実際に問題を解いていきたいと思います。
- 投稿日:2020-03-14T17:49:21+09:00
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に変換するリバースプロキシサーバの役割を担うことができる、というものです。gRPCサーバの作成
なにはともあれ、grpc-gatewayを試すにはgRPCサーバが必要ですので、まずはそれをサクッと作成します。
まずはprotoファイルの定義からservice.protosyntax = "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.gotype 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.gofunc 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.gofunc 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.protosyntax = "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.protogrpc-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.gofunc 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.gofunc 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"}無事、疎通確認ができました
?
?
まとめ
gRPCサーバさえあれば、導入しきい値は結構低いのではないかなと思っています。かなり簡単にgrpc-gatewayの疎通確認ができました。
再掲ですが、githubにコードを公開しています
https://github.com/KatsuyaAkasaka/grpc-gateway-sample
- 投稿日:2020-03-14T16:30:39+09:00
【GO/DDD】レイヤードアーキテクチャの整理
もうすでに詳しい記事が色々ありますが、自分の知識の整理として書いていきます。
レイヤードアーキテクチャ
下記の図の構成になります。
Presentation層(handler)→Application層(usecase)→Domain層(domain)→Infrastructure層(infrastructure)の順番で、一つの方向に依存して構成になっています。
またそれぞれの層にinterface
,struct
を定義しています。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 これとかもめちゃくちゃ参考になった。
てめえらの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の中では戦術的設計に該当します。
依存関係
上記のレイヤードアーキテクチャのような構成を取り入れると何が嬉しいのか。
それは依存関係が単一の方向になり、どちらか一方の変更でもう一方が壊れるリスクを無くしてくれます。
つまり変更に強くなるということに集約されると思います。理想
個人的に注意すること
- 依存性の大きい要素は使われる場所を局所的にする
- 依存性の小さい要素は多くの場所から使われるようにする
- 各層でも依存関係が単一になっていること
プログラムの依存関係とモジュール構成のこと - Qiitaより引用
interface
機能を約束してくれて、異なる構造体(中身が全く異なっていたとしても)でも問題なく使えます。
Goのinterfaceがわからない人へ - Qiita
インタフェースの実装パターン #golang - Qiitaその他参考になる記事
Goのpackage構成と開発のベタープラクティス - Tech Blog - Recruit Lifestyle Engineer
レイヤードアーキテクチャの視点 - Qiita
Golang APIレイヤードアーキテクチャで組んでみた。 - Qiita
MVC、3 層アーキテクチャから設計を学び始めるための基礎知識 - Qiita
- 投稿日:2020-03-14T15:58:24+09:00
gRPC-Web + React Hooks + Go でリアルタイムチャットをつくる
概要
分散システムを学ぶうちにgRPCに興味を持った。きくところによると、gRPC-Webというものもあるらしい。
この記事では、gRPC-Web + React Hooks + Goを用いてリアルタイムチャットを作りながら、実装の流れを書いてみようと思う。
コードだけ見たいという方は↓へ、
gRPC-Webってなんやねん?という方は↓へどうぞ!
全体像
サービスの全体像は以下のようになる。
タイトルのとおり、ReactクライアントからgRPC-WebでGoサーバーと通信するチャットサービスだ。
デモはこんな感じ↓
(リアルタイムですべてのクライアントにメッセージが配信される)開発
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-go
、protoc-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-godocker-compose.ymlversion: '3' services: proto: command: ./proto/scripts/protoc.sh build: context: . dockerfile: DockerfileProto volumes: - .:/protoprotoc.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の置き換えとしては十分候補に入れてよいのではないだろうか。また、領域を問わずコンテナベースでの開発がスタンダードになっていることを改めて実感できた。
参考
- 投稿日:2020-03-14T15:18:25+09:00
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をつかって簡単なチャットを作ってみた↓
参考
- 投稿日:2020-03-14T12:24:40+09:00
zmq4でFizzBuzzする
初めに
Goのすごい人がついったーで
gophernotes の pure go zeromq 対応が入った。
とツイートしてた。
すごい人が言ってるんだから使っておいて損無いハズ。5分ぐらい考えた結果、FizzBuzzすることに決めた。
プログラマならFizzBuzzとか数列とか使いたくなるんだからしょうがない。できたもの
斬新な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.pyconfigは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.0zmq4
まずは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の結果が正しいかよく分からなくなった