- 投稿日:2020-01-25T17:56:13+09:00
CircleCIでGo言語製のRest APIのテストを自動で回す〜docker-composeを利用して〜
はじめに
こんにちは
コンテナを使ったアプリケーション開発もぼちぼち増えてきたように感じます。
その中でも開発時に複数のコンテナ・プロセスを管理したいためにdocker-composeを利用している方も多いのではないでしょうか。今回は、開発環境で利用しているdocker-composeをそのまま利用して、
CircleCIで自動テストする一例をご紹介したいと思います。コンテナ構成
docker-compose.ymlversion: '2' services: db: build: context: db/ expose: - "3306" environment: - MYSQL_ROOT_PASSWORD=root #rootパスワードの設定 - MYSQL_DATABASE=test - MYSQL_USER=user - MYSQL_PASSWORD=password volumes: - ./db/mysql_data:/db/mysql_data ports: - "3306:3306" app: build: context: app/ volumes: - ./app/:/go/src/app command: realize start --run --no-config # DEBUG restart: always ports: - "8080:8080" depends_on: - db今回作成するAPI
GETだけできるとてもシンプルなものにします。
Status Code: 200
リクエスト
GET /user/1レスポンス
{ "name": "山田", "age": 11 }Status Code: 404
リクエスト
GET /user/3レスポンス
{ "code": "Not Found", "message": "レコードが見つかりません" }対応するテーブル・スキーマ
desc users:Field, Type, Null, Key, Extra --------------------------------------- id , int(11), No, PRI, auto_increment name, varchar(255), YES age, int(11), YES実装コード
main.gor.GET("/user/:id", func(c *gin.Context) { userId, _ := strconv.Atoi(c.Param("id")) db := getDB() var user model.User if err := db.Where("id = ?", userId).First(&user).Error; gorm.IsRecordNotFoundError(err) { c.JSON(http.StatusNotFound, gin.H{ "code": "Not Found", "message": "レコードが見つかりません", }) return } c.JSON(http.StatusOK, user) defer db.Close() })※Webフレームワークにgin, ORMにgormを採用しています。
マッピングしているUser構造体
type User struct { Id int `json:"-"` Name string `json:"name"` Age int `json:"age"` }テストコード
200と404を確認します。
テストレコードの作成もテストコードの中でやってしまいます。
※headerのチェックのみの簡略的なものとします。main_test.gofunc Test_Main(t *testing.T) { db, err := gorm.Open("mysql", "user:password@tcp(db:3306)/test?charset=utf8mb4&parseTime=True&loc=Local") if err != nil { panic(err.Error()) } // make db scheme db.AutoMigrate(&model.User{}) // insert test record db.Create(&model.User{Id: 1, Name: "山田", Age: 11}) t.Run("Check HTTP 200", func(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/user/1", nil) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) }) t.Run("Check HTTP 404", func(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/user/2", nil) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) }) }CircleCIのconfig.yml
ポイントは、
buildタイプにmachineビルドを選択し、imageにcircleci/classic:edgeを選択する点です。.circleci/config.ymlversion: 2 jobs: build: machine: image: circleci/classic:edge working_directory: ~/repo steps: - checkout - run: name: install docker-compose command: | curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose chmod +x ~/docker-compose sudo mv ~/docker-compose /usr/local/bin/docker-compose - run: name: docker-compose up command: | set -x docker-compose up -d - run: name: test command: docker-compose exec app go test -v main_test.go main.go - run: name: docker-compose down command: docker-compose down参考
Choosing an Executor Type
Installing and Using docker-compose
build結果
$ #!/bin/bash -eo pipefail docker-compose exec app go test -v main_test.go main.go ^@^@[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /user/:id --> command-line-arguments.Router.func1 (3 handlers) === RUN Test_Main === RUN Test_Main/Check_HTTP_200 [GIN] 2020/01/25 - 07:51:33 | 200 | 2.23664ms | | GET /user/1 === RUN Test_Main/Check_HTTP_404 [GIN] 2020/01/25 - 07:51:33 | 404 | 1.848185ms | | GET /user/2 --- PASS: Test_Main (0.03s) --- PASS: Test_Main/Check_HTTP_200 (0.00s) --- PASS: Test_Main/Check_HTTP_404 (0.00s) PASS ok command-line-arguments 0.040s細かいところはソースをご覧下さい。
注意点
image: circleci/classic:edgeはローカル環境でpullすることができないため(2020/01/25時点)、ローカルでの動作確認はコンテナにアタッチして手動で行う必要があります。要は、
ローカルコマンド
$ circleci build
が必ずコケて使えないということです。
※おそらくCircleCI側の都合だと思います。#リソース制限などおわりに
Pull Requestの段階で強制的に自動テストが走るようにすることは、もはやチーム開発において必須の仕組みだと感じました。
これからもCircleCIなどのCIツールを活用して、アプリケーションの品質向上と将来的な開発速度の向上を目指したいと思います。
CircleCIを活用したおもしろい例があれば、ぜひご紹介下さい。
- 投稿日:2020-01-25T16:03:20+09:00
Go言語でExcelからセル値を取得 & セル座標⇔番号変換
https://github.com/yagrush/excelutil
golangでExcelを読み込む必要があったので開発しました。
ポイントは、
『ユーザーがどんな座標指定の仕方をしてくるか、未知』
だったので、ある程度の柔軟性とエラーチェックを仕込みました。使い方
cmd/test.go
がそのまんまサンプルになっているので、ご参照ください。関数のご紹介
excel.go
Init(excelFilePath string)
なにはともあれ、最初に呼んでやってください。
Javaのコンストラクタのような感じです。
Excelファイルのパスを渡すと、Excelファイルを読み込む下準備をして、構造体を返してくれます。(excelFileInstance *ExcelFile) ReadCell(sheetName string, colNum, rowNum int)
Excelファイルの中のシートの名前、セルの列番号、行番号を指定すると、セル値を読み出してくれます。
セルの列番号、行番号の指定方法は、例えば D5 セルだったら 4, 5 です。
Initで得た構造体にドット . を付けて、直接呼び出してください。(excelFileInstance *ExcelFile) ReadCellByCellAddress(sheetName string, cellAddress string)
Excelファイルの中のシートの名前、セル座標を指す文字を指定すると、セル値を読み出してくれます。
"セル座標の文字" とは、Excelではお馴染みの A7 とか BC20 とかの表記法です。
Initで得た構造体にドット . を付けて、直接呼び出してください。ConvExcelCellAddressToColnumAndRownum(excelCellAddress string)
セル座標を指す文字を指定すると、列番号と行番号に変換して返します。
例えば、D5 セルだったら 4, 5 です。
ちなみに B:3 とか AZ-983 とか、間に文字を挟んで指定するユーザーもいるかも知れないので、対応しました。
(間に何か挟んでても、ちゃんと処理します)
ReadCellByCellAddress
関数も中でこれを使っているので、同様です。ConvExcelColNumToAlphabet(cellColnum int)
列番号を指定すると、セル座標を指す文字の"列部分"を返します。
例えば 3 を指定すると C を返します。
開発にあたってはこちらを参考にさせて頂きました。
https://qiita.com/bamchoh/items/447a4c40e5f39edb2512
ありがとうございます。またExcelの根幹処理は、もはや定番のこちらを。
https://github.com/tealeg/xlsx
感謝!
- 投稿日:2020-01-25T12:13:27+09:00
goでGRPCのSimpleRPCやってみた(Unary RPC)
goでgrpcのSimpleRPCを実行してみました!
この記事内容を上から行えば、動く環境が作れます。grpc
・SimpleRPCとは
一つのデータを受け取って、一つのデータを返すdocker環境
・フォルダ構成
以下のフォルダを想定しています。./infrastructure/docker/go-grpc/Dockerfile ./infrastructure/docker/docker-compose.yml・フォルダを作成します。
mkdir -p ./infrastructure/docker/go-grpc cd ./infrastructure/docker touch docker-compose.yml ./go-grpc/Dockerfile・Dockerfile
FROM golang:1.13-stretch SHELL ["/bin/bash", "-c"] RUN apt update && apt-get install -y vim unzip # install protc WORKDIR /protoc RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.2/protoc-3.11.2-linux-x86_64.zip RUN unzip protoc-3.11.2-linux-x86_64.zip RUN ln -s /protoc/bin/protoc /bin/protoc # golang WORKDIR /go-grpc ENV GO111MODULE on RUN go get -u github.com/golang/protobuf/protoc-gen-go・docker-compose.yml
version: "3.7" services: go-grpc: build: context: ./go-grpc/ dockerfile: Dockerfile container_name: "go-grpc" volumes: - ../../:/go-grpc tty: true privileged: true・環境に入る
以下のコマンドでdockerコンテナの中に入れます。cd ./infrastructure/docker docker-compose up -d docker-compose exec go-grpc bashprotoの作成
以下dockerコンテナに入って状態で作業します。
コンパイラである「protoc」は、dockerコンテナにインストールしてあります。足算を想定したprotoです。
mkdir ./proto touch ./proto/calc.proto・calc.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.simple"; option java_outer_classname = "SimpleProto"; // package名 package calc; // 上の4つのパターンをここで指定する。 service Calc { rpc Sum (SumRequest) returns (SumReply) {} } // リクエストで送る値。「1、2」はそのデータの番号。 message SumRequest { int32 a = 1; int32 b = 2; } // レスポンスで送る値 message SumReply { string message = 1; }・上の「.protoファイル」を、goファイルにコンパイルします。
以下のコマンドで、./pb/calcフォルダに「calc.pb.go」が自動生成されます。protoc --proto_path ./proto --go_out=plugins=grpc:./pb/calc calc.proto「calc.pb.go」が生成されました。
goで実行
・以下のコマンドで、gomoduleを初期設定をします。
go mod init grpc-sample「受け取る側のサーバー」と「送る側のクライアント」の2つの実行ファイルを作成します。
mkdir server client touch ./server/main.go ./client/main.go・./server/main.go
package main import ( "context" "fmt" "log" "net" pb "grpc-sample/pb/calc" "google.golang.org/grpc" ) const ( port = ":50051" ) type server struct { pb.UnimplementedCalcServer } func (s *server) Sum(ctx context.Context, in *pb.SumRequest) (*pb.SumReply, error) { a := in.GetA() b := in.GetB() log.Printf("%v, %v", a, b) reply := fmt.Sprintf("%d + %d = %d", a, b, a+b) return &pb.SumReply{Message: reply}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterCalcServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }・./client/main.go
package main import ( "context" "fmt" "log" "time" "github.com/pkg/errors" pb "grpc-sample/pb/calc" "google.golang.org/grpc" ) func getAdress() string { const ( host = "localhost" port = "50051" ) return fmt.Sprintf("%s:%s", host, port) } func exec(a, b int32) error { address := getAdress() conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { return errors.Wrap(err, "did not connect") } defer conn.Close() client := pb.NewCalcClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() reply, err := client.Sum(ctx, &pb.SumRequest{ A: a, B: b, }) if err != nil { return errors.Wrap(err, "could not greet") } log.Printf("サーバからの受け取り\n %s", reply.GetMessage()) return nil } func main() { exec(300, 500) }・サーバを実行します。
go run server/main.goこれでサーバが立ったので、クライアントからデータを送ります。
go run client/main.goクライアント側の表示
300, 500という数字を送ったので、800という数字がサーバから返ってきました。
サーバ側の表示
クライアントから、300と500という値を受け取りました。
この下に、コードの説明も書きます。
- 投稿日:2020-01-25T12:13:27+09:00
goでgRPCの4つの通信方式やってみた(Dockerのサンプルあり)
goでgRPCを実装してみました。
gRPCの4つの通信方式を実際にやってみます。
・Unary RPCs(SimpleRPC)
・Server streaming RPC
・Client streaming RPC
・Bidirectional streaming RPCこの記事内容を上から行えば、Dockerで動く環境が作れます。
Dockerでこの4つの実装してる記事は少ないはず。gRPCとは
googleの「RPCのフレームワーク」です。
RPCは、アプリケーション間のデータ通信するためのプログラムのことです。
gRPCでは、以下を提供します。
HTTP/2、10言語の公式サポートライブラリ、プロトコルバッファ(構造体みたいなもの)を定義することにより言語を問わずにデータ通信が可能、ヘルスチェック、ロードバランシングなどをサポート。一言で言うと、簡単にアプリケーション間のデータ通信が高速にできるやつ。
RESTAPI化もできる。
gRPCがサポートされていない言語やクライアントからRESTAPI形式でデータの受け渡したい時や、下位互換性のためなどに使用する。
(grpc-gateway使わなかったら、プロトコルバッファをコンパイルしないといけないので、ブラウザなどからはまだ呼べない。)
grpc-gatewaygRPCの4つの通信方式
StreamingとUnaryの組み合わせのため4つあります。
以下用途と共に説明します。
公式を参考・Unary RPC
一つのデータを受け取って、一つのデータを返す。
動きはよくあるrestAPIと同じ。
用途:サーバ間通信、API、アプリとサーバのデータ送受信。
(Unary : Unary)
・Server streaming RPC
サーバがクライアントに複数のリクエストを送る。
用途:サーバから任意のタイミングでクライアントに通知させたい時など。
(Unary : Streaming)
・Client streaming RPC
クライアントがサーバに複数のリクエストを送る。
用途:データアップロードや、クライアントから多くのデータを送る場合。
(Streaming : Unary)
・Bidirectional streaming RPC
双方向でデータのやりとりをする。
用途:チャットやオンライン対戦など。
(Streaming : Streaming)
サーバ間通信とかで使う
ある処理がめっちゃ「メモリ使う、CPU使う」という時は、サーバを分けるとコスト削減できます。
(ユーザ数とかが多くない場合は不要)
そういう時に、サーバ間でデータの受け渡しが必要になります。gRPCを使うメリットとして、
・プロトコルバッファを定義すれば、そのデータ形式(構造体的な感じ)でやりとりができる。
=> restAPIだとエンドポイントを定義してそこにリクエストを叩かないといけないので、単純に面倒。
・プロトコルバッファに型とキー情報があるのでドキュメント不要。
=> restAPIと違って、エンドポイントのドキュメントなくても分かりやすい。
・速いプロトコルバッファ
データや通信方式を定義するIDLのこと。
・「レスポンスとリクエスト」のデータ形式
・ストリーミングを使うか
・パッケージ名
などを定義する。
クライアントとサーバの両方で持ってないといけない。
protcというコンパイラがあるので、今回はこのコンパイラを使ってGO言語ファイルを吐き出す。環境構築
Docker環境
・フォルダ構成
以下のフォルダを想定しています。./infrastructure/docker/go-grpc/Dockerfile ./infrastructure/docker/docker-compose.yml・フォルダを作成します。
mkdir -p ./infrastructure/docker/go-grpc cd ./infrastructure/docker touch docker-compose.yml ./go-grpc/Dockerfile・Dockerfile
FROM golang:1.13-stretch SHELL ["/bin/bash", "-c"] RUN apt update && apt-get install -y vim unzip # install protc WORKDIR /protoc RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.2/protoc-3.11.2-linux-x86_64.zip RUN unzip protoc-3.11.2-linux-x86_64.zip RUN ln -s /protoc/bin/protoc /bin/protoc # golang WORKDIR /go-grpc ENV GO111MODULE on RUN go get -u github.com/golang/protobuf/protoc-gen-go・docker-compose.yml
version: "3.7" services: go-grpc: build: context: ./go-grpc/ dockerfile: Dockerfile container_name: "go-grpc" volumes: - ../../:/go-grpc tty: true privileged: true・環境に入る
以下のコマンドでdockerコンテナの中に入ります!cd ./infrastructure/docker docker-compose up -d docker-compose exec go-grpc bash実装
上の4つの通信方式を全て動かしてみます。
必要なやつだけを読んでも大丈夫です。Unary RPC
2つの数字を送ったら足算して返すサーバとクライアントを作成します。
protoの作成
以下dockerコンテナに入った状態で作業します。
コンパイラである「protoc」は、dockerコンテナにインストールしてあります。「a,bをintで受け取ってメッセージを返す」を想定したprotoです。
mkdir ./proto touch ./proto/calc.proto・calc.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.simple"; option java_outer_classname = "SimpleProto"; // package名 package calc; // 上の4つのパターンをここで指定する。 service Calc { rpc Sum (SumRequest) returns (SumReply) {} } // リクエストで送る値。「1、2」はそのデータの番号。 message SumRequest { int32 a = 1; int32 b = 2; } // レスポンスで送る値 message SumReply { string message = 1; }・上の「.protoファイル」を、goファイルにコンパイルします。
以下のコマンドで、./pb/calcフォルダに「calc.pb.go」が自動生成されます。protoc --proto_path ./proto --go_out=plugins=grpc:./pb/calc calc.proto「calc.pb.go」が生成されました。
goで実行
・以下のコマンドで、gomoduleを初期設定をします。
go mod init grpc-sample「受け取る側のサーバー」と「送る側のクライアント」の2つの実行ファイルを作成します。
mkdir server client touch ./server/main.go ./client/main.go・./server/main.go
package main import ( "context" "fmt" "log" "net" pb "grpc-sample/pb/calc" "github.com/pkg/errors" "google.golang.org/grpc" ) const port = ":50051" // ServerUnary is server type ServerUnary struct { pb.UnimplementedCalcServer } // Sum 二つの値を受け取り、合計してクライアントへ返す func (s *ServerUnary) Sum(ctx context.Context, in *pb.SumRequest) (*pb.SumReply, error) { a := in.GetA() b := in.GetB() fmt.Println(a, b) reply := fmt.Sprintf("%d + %d = %d", a, b, a+b) return &pb.SumReply{ Message: reply, }, nil } func set() error { lis, err := net.Listen("tcp", port) if err != nil { return errors.Wrap(err, "ポート失敗") } s := grpc.NewServer() var server ServerUnary pb.RegisterCalcServer(s, &server) if err := s.Serve(lis); err != nil { return errors.Wrap(err, "サーバ起動失敗") } return nil } func main() { fmt.Println("起動") if err := set(); err != nil { log.Fatalf("%v", err) } }・./client/main.go
package main import ( "context" "log" "time" "github.com/pkg/errors" pb "grpc-sample/pb/calc" "google.golang.org/grpc" ) func request(client pb.CalcClient, a, b int32) error { ctx, cancel := context.WithTimeout( context.Background(), time.Second, ) defer cancel() sumRequest := pb.SumRequest{ A: a, B: b, } reply, err := client.Sum(ctx, &sumRequest) if err != nil { return errors.Wrap(err, "受取り失敗") } log.Printf("サーバからの受け取り\n %s", reply.GetMessage()) return nil } func sum(a, b int32) error { address := "localhost:50051" conn, err := grpc.Dial( address, grpc.WithInsecure(), grpc.WithBlock(), ) if err != nil { return errors.Wrap(err, "コネクションエラー") } defer conn.Close() client := pb.NewCalcClient(conn) return request(client, a, b) } func main() { a := int32(300) b := int32(500) if err := sum(a, b); err != nil { log.Fatalf("%v", err) } }・サーバを実行します。
go run server/main.goこれでサーバが立ったので、クライアントからデータを送ります。
go run client/main.goサーバ側の表示
上のコマンドを実行したので、
クライアントから、300と500という値を受け取りました。
クライアント側の表示
300, 500という数字を送ったので、800という数字がサーバから返ってきました。
1リクエスト1レスポンスのサーバとクライアンを動かすことができました。
これで、「めっちゃ負荷が高い処理をする機能」があった場合とかにサーバを分割できたりもする。Server streaming RPC
サーバから値を受け取りづつけます。
protoの作成
以下dockerコンテナに入った状態で作業します。
mkdir ./proto touch ./proto/notification.proto・notification.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.notification"; option java_outer_classname = "NotificationProto"; package notification; service Notification { rpc Notification (NotificationRequest) returns (stream NotificationReply) {} } message NotificationRequest { int32 num = 1; } message NotificationReply { string message = 1; }・上の「.protoファイル」を、goファイルにコンパイルします。
以下のコマンドで、./pb/notificationフォルダに「notification.pb.go」が自動生成されます。protoc --proto_path ./proto --go_out=plugins=grpc:./pb/notification notification.proto「notification.pb.go」が生成されました。
・clinet
package main import ( "context" "io" "log" "github.com/pkg/errors" pb "grpc-sample/pb/notification" "google.golang.org/grpc" ) func request(client pb.NotificationClient, num int32) error { req := &pb.NotificationRequest{ Num: num, } stream, err := client.Notification(context.Background(), req) if err != nil { return errors.Wrap(err, "streamエラー") } for { reply, err := stream.Recv() if err == io.EOF { break } if err != nil { return err } log.Println("これ:", reply.GetMessage()) } return nil } func exec(num int32) error { address := "localhost:50051" conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { return errors.Wrap(err, "コネクションエラー") } defer conn.Close() client := pb.NewNotificationClient(conn) return request(client, num) } func main() { num := int32(5) if err := exec(num); err != nil { log.Println(err) } }・server
package main import ( "fmt" "log" "net" "time" pb "grpc-sample/pb/notification" "github.com/pkg/errors" "google.golang.org/grpc" ) const port = ":50051" // ServerServerSide is server type ServerServerSide struct { pb.UnimplementedNotificationServer } // Notification is func (s *ServerServerSide) Notification(req *pb.NotificationRequest, stream pb.Notification_NotificationServer) error { fmt.Println("リクエスト受け取った") for i := int32(0); i < req.GetNum(); i++ { message := fmt.Sprintf("%d", i) if err := stream.Send(&pb.NotificationReply{ Message: message, }); err != nil { return err } time.Sleep(time.Second * 1) } return nil } func set() error { lis, err := net.Listen("tcp", port) if err != nil { return errors.Wrap(err, "ポート失敗") } s := grpc.NewServer() var server ServerServerSide pb.RegisterNotificationServer(s, &server) if err := s.Serve(lis); err != nil { return errors.Wrap(err, "サーバ起動失敗") } return nil } func main() { fmt.Println("起動") if err := set(); err != nil { log.Fatalf("%v", err) } }Client streaming RPC
クライアントから値を受け取りづつけます。
protoの作成
以下dockerコンテナに入った状態で作業します。
mkdir ./proto touch ./proto/upload.proto・upload.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.upload"; option java_outer_classname = "UploadProto"; package upload; service Upload { rpc Upload (stream UploadRequest) returns (UploadReply) {} } message UploadRequest { int32 value = 1; } message UploadReply { string message = 1; }・上の「.protoファイル」を、goファイルにコンパイルします。
以下のコマンドで、./pb/uploadフォルダに「upload.pb.go」が自動生成されます。protoc --proto_path ./proto --go_out=plugins=grpc:./pb/upload upload.proto「upload.pb.go」が生成されました。
・clinet
package main import ( "context" "fmt" "io" "log" "time" "github.com/pkg/errors" pb "grpc-sample/pb/upload" "google.golang.org/grpc" ) func request(client pb.UploadClient) error { stream, err := client.Upload(context.Background()) if err != nil { return err } values := []int32{1, 2, 3, 4, 5} for _, value := range values { fmt.Println("送る値:", value) if err := stream.Send(&pb.UploadRequest{ Value: value, }); err != nil { if err == io.EOF { break } return err } time.Sleep(time.Second * 1) } reply, err := stream.CloseAndRecv() if err != nil { return err } log.Printf("結果: %v", reply) return nil } func exec() error { address := "localhost:50051" conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { return errors.Wrap(err, "コネクションエラー") } defer conn.Close() client := pb.NewUploadClient(conn) return request(client) } func main() { if err := exec(); err != nil { log.Println(err) } }・server
package main import ( "fmt" "io" "log" "net" pb "grpc-sample/pb/upload" "github.com/pkg/errors" "google.golang.org/grpc" ) const port = ":50051" // ServerClientSide is servre type ServerClientSide struct { pb.UnimplementedUploadServer } // Upload 複数の送られてきた数字を合計する func (s *ServerClientSide) Upload(stream pb.Upload_UploadServer) error { var sum int32 for { req, err := stream.Recv() if err == io.EOF { message := fmt.Sprintf("DONE: sum = %d", sum) return stream.SendAndClose(&pb.UploadReply{ Message: message, }) } if err != nil { return err } fmt.Println(req.GetValue()) sum += req.GetValue() } } func set() error { lis, err := net.Listen("tcp", port) if err != nil { return errors.Wrap(err, "ポート失敗") } s := grpc.NewServer() var server ServerClientSide pb.RegisterUploadServer(s, &server) if err := s.Serve(lis); err != nil { return errors.Wrap(err, "サーバ起動失敗") } return nil } func main() { fmt.Println("起動") if err := set(); err != nil { log.Fatalf("%v", err) } }Bidirectional streaming RPC
双方で、値を受け取りづつけます。
protoの作成
以下dockerコンテナに入った状態で作業します。
mkdir ./proto touch ./proto/chat.proto・chat.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.chat"; option java_outer_classname = "ChatProto"; package chat; service Chat { rpc Chat (stream ChatRequest) returns (stream ChatReply) {} } message ChatRequest { string message = 1; } message ChatReply { string message = 1; }・上の「.protoファイル」を、goファイルにコンパイルします。
以下のコマンドで、./pb/chatフォルダに「chat.pb.go」が自動生成されます。protoc --proto_path ./proto --go_out=plugins=grpc:./pb/chat chat.proto「chat.pb.go」が生成されました。
・clinet
package main import ( "context" "io" "log" "time" "github.com/pkg/errors" pb "grpc-sample/pb/chat" "google.golang.org/grpc" ) func receive(stream pb.Chat_ChatClient) error { waitc := make(chan struct{}) go func() { for { in, err := stream.Recv() if err == io.EOF { close(waitc) return } if err != nil { log.Fatalf("エラー: %v", err) } log.Printf("サーバから:%s", in.Message) // お返し stream.Send(&pb.ChatRequest{ Message: time.Now().Format("2006-01-02 15:04:05"), }) } }() <-waitc return nil } func request(stream pb.Chat_ChatClient) error { return stream.Send(&pb.ChatRequest{ Message: "こんにちは", }) } func chat(client pb.ChatClient) error { stream, err := client.Chat(context.Background()) if err != nil { return err } if err := request(stream); err != nil { return err } if err := receive(stream); err != nil { return err } stream.CloseSend() return nil } func exec() error { address := "localhost:50051" conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { return errors.Wrap(err, "コネクションエラー") } defer conn.Close() client := pb.NewChatClient(conn) return chat(client) } func main() { if err := exec(); err != nil { log.Println(err) } }・server
package main import ( "fmt" "io" "log" "net" "time" pb "grpc-sample/pb/chat" "github.com/pkg/errors" "google.golang.org/grpc" ) const port = ":50051" // ServerBidirectional is server type ServerBidirectional struct { pb.UnimplementedChatServer } func request(stream pb.Chat_ChatServer, message string) error { reply := fmt.Sprintf("%sを受け取ったよ!ありがとう^^", message) return stream.Send(&pb.ChatReply{ Message: reply, }) } // Chat クライアントから受け取った言葉に、言葉を返す func (s *ServerBidirectional) Chat(stream pb.Chat_ChatServer) error { for { in, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } message := in.GetMessage() fmt.Println("受取:", message) if err := request(stream, message); err != nil { return err } time.Sleep(time.Second * 1) } } func set() error { lis, err := net.Listen("tcp", port) if err != nil { return errors.Wrap(err, "ポート失敗") } s := grpc.NewServer() var server ServerBidirectional pb.RegisterChatServer(s, &server) if err := s.Serve(lis); err != nil { return errors.Wrap(err, "サーバ起動失敗") } return nil } func main() { fmt.Println("起動") if err := set(); err != nil { log.Fatalf("%v", err) } }感想
4方式をやってみて感覚が掴めました。
エンドポイントの定義とかしなくて良いし、簡単にストリーミングも使えるしめっちゃ便利だと思った。
サーバ間通信やUnityやスマホアプリで使ってみたい。
- 投稿日:2020-01-25T12:13:27+09:00
goでGRPCのUnary RPCsやってみた(SimpleRPC)
goでgrpcを実装してみました。
今回はまず、Unary RPCs(SimpleRPC)をやります!
他記事で残り3つの方式もやります。
この記事内容を上から行えば、dockerで動く環境が作れます。
dockerで実装してる記事は少ないはず。grpcの4つの通信方式
StreamingとUnaryの組み合わせのため4つある。
詳しくは公式を参考・Unary RPC
一つのデータを受け取って、一つのデータを返す。
動きはよくあるrestAPIと同じ。
用途:サーバ間通信、API。
(Unary : Unary)・Server streaming RPC
サーバがクライアントに複数のリクエストを送る。
用途:サーバから任意のタイミングでクライアントに通知させたい時など。
(Streaming : Unary)・Client streaming RPC
クライアントがサーバに複数のリクエストを送る。
用途:データアップロードや、クライアントから多くのデータを送る場合。
(Unary : Streaming)・Bidirectional streaming RPC
双方向でデータのやりとりをする。
用途:チャットやオンライン対戦など。
(Streaming : Streaming)環境構築
docker環境
・フォルダ構成
以下のフォルダを想定しています。./infrastructure/docker/go-grpc/Dockerfile ./infrastructure/docker/docker-compose.yml・フォルダを作成します。
mkdir -p ./infrastructure/docker/go-grpc cd ./infrastructure/docker touch docker-compose.yml ./go-grpc/Dockerfile・Dockerfile
FROM golang:1.13-stretch SHELL ["/bin/bash", "-c"] RUN apt update && apt-get install -y vim unzip # install protc WORKDIR /protoc RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.2/protoc-3.11.2-linux-x86_64.zip RUN unzip protoc-3.11.2-linux-x86_64.zip RUN ln -s /protoc/bin/protoc /bin/protoc # golang WORKDIR /go-grpc ENV GO111MODULE on RUN go get -u github.com/golang/protobuf/protoc-gen-go・docker-compose.yml
version: "3.7" services: go-grpc: build: context: ./go-grpc/ dockerfile: Dockerfile container_name: "go-grpc" volumes: - ../../:/go-grpc tty: true privileged: true・環境に入る
以下のコマンドでdockerコンテナの中に入ります!cd ./infrastructure/docker docker-compose up -d docker-compose exec go-grpc bash実装
2つの数字を送ったら足算して返すサーバとクライアントを作成します。
protoの作成
以下dockerコンテナに入った状態で作業します。
コンパイラである「protoc」は、dockerコンテナにインストールしてあります。足算を想定したprotoです。
mkdir ./proto touch ./proto/calc.proto・calc.proto
syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.simple"; option java_outer_classname = "SimpleProto"; // package名 package calc; // 上の4つのパターンをここで指定する。 service Calc { rpc Sum (SumRequest) returns (SumReply) {} } // リクエストで送る値。「1、2」はそのデータの番号。 message SumRequest { int32 a = 1; int32 b = 2; } // レスポンスで送る値 message SumReply { string message = 1; }・上の「.protoファイル」を、goファイルにコンパイルします。
以下のコマンドで、./pb/calcフォルダに「calc.pb.go」が自動生成されます。protoc --proto_path ./proto --go_out=plugins=grpc:./pb/calc calc.proto「calc.pb.go」が生成されました。
goで実行
・以下のコマンドで、gomoduleを初期設定をします。
go mod init grpc-sample「受け取る側のサーバー」と「送る側のクライアント」の2つの実行ファイルを作成します。
mkdir server client touch ./server/main.go ./client/main.go・./server/main.go
package main import ( "context" "fmt" "log" "net" pb "grpc-sample/pb/calc" "google.golang.org/grpc" ) const ( port = ":50051" ) type server struct { pb.UnimplementedCalcServer } func (s *server) Sum(ctx context.Context, in *pb.SumRequest) (*pb.SumReply, error) { a := in.GetA() b := in.GetB() log.Printf("%v, %v", a, b) reply := fmt.Sprintf("%d + %d = %d", a, b, a+b) return &pb.SumReply{Message: reply}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterCalcServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("サーバ起動失敗: %v", err) } }・./client/main.go
package main import ( "context" "fmt" "log" "time" "github.com/pkg/errors" pb "grpc-sample/pb/calc" "google.golang.org/grpc" ) func getAdress() string { const ( host = "localhost" port = "50051" ) return fmt.Sprintf("%s:%s", host, port) } func exec(a, b int32) error { address := getAdress() conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { return errors.Wrap(err, "コネクションエラー") } defer conn.Close() client := pb.NewCalcClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() reply, err := client.Sum(ctx, &pb.SumRequest{ A: a, B: b, }) if err != nil { return errors.Wrap(err, "受取り失敗") } log.Printf("サーバからの受け取り\n %s", reply.GetMessage()) return nil } func main() { exec(300, 500) }・サーバを実行します。
go run server/main.goこれでサーバが立ったので、クライアントからデータを送ります。
go run client/main.goサーバ側の表示
上のコマンドを実行したので、
クライアントから、300と500という値を受け取りました。
クライアント側の表示
300, 500という数字を送ったので、800という数字がサーバから返ってきました。
1リクエスト1レスポンスのサーバとクライアンを動かすことができました。
これで、「めっちゃ負荷が高い処理をする機能」があった場合とかにサーバを分割できたりもする。感想
残りの3つの通信方式もやります。
サーバ間通信やunityなどで使ってみたい。
- 投稿日:2020-01-25T07:36:25+09:00
[Go言語]ハッシュキーの簡単な作り方(hmacを使う)
⬛️ はじめに
以前、Go言語で認証機能を作ろう!にて、認証機能を作った。
この記事ではDBに格納したデータとクライアントから送られてきたデータを付き合わせて、認証機能を実現していたが、実際にやりたいことはこんな感じのことではなかった。
うまく説明できないが、ハッシュを作って、ハッシュ同士でチェックし合う?みたいなことを本当はやりたかった。
そんな思いがある中で、ハッシュキーの簡単な作り方を学んだので、メモしておく。
⬛️ hmacについて
hmacを使うと簡単にハッシュを生成することができる。
以下、公式サイトからの引用。
main.go// ValidMAC reports whether messageMAC is a valid HMAC tag for message. func ValidMAC(message, messageMAC, key []byte) bool { mac := hmac.New(sha256.New, key) // ハッシュ作成 mac.Write(message) // ハッシュにデータを書き込む expectedMAC := mac.Sum(nil) // nilを加える(そういう仕様?) return hmac.Equal(messageMAC, expectedMAC) }⬛️ 実装
早速、実装する。
main.gopackage main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" ) // DBと見立てる var LikeDB = map[string]string{ "key1": "password1", "key2": "password2", } // Sesverと見立てる func reciverOnServer(apiKey, sign string, data []byte) { apiPassword := LikeDB[apiKey] h := hmac.New(sha256.New, []byte(apiPassword)) h.Write(data) expectedHMAC := hex.EncodeToString(h.Sum(nil)) fmt.Println(sign == expectedHMAC) } // Clientと見立てる func senderOnClient() { const apiKey = "key1" const apiPassword = "password1" body := []byte("ハッシュを作ろう!") h := hmac.New(sha256.New, []byte(apiPassword)) h.Write(body) signature := hex.EncodeToString(h.Sum(nil)) reciverOnServer(apiKey, signature, body) } func main() { senderOnClient() }今回は簡便に
senderOnClientは、クライアント側のコード。
reciverOnServerは、サーバー側のコード。
LikeDBはDBに見立てたmapデータ。として、main.goに実装した。
senderOnClientでは、クライアントから送信されるapiPasswordを元にハッシュを作成し、apiKeyとともにreciverOnServer(サーバー側)に送信する。
reciverOnsServerは、受け取ったapiKeyを元にDB(LikeDB)からデータを取得し、そのデータをハッシュ化して、senderOnClientから送られてきたハッシュと一致するかチェックする。
一致していたらTrue,そうでない場合はFalseにしてコンソールに出力する流れになる。
⬛️ おわりに
RestAPIなどで、headerに認証用のハッシュコードを埋め込みたいとなった時に、かなり使えると思う。
Djangoなどのフレームワークを使えば、このあたりは用意されているから簡単に実装できるが、Go言語のみでフルスクラッチするときなどは若干「どうすればいいんだっけ?」となるので、結構、重宝しそう。