go getとかは割愛。こんなファイルを作る。
syntax = "proto3";
option go_package = "proto";
package user;
service UserService {
rpc ListUser(RequestType) returns (stream User) {}
rpc AddUser(User) returns (ResponseType) {}
}
message ResponseType {
}
message RequestType {
}
message User {
string id = 1;
string email = 2;
string name = 3;
Status status = 4;
}
enum Status {
ACTIVE = 0;
INACTIVE = 1;
}
protocコマンドを実行する。引数はprotoファイルのパスだけど、それ以外のオプションがhelp読んでもよくわからんかったorz とりあえず、これでuser.pb.goってライブラリが完成した。
protoc --proto_path=proto --go_out=plugins=grpc:proto-out proto/user.proto
次にserverを実装する。↓を参考にした。
メモがわりのコメント多め。
package main
import (
"log"
"net"
"os"
"os/signal"
"sync"
pb "./proto-out" // ここはgithub上のパス(例:"google.golang.org/grpc/examples/helloworld/helloworld")にしてるっぽいけど一旦こうする
"golang.org/x/net/context"
"google.golang.org/grpc"
)
const (
port = ":50051" // ポート番号
)
// Serverというインターフェースを定義する
type Server struct {
users []*pb.User
m sync.Mutex // ここでMutexを持たせなくてもいいかもしれない(参考:https://qiita.com/h3_poteto/items/3a39c41743b4fd87c134)
}
// ServerインターフェースにListUserメソッドとAddUserメソッドを追加する。
// これでGoのダッグタイピングによって、UserServiceServerを実装したことと同じになる
func (cs *Server) ListUser(p *pb.RequestType, stream pb.UserService_ListUserServer) error {
cs.m.Lock() // ロックする
defer cs.m.Unlock() // deferはこの関数終了時に行う処理を定義する。finalyみたいなもん
for _, p := range cs.users {
if err := stream.Send(p); err != nil {
return err
}
}
return nil
}
func (cs *Server) AddUser(c context.Context, p *pb.User) (*pb.ResponseType, error) {
cs.m.Lock()
defer cs.m.Unlock()
cs.users = append(cs.users, p)
return new(pb.ResponseType), nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("faild to listen: %v", err)
}
server := grpc.NewServer()
// new(T)は、型Tの新しいアイテム用にゼロ化した領域を割り当て、そのアドレスである*T型の値を返す。
// `ゼロ化した領域`はすなわちinitされた領域だと思われる。
// 参考:http://golang.jp/effective_go#allocation_new
pb.RegisterUserServiceServer(server, new(Server))
go func() {
log.Printf("start grpc Server port: %s", port)
server.Serve(lis)
}()
// os.Signal型のチャネルを定義して、
// <-quitのところでブロッキングして、
// quitメッセージが来たらサーバーを停止させている。
quit := make(chan os.Signal)
// 多分、OSからのInterruptが来たら、quitキューに入れる、ってことを定義している
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("stopping grpc Server...")
server.GracefulStop()
}
サーバーを起動してみる。
go run user_server.go 2018/05/13 23:19:52 start grpc Server port: :50051 ^C2018/05/13 23:19:57 stopping grpc Server...
次にクライアントを作る。↓参考にした
gRPC(Go) で API を実装する (フェンリル | デベロッパーズブログ)
package main
import (
"log"
"time"
"golang.org/x/net/context"
"google.golang.org/grpc"
pb "./proto-out"
"io"
"fmt"
)
const (
address = "localhost:50051"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalln("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
user_1 := pb.User{ Id: "doilux1", Email: "doilux1@example.com", Name: "hoge fuga" }
user_2 := pb.User{ Id: "doilux2", Email: "doilux2@example.com", Name: "hoge fuga"}
user_3 := pb.User{ Id: "doilux3", Email: "doilux3@example.com", Name: "hoge fuga"}
c.AddUser(ctx, &user_1)
c.AddUser(ctx, &user_2)
c.AddUser(ctx, &user_3)
stream, err := c.ListUser(ctx, &pb.RequestType{})
if err != nil {
log.Fatalln("could not get user: %v", err)
}
for {
msg, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalln("Receive:", err)
}
fmt.Println(msg)
}
}
実行してみる。サーバーを起動した状態で以下実行する。
go run user_client.go id:"doilux1" email:"doilux1@example.com" name:"hoge fuga" id:"doilux2" email:"doilux2@example.com" name:"hoge fuga" id:"doilux3" email:"doilux3@example.com" name:"hoge fuga"
一応、できたことはできた。
わからなかったこと
ロジックどこに書く?
例えば、「すでに使われているIDは登録できない」とか、そんなルールがあるときにどこに実装するんだろう?やっぱりここになるんだろうか(実際にはどこかに委譲するとおもうけど)
func (cs *Server) AddUser(c context.Context, p *pb.User) (*pb.ResponseType, error) {
cs.m.Lock()
defer cs.m.Unlock()
cs.users = append(cs.users, p)
return new(pb.ResponseType), nil
}
エラーの時どうする?
上記に関連して、ビジネスエラーだったときに一般的にどんなレスポンスを返してるんだろうか。
ということで今日はこれまで
追記
これが参考になりそう
GitHub - harlow/go-micro-services: HTTP up front, Protobufs in the rear