Mac(M1)使ってます。
作りたいサービス
名前送ったら(Cli)、なんか(SV)返ってくるやつ。
具体的には以下の4つのメソッドを作成する。
- 名前を送ると、”Hello 名前”って返ってくる。
- 名前を送ると、”Hello 名前!”, "Hello 名前!!"みたいに「!」が増えて返ってくる。
- 名前を複数回送ると、まとめて"Hello 名前1、名前2、、、"って返ってくる。
- 名前を何個も送ると、送った分"Hello 名前"って返ってくる。
gRPCとは(超簡単に)
ディレクトリ構成
current dir
└──hello
├── client
├── proto
└── server
まずはProtocol Bufferの記述
hello/proto/hello.proto
// バージョン
syntax = "proto3";
// Goのパッケージ名として使われる。
// 別Protoファイルで同じmessageが使用されていた場合に衝突を回避できる。
package hello;
// Goのインポートパス
option go_package = "github.com/hirohokke/grpc-go/hello/proto";
// リクエストを宣言(Goのstructのようなもの)
message HelloRequest {
// 型 フィールド名 = タグ;
string first_name = 1;
}
// レスポンスを宣言(Goのstructのようなもの)
message HelloResponse {
string result = 1;
}
// サービスを宣言
// ここに宣言されたRPCをクライアントから呼び出す
service HelloService {
// リクエストを渡して、レスポンスを返す
rpc Hello (HelloRequest) returns (HelloResponse);
// レスポンスにstreamを付けることでSVは複数レスポンスを返せる
rpc HelloAmp (HelloRequest) returns (stream HelloResponse);
// リクエストにstreamを付けることでクライアントは複数リクエストを渡せる
rpc HelloManyTimes (stream HelloRequest) returns (HelloResponse);
// リクエスト、レスポンスにstreamを付けることで双方向でストリーミングできる
rpc HelloEveryone (stream HelloRequest) returns (stream HelloResponse);
}
protobufについて簡単に超ざっくり説明
- データを構造化してシリアライズするための、Googleによって作られた機構。
- バイナリでシリアライズされるので、サイズが小さくPayloadが小さくて通信が早くなる。(よく使われるJSONやXMLに比べて←プレーンテキスト)
- 型宣言するので、型の安全性が保証される。
- いろんな言語に対応
etc.
protobufのコンパイル
まず、protobufのインストール
$ brew install protobuf
Go用のprotobufランタイムをインストール
$ go get -u google.golang.org/grpc
$ go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
Go用にコンパイル
$ protoc -Ihello/proto \
--go_opt=module=github.com/hirohokke/grpc-go \
--go_out=. \
--go-grpc_opt=module=github.com/hirohokke/grpc-go\
--go-grpc_out=. hello/proto/*.proto
こうすると、hello/proto配下にgoコードが生成される。
-I
は依存関係を解決するみたい。今回はないけど。
Goファイル作成
準備ができたので、Goファイルの作成
※エラーハンドリングしてたりしてなかったり等ありますが、気にしないでください。適当です。
Server
hello/server/main.go
package main
import (
pb "github.com/hirohokke/grpc-go/hello/proto"
"google.golang.org/grpc"
"log"
"net"
)
// gRPCサーバーで受け付けるアドレス
var addr string = "0.0.0.0:50051"
// gRPCサーバーの構造体を宣言
type Server struct {
pb.HelloServiceServer
}
func main() {
// 指定したアドレスでTCPプロトコルで通信受付インスタンス生成
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Failed to listen on: %v\n", err)
}
log.Printf("Listen on %s\n", addr)
// grpcサーバーを生成し、Helloサービスを設定
s := grpc.NewServer()
pb.RegisterHelloServiceServer(s, &Server{})
// サーバー起動
if err = s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v\n", err)
}
}
次に、protobufに記述したRPCサービスを実装
hello/client/hello.go
package main
import (
"context"
"fmt"
pb "github.com/hirohokke/grpc-go/hello/proto" // protobufのインポート
"io"
"log"
"strings"
)
// 1. 名前を送ると、”Hello FirstName”を返す
func (s *Server) Hello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
log.Printf("Helloが呼び出されました: %v\n", in)
// ここでは、文字列を生成し、レスポンスを返すだけ。
return &pb.HelloResponse{
Result: fmt.Sprintf("Hello %s", in.FirstName),
}, nil
}
// 2. !が増幅して返ってくる
func (s *Server) HelloAmp(in *pb.HelloRequest, stream pb.HelloService_HelloAmpServer) error {
log.Printf("HelloAmpが呼び出されました: %v\n", in)
for i := 0; i < 5; i++ {
// レスポンス作成
res := fmt.Sprintf("Hello %s%s", in.FirstName, strings.Repeat("!", i))
// クライアントにレスポンスを送信
err := stream.Send(&pb.HelloResponse{Result: res})
if err != nil {
log.Fatalf("レスポンス送信中にエラーが発生: %v\n", err)
}
}
return nil
}
// 3. 複数回送られてくるリクエストを結合して返す
func (s *Server) HelloManyTimes(stream pb.HelloService_HelloManyTimesServer) error {
log.Println("HelloManyTimesが呼び出されました。")
res := "Hello"
// 無限ループでリクエストを受け取り続ける
for {
// リクエストを取得
req, err := stream.Recv()
// リクエスト終端の場合、レスポンスを送信
if err == io.EOF {
return stream.SendAndClose(&pb.HelloResponse{
Result: res,
})
}
// 終端以外のエラーの場合
if err != nil {
log.Fatalf("リクエスト読み取りでエラー発生: %v\n", err)
}
res = res + " " + req.FirstName
}
}
// 4. リクエストごとに返信する
func (s *Server) HelloEveryone(stream pb.HelloService_HelloEveryoneServer) error {
log.Println("HelloEveryoneが呼び出されました")
for {
// streamからリクエストを受け取り
req, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
log.Fatalf("リクエスト読み取り中にエラー発生: %v\n", err)
}
res := fmt.Sprintf("Hello %s", req.FirstName)
// streamにレスポンスを送信
err = stream.Send(&pb.HelloResponse{Result: res})
if err != nil {
log.Fatalf("レスポンス送信中にエラー発生: %v\n", err)
}
}
}
次にクライアント
Client
hello/client/main.go
package main
import (
pb "github.com/hirohokke/grpc-go/hello/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
)
// 作成したgRPCサーバーのアドレス
var addr string = "localhost:50051"
func main() {
// 作ったサーバーとのコネクタを作成
// ssl認証なし
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("接続失敗: %v\n", err)
}
defer conn.Close()
// gRPCクライアントの生成
c := pb.NewHelloServiceClient(conn)
doHello(c)
doHelloAmp(c)
doHelloManyTimes(c)
doHelloEveryone(c)
}
呼び出し部分
hello/client/hello.go
package main
import (
"context"
pb "github.com/hirohokke/grpc-go/hello/proto"
"io"
"log"
"time"
)
// 1. Hello呼び出し
func doHello(c pb.HelloServiceClient) {
log.Println("---doHello---")
// リクエストを作成
req := &pb.HelloRequest{FirstName: "Hokke"}
// Helloサービス呼び出し
res, err := c.Hello(context.Background(), req)
if err != nil {
log.Fatalf("Helloの呼び出しでエラー: %v\n", err)
}
log.Printf("%s\n", res.Result)
}
// 2. HelloAmp呼び出し
func doHelloAmp(c pb.HelloServiceClient) {
log.Println("---doHelloAmp---")
// リクエスト作成
req := &pb.HelloRequest{FirstName: "Hokke"}
// gRPCとのstream生成
stream, err := c.HelloAmp(context.Background(), req)
if err != nil {
log.Fatalf("HelloAmp呼び出しでエラー: %v\n", err)
}
// EOFが送られるまでレスポンスを取得し続けるため、無限ループ
for {
// レスポンス取得
res, err := stream.Recv()
// レスポンスが終わった場合
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("レスポンス取得でエラー発生: %v\n", err)
}
log.Printf("Response is %s\n", res.Result)
}
}
// 3. HelloManyTimes呼び出し
func doHelloManyTimes(c pb.HelloServiceClient) {
log.Println("---doHelloManyTimes---")
// stream生成
stream, err := c.HelloManyTimes(context.Background())
if err != nil {
log.Fatalf("HelloManyTimes呼び出しでエラー")
}
// リクエスト生成
reqs := []*pb.HelloRequest{
{FirstName: "Hokke"},
{FirstName: "Hiro"},
{FirstName: "Hoge"},
}
// リクエスト数分送信
for _, req := range reqs {
log.Printf("Sending Request: %v\n", req)
stream.Send(req)
}
res, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("レスポンス取得でエラー: %v\n", err)
}
log.Println(res.Result)
}
// 4. HelloEveryone呼び出し
func doHelloEveryone(c pb.HelloServiceClient) {
log.Println("---doHelloEveryone---")
stream, err := c.HelloEveryone(context.Background())
if err != nil {
log.Fatalf("呼び出しでエラー")
}
reqs := []*pb.HelloRequest{
{FirstName: "Hokke"},
{FirstName: "Hiro"},
{FirstName: "Hoge"},
}
// ゴルーチンを使うので、待機チャネルを使用
waitc := make(chan struct{})
go func() {
for _, req := range reqs {
log.Printf("Sending request: %v\n", req)
stream.Send(req)
// わかりやすいように、1s待つ
time.Sleep(1 * time.Second)
}
stream.CloseSend()
}()
go func() {
for {
res, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("レスポンス取得中にエラー: %v\n", err)
}
log.Printf("Response is %s\n", res.Result)
}
close(waitc)
}()
<-waitc
}
実行
サーバー、クライアントそれぞれモジュール作成
$ go build -o bin/server ./hello/server
$ go build -o bin/client ./hello/client
あとは実行するだけ
terminal1
$ ./bin/server
2023/04/26 22:04:55 Listen on 0.0.0.0:50051
terminal2
$ ./bin/client
2023/04/26 22:05:03 ---doHello---
2023/04/26 22:05:03 Hello Hokke
2023/04/26 22:05:03 ---doHelloAmp---
2023/04/26 22:05:03 Response is Hello Hokke
2023/04/26 22:05:03 Response is Hello Hokke!
2023/04/26 22:05:03 Response is Hello Hokke!!
2023/04/26 22:05:03 Response is Hello Hokke!!!
2023/04/26 22:05:03 Response is Hello Hokke!!!!
2023/04/26 22:05:03 ---doHelloManyTimes---
2023/04/26 22:05:03 Sending Request: first_name:"Hokke"
2023/04/26 22:05:03 Sending Request: first_name:"Hiro"
2023/04/26 22:05:03 Sending Request: first_name:"Hoge"
2023/04/26 22:05:03 Hello Hokke Hiro Hoge
2023/04/26 22:05:03 ---doHelloEveryone---
2023/04/26 22:05:03 Sending request: first_name:"Hokke"
2023/04/26 22:05:03 Response is Hello Hokke
2023/04/26 22:05:04 Sending request: first_name:"Hiro"
2023/04/26 22:05:04 Response is Hello Hiro
2023/04/26 22:05:05 Sending request: first_name:"Hoge"
2023/04/26 22:05:05 Response is Hello Hoge
terminal1クライアント実行後
$ ./bin/server
2023/04/26 22:04:55 Listen on 0.0.0.0:50051
2023/04/26 22:05:03 Helloが呼び出されました: first_name:"Hokke"
2023/04/26 22:05:03 HelloAmpが呼び出されました: first_name:"Hokke"
2023/04/26 22:05:03 HelloManyTimesが呼び出されました。
2023/04/26 22:05:03 HelloEveryoneが呼び出されました
クライアントで一気にサービスを呼び出してるけど、別々で呼び出した方が処理わかりやすい。
おまけ
エラーについて
サーバーからクライアントにエラーも返せる
server.go
// 処理
if err != nil {
return status.Errorf(
codes.Internal, // 例)内部エラー
fmt.Sprinf("内部エラー発生"),
)
}
client.go
// 例
err := callGRPC()
if err != nil {
e, ok := status.FromError(err)
if ok {
if e.Code() == codes.Internal {
// エラーハンドリング
} else {
log.Fatalf("Unexpected gRPC error: %v\n", err)
}
} else {
log.Fatalf("A non gRPC error: %v\n", err)
}
}