golang+grpcの備忘録

# 始めに
　grpcの[クイックスタート](http://www.grpc.io/docs/quickstart/go.html)と大差ないけど自分用に書いておく。
　ソース：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](https://github.com/labstack/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が解放されるとサーバーが送信先を失っていまいちなエラーが起こります。