LoginSignup
10
14

More than 5 years have passed since last update.

golang+grpcの備忘録

Last updated at Posted at 2017-04-04

始めに

 grpcのクイックスタートと大差ないけど自分用に書いておく。
 ソース:https://github.com/lightstaff/grpc_test

GRPCの準備

 ここはさすがに上記のクイックスタート通りで…

protocolの準備

 **.protoに定義を書く。今回は単純にHelloって文字列を持つstructを返すサービス(GetHello)とstreamを使って受けた文字列を大文字化して返すサービス(UpperCharacters)を定義する。
 インポートしている[github.com/gogo/protobuf/gogoproto/gogo.proto]はgeneratorを拡張して色々してくれるので便利です(以下省略)。

protobuf.proto

syntax = "proto3";

package gprc_test;

// 色々便利
import "github.com/gogo/protobuf/gogoproto/gogo.proto";

option go_package = "protobuf";
option (gogoproto.marshaler_all) = true;
option (gogoproto.sizer_all) = true;
option (gogoproto.unmarshaler_all) = true;
option (gogoproto.goproto_getters_all) = false;

// サービス定義
service GRPCTestServcie {
    // Helloと返すだけのサービス
    rpc GetHello(Empty) returns (ReplyModel) {}

    // stream経由で受けた文字列を大文字化して返すサービス
    rpc UpperCharacters(stream ReqModel) returns (stream ReplyModel) {}
}

// 空
message Empty {}

// Request
message ReqModel {
    string message = 1;
}

// Replay
message ReplyModel {
    string result = 1;
}

上記を
protoc --proto_path=$GOPATH/src:$GOPATH/src/github.com/gogo/protobuf/protobuf:. --gofast_out=plugins=grpc:. ./protobuf/protobuf.proto
でgoのプロトコル(protobuf/protobuf.pb.go)ができます。

サーバーサイド

 protobuf/protobuf.pb.goを参照し、サーバーを書きます。

 現在、1.6だったか1.7だったかで標準化されたcontextは使えません。golang.org/x/net/contextを使う必要があります。混同に注意。

service.go

package main

import (
    "io"
    "strings"

    pb "github.com/lightstaff/grpc_test/protobuf"

    netCtx "golang.org/x/net/context"
)

// Service model
type Service struct{}

// 単純にHelloと返す
func (s *Service) GetHello(ctx netCtx.Context, e *pb.Empty) (*pb.ReplyModel, error) {
    return &pb.ReplyModel{
        Result: "Hello",
    }, nil
}

// stream経由で受けた文字列を大文字化して返す
func (s *Service) UpperCharacters(stream pb.GRPCTestServcie_UpperCharactersServer) error {
    for {
        // streamが終了するまで受信し続ける
        req, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }

        // 受けたReqModelから大文字化してstreamにReplyModelを送信
        if err := stream.Send(&pb.ReplyModel{
            Result: strings.ToUpper(req.Message),
        }); err != nil {
            return err
        }
    }

    return nil
}

main.go

package main

import (
    "net"
    "os"
    "os/signal"
    "syscall"

    pb "github.com/lightstaff/grpc_test/protobuf"

    "google.golang.org/grpc"
)

func main() {
    g := grpc.NewServer()
    s := &Service{}

    pb.RegisterGRPCTestServcieServer(g, s)

    errC := make(chan error)

    go func() {
        lis, err := net.Listen("tcp", ":18080")
        if err != nil {
            errC <- err
        }

        if err := g.Serve(lis); err != nil {
            errC <- err
        }
    }()

    quitC := make(chan os.Signal)
    signal.Notify(quitC, syscall.SIGINT, syscall.SIGTERM)

    select {
    case err := <-errC:
        panic(err)
    case <-quitC:
        g.Stop()
    }
}

 これでlocalhost:18080にダイアルすることができるようになります。

クライアントサイド

 クライアントはWebサービスを想定し、echoを使います。

controller.go

package main

import (
    "io"
    "net/http"

    pb "github.com/lightstaff/grpc_test/protobuf"

    "github.com/labstack/echo"
    netCtx "golang.org/x/net/context"
)

// Heloと返すだけ
func GetHello(c echo.Context) error {
    sc, ok := c.(*ServiceContext)
    if !ok {
        return echo.NewHTTPError(http.StatusBadRequest, "コンテキストが取得できません")
    }

    rep, err := sc.ServiceClient.GetHello(netCtx.Background(), &pb.Empty{})
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "reply": rep.Result,
    })
}

// stream経由で受けた文字列を大文字化して返すサービスを呼び出してやりとり
func UpperCharacters(c echo.Context) error {
    sc, ok := c.(*ServiceContext)
    if !ok {
        return echo.NewHTTPError(http.StatusBadRequest, "コンテキストが取得できません")
    }

    type bodyModel struct {
        Messages []string `json:"messages"`
    }

    // JSONを変換
    var m bodyModel
    if err := c.Bind(&m); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // streamを生成
    stream, err := sc.ServiceClient.UpperCharacters(netCtx.Background())
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // 受信はgoroutineで
    errC := make(chan error)
    resultC := make(chan *pb.ReplyModel)
    doneC := make(chan struct{})
    go func() {
        defer func() {
            close(errC)
            close(resultC)
            close(doneC)
        }()

        for {
            res, err := stream.Recv()
            if err == io.EOF {
                break
            }
            if err != nil {
                errC <- err
                return
            }

            resultC <- res
        }
    }()

    // 文字列をsteamに送る
    for _, message := range m.Messages {
        if err := stream.Send(&pb.ReqModel{
            Message: message,
        }); err != nil {
            return echo.NewHTTPError(http.StatusBadRequest, err.Error())
        }
    }

    if err := stream.CloseSend(); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // この辺もうちょっとスマートに書きたい…
    results := make([]string, 0)
    for {
        select {
        case err := <-errC:
            if err != nil {
                return echo.NewHTTPError(http.StatusBadRequest, err.Error())
            }
        case result := <-resultC:
            if result != nil {
                results = append(results, result.Result)
            }
        case <-doneC:
            return c.JSON(http.StatusOK, map[string]interface{}{
                "results": results,
            })
        }
    }
}

main.go

package main

import (
    "context"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    pb "github.com/lightstaff/grpc_test/protobuf"

    "google.golang.org/grpc"
    "github.com/labstack/echo"
)

type ServiceContext struct {
    echo.Context
    ServiceClient pb.GRPCTestServcieClient
}

// このMiddlewareでGRPCにダイアル
func serviceContextMiddleware(grpcAddr string) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            cc, err := grpc.Dial(grpcAddr, grpc.WithBlock(), grpc.WithInsecure())
            if err != nil {
                return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
            }
            defer cc.Close()

            sc := &ServiceContext{
                Context:       c,
                ServiceClient: pb.NewGRPCTestServcieClient(cc),
            }

            return next(sc)
        }
    }
}

func main() {
    e := echo.New()

    e.Use(serviceContextMiddleware("localhost:18080"))

    e.GET("/hello", GetHello)
    e.POST("/upper-characters", UpperCharacters)

    errC := make(chan error)
    go func() {
        if err := e.Start(":8080"); err != nil {
            errC <- err
        }
    }()

    quitC := make(chan os.Signal)
    signal.Notify(quitC, syscall.SIGINT, syscall.SIGTERM)

    select {
    case err := <-errC:
        panic(err)
    case <-quitC:
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        if err := e.Shutdown(shutdownCtx); err != nil {
            errC <- err
        }
    }
}

 /upper-charactersにはJSONで'{"messages:["aaa","bbb","ccc"]}'と渡してあげてください。
 ちなみにサーバーからReplyModelが帰ってくる前にメソッドが終了し、ServiceClientが解放されるとサーバーが送信先を失っていまいちなエラーが起こります。

10
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
14