20200125のGoに関する記事は6件です。

CircleCIでGo言語製のRest APIのテストを自動で回す〜docker-composeを利用して〜

はじめに

こんにちは

コンテナを使ったアプリケーション開発もぼちぼち増えてきたように感じます。
その中でも開発時に複数のコンテナ・プロセスを管理したいためにdocker-composeを利用している方も多いのではないでしょうか。

今回は、開発環境で利用しているdocker-composeをそのまま利用して、
CircleCIで自動テストする一例をご紹介したいと思います。

コンテナ構成

docker-compose.yml
version: '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.go
r.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.go
func 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.yml
version: 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

参考

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を活用したおもしろい例があれば、ぜひご紹介下さい。

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

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
感謝!:pray:

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

goでGRPCのSimpleRPCやってみた(Unary RPC)

スクリーンショット 2020-01-25 12.11.22.png

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

作成されました。
スクリーンショット 2020-01-25 12.01.00.png

・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

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」が生成されました。

スクリーンショット 2020-01-25 12.00.10.png

goで実行

・以下のコマンドで、gomoduleを初期設定をします。

go mod init grpc-sample

以下のファイルが作成されました。
スクリーンショット 2020-01-25 11.58.16.png

「受け取る側のサーバー」と「送る側のクライアント」の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という数字がサーバから返ってきました。
スクリーンショット 2020-01-25 12.05.24.png

サーバ側の表示
クライアントから、300と500という値を受け取りました。
スクリーンショット 2020-01-25 12.05.38.png

この下に、コードの説明も書きます。

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

goでgRPCの4つの通信方式やってみた(Dockerのサンプルあり)

スクリーンショット 2020-01-25 12.46.05.png

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-gateway

gRPCの4つの通信方式

StreamingとUnaryの組み合わせのため4つあります。
以下用途と共に説明します。
公式を参考

・Unary RPC

一つのデータを受け取って、一つのデータを返す。
動きはよくあるrestAPIと同じ。
用途:サーバ間通信、API、アプリとサーバのデータ送受信。
(Unary : Unary)
one.png

・Server streaming RPC

サーバがクライアントに複数のリクエストを送る。
用途:サーバから任意のタイミングでクライアントに通知させたい時など。
(Unary : Streaming)
three.jpg

・Client streaming RPC

クライアントがサーバに複数のリクエストを送る。
用途:データアップロードや、クライアントから多くのデータを送る場合。
(Streaming : Unary)
two.png

・Bidirectional streaming RPC

双方向でデータのやりとりをする。
用途:チャットやオンライン対戦など。
(Streaming : Streaming)
four.jpg

サーバ間通信とかで使う

ある処理がめっちゃ「メモリ使う、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

作成されました。
スクリーンショット 2020-01-25 12.01.00.png

・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

スクリーンショット 2020-01-26 21.11.40.png

実装

上の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」が生成されました。

スクリーンショット 2020-01-25 12.00.10.png

goで実行

・以下のコマンドで、gomoduleを初期設定をします。

go mod init grpc-sample

以下のファイルが作成されました。
スクリーンショット 2020-01-25 11.58.16.png

「受け取る側のサーバー」と「送る側のクライアント」の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という値を受け取りました。
スクリーンショット 2020-01-25 12.05.38.png

クライアント側の表示
300, 500という数字を送ったので、800という数字がサーバから返ってきました。
スクリーンショット 2020-01-25 12.05.24.png

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やスマホアプリで使ってみたい。

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

goでGRPCのUnary RPCsやってみた(SimpleRPC)

スクリーンショット 2020-01-25 12.46.05.png

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

作成されました。
スクリーンショット 2020-01-25 12.01.00.png

・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」が生成されました。

スクリーンショット 2020-01-25 12.00.10.png

goで実行

・以下のコマンドで、gomoduleを初期設定をします。

go mod init grpc-sample

以下のファイルが作成されました。
スクリーンショット 2020-01-25 11.58.16.png

「受け取る側のサーバー」と「送る側のクライアント」の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という値を受け取りました。
スクリーンショット 2020-01-25 12.05.38.png

クライアント側の表示
300, 500という数字を送ったので、800という数字がサーバから返ってきました。
スクリーンショット 2020-01-25 12.05.24.png

1リクエスト1レスポンスのサーバとクライアンを動かすことができました。
これで、「めっちゃ負荷が高い処理をする機能」があった場合とかにサーバを分割できたりもする。

感想

残りの3つの通信方式もやります。
サーバ間通信やunityなどで使ってみたい。

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

[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.go
package 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言語のみでフルスクラッチするときなどは若干「どうすればいいんだっけ?」となるので、結構、重宝しそう。

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