はじめに
こちらは、Go4 Advent Calendar 2019 の24日目の記事です。
こんにちは、最近Go言語にはまっているエンジニアです。
この前は別のアドベントカレンダーでGobotを使ってドローンを飛ばす記事を作成しました。
Go言語のフレームワークGobotでドローンを制御してみた。
今回は、最近入門したgRPCについて書いていきたいと思います。
gRPCとは
Googleが開発したRPC呼出プロトコルで、Protocol Buffersを使うことで、REST APIより高速で堅牢な通信を実現できる点が特長です。メッセージはProtocolBuffersを用いて通信を行い、HTTP/2を用いて並列呼出、双方向呼出、ストリーミングなどが可能となっています。
.protoファイルでサーバー側、クライアント側の雛形コードを作成し、それを元に様々な言語のコードを自動生成することができます(今回はGo言語)。この雛形からの自動生成によってAPIの仕様を半ば強制に明文化することが可能になっています。
さらにgRPCでは、クライアントアプリケーションは別のマシン上のサーバーアプリケーションのメソッドをローカルオブジェクトのように直接呼び出すことができるため、分散型のアプリケーションやサービスを簡単に作成できます。
また、別々の言語を持ったシステム同士をつなぐことも容易です。
gRPCのRPC方式
gRPCは通信方法にHTTP/2を使用いるので、一般的なRPCにおける1Request-1Responseだけでなく、1つのTCPコネクションの中で複数のRequest/Responseをやり取りすることが可能となっており、下記の四つの方式に分かれます
- Unary(Simple)
1 request-1 response - ServerStreaming
1 request- N response - ClientStreaming
N request-1 response - BidirectionalStreaming
N request-N response
では今回は基本的な1req-1res方式と、1req-Nresを用いてCRUD+α機能を実装していきます。
環境構築
Go言語の環境が整っている前提でいきます。
gRPCの環境自体はこの2つをインストールするのみです。
gRPCの環境構築
#grpcのインストール
$ go get -u google.golang.org/grpc
#Protocol Bufferのインストール
$ go get -u github.com/golang/protobuf/protoc-gen-go
また、今回はDBとしてMongoDBを利用するので、MongoDBをインストールします。
MongoDBのインストール
https://www.mongodb.com/jp
こちらのサイトからダウンロードとインストールをしてください。
また下記のgithubよりmogo-go-driverをインストールし、パッケージを取得します。(depを使います。)
https://github.com/mongodb/mongo-go-driver
下記コマンドを実行すルコとでインストールが可能です。
dep ensure -add "go.mongodb.org/mongo-driver/mongo"
まずprotoファイルを作成し、APIの基礎となるコードを実装していきます。
今回は簡易ツイッターのようなid,user_id,contentを持ったGweetをCreate,Read,Delete,Updateとgweet全取得の機能を作成していきます。
protoファイル
基本的には型を設定してそれを用いて、リクエストとレスポンスの方式を決めていくため、シンプルなコードになっています。
syntax = "proto3";
package gwitter;
option go_package = "gwitterpb";
//Gweetの型を決めます。各フィールドは型と名称を持ちます。
message Gweet {
string id = 1;
string user_id = 2;
string content = 3;
}
message PostGwitterRequest{
Gweet gweet = 1;
}
message PostGwitterResponse{
Gweet gweet = 1;
}
message ReadGwitterRequest{
string gweet_id = 1;
}
message ReadGwitterResponse {
Gweet gweet = 1;
}
message UpdateGwitterRequest{
Gweet gweet = 1;
}
message UpdateGwitterResponse {
Gweet gweet = 1;
}
message DeleteGwitterRequest {
string gweet_id = 1;
}
message DeleteGwitterResponse {
string gweet_id = 1;
}
//全取得の時は特に何も指定しない
message ListGwitterRequest {
}
message ListGwitterResponse {
Gweet gweet = 1;
}
service GweetService{
rpc PostGwitter (PostGwitterRequest) returns (PostGwitterResponse);
rpc ReadGwitter (ReadGwitterRequest) returns (ReadGwitterResponse);
rpc UpdateGwitter (UpdateGwitterRequest) returns (UpdateGwitterResponse);
rpc DeleteGwitter (DeleteGwitterRequest) returns (DeleteGwitterResponse); // return NOT_FOUND if not found
rpc ListGwitter (ListGwitterRequest) returns (stream ListGwitterResponse);
}
protoファイルを作成後、下記コマンドを叩くだけで、各メッセージ型に対する型を含んだ .pb.go ファイルが作成されます。
protoファイルから雛形コードの生成
次に上記のprotoファイルからコードを生成していきます。
以下のコマンドを叩いてください。
protoc calculator/proto/calculator.proto --go_out=plugins=grpc:.
長いのでここでは割愛しますが、ファイルが作成されます。文頭にコメントで「DO NOT EDIT」と書かれているように、このファイルには絶対に手を加えてはいけません。これを基本として、Client&Server側を実装していきます。
コードを見たい方はこちら
https://github.com/waytkheming/gwitter-proto/blob/master/gwitterpb/gwitter.pb.go
土台となるServer,Clientのコードを実装
- Server側
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"
"github.com/waytkheming/grpc-go-course/gwitter/gwitterpb"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var collection *mongo.Collection
type server struct {
}
func main() {
//if crash the code, get the file name and line number
log.SetFlags(log.LstdFlags | log.Lshortfile)
// client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://localhost:27017"))
// connect to database
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
err = client.Connect(context.TODO())
if err != nil {
log.Fatal(err)
}
fmt.Println("Blog Service Started")
collection = client.Database("mydb").Collection("gwitter")
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
opts := []grpc.ServerOption{}
s := grpc.NewServer(opts...)
gwitterpb.RegisterGweetServiceServer(s, &server{})
go func() {
fmt.Println("Starting server ....")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}()
//Wait for Control C to Exit
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
<-ch
fmt.Println("Stopping the server")
s.Stop()
fmt.Println("Close the listener")
lis.Close()
fmt.Println("Closeing connection")
client.Disconnect(context.TODO())
fmt.Println("end of program")
}
- Client側
package main
import (
"fmt"
"io"
"log"
"context"
"github.com/waytkheming/grpc-go-course/gwitter/gwitterpb"
"google.golang.org/grpc"
)
func main() {
fmt.Println("Hello from Gwitter client")
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("could not connect:%v", err)
}
defer conn.Close()
}
Create
まずはCreate機能から作成して行きます。
- Server側
//型を定義(bsonはMongoDB特有のフォーマットです。)
type gwitterItem struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
UserID string `bson:"user_id"`
Content string `bson:"content"`
}
//Post(Create)メソッドです。
func (*server) PostGwitter(ctx context.Context, req *gwitterpb.PostGwitterRequest) (*gwitterpb.PostGwitterResponse, error) {
fmt.Println("Post Gweet invoked")
gweet := req.GetGweet()
data := gwitterItem{
UserID: gweet.GetUserId(),
Content: gweet.GetContent(),
}
//MongoDBへ保存
res, err := collection.InsertOne(context.Background(), data)
if err != nil {
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Internal error: %v", err),
)
}
oid, ok := res.InsertedID.(primitive.ObjectID)
if !ok {
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Cannnot convert to OID "),
)
}
return &gwitterpb.PostGwitterResponse{
Gweet: &gwitterpb.Gweet{
Id: oid.Hex(),
UserId: gweet.GetUserId(),
Content: gweet.GetContent(),
},
}, nil
}
- Client側
c := gwitterpb.NewGweetServiceClient(conn)
gweet := &gwitterpb.Gweet{
UserId: "waytkheming",
Content: "First Gweet",
}
createGweetRes, err := c.PostGwitter(context.Background(), &gwitterpb.PostGwitterRequest{Gweet: gweet})
if err != nil {
log.Fatalf("Unexpected Error %v: \n", err)
}
fmt.Printf("Gweet has been gweeted: %v \n", createGweetRes)
Read
- Server側
func (*server) ReadGwitter(ctx context.Context, req *gwitterpb.ReadGwitterRequest) (*gwitterpb.ReadGwitterResponse, error) {
fmt.Println("Read Gweet invoked")
gweetID := req.GetGweetId()
oid, err := primitive.ObjectIDFromHex(gweetID)
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"))
}
// create empty struct
data := &gwitterItem{}
filter := bson.M{"_id": oid}
res := collection.FindOne(context.Background(), filter)
if err := res.Decode(data); err != nil {
return nil, status.Errorf(
codes.NotFound,
fmt.Sprintf("cannnot fing gweet with this id: %v", err),
)
}
return &gwitterpb.ReadGwitterResponse{
Gweet: dataToGweetPb(data),
}, nil
}
func dataToGweetPb(data *gwitterItem) *gwitterpb.Gweet {
return &gwitterpb.Gweet{
Id: data.ID.Hex(),
UserId: data.UserID,
Content: data.Content,
}
}
- Client側
gweetID := createGweetRes.GetGweet().GetId()
// read gwitter
fmt.Println("Reading the gwitter")
_, err2 := c.ReadGwitter(context.Background(), &gwitterpb.ReadGwitterRequest{GweetId: "waytkheming"})
if err2 != nil {
fmt.Printf("Error happened WHILE READING: %v \n", err2)
}
readGweetReq := &gwitterpb.ReadGwitterRequest{GweetId: gweetID}
readGweetRes, readGweetError := c.ReadGwitter(context.Background(), readGweetReq)
if readGweetError != nil {
fmt.Printf("Error happened WHILE READING: %v \n", readGweetError)
}
fmt.Printf("Gweet was read: %v \n", readGweetRes)
Update
- Server側
func (*server) UpdateGwitter(ctx context.Context, req *gwitterpb.UpdateGwitterRequest) (*gwitterpb.UpdateGwitterResponse, error) {
fmt.Println("Update Gweet invoked")
gweet := req.GetGweet()
oid, err := primitive.ObjectIDFromHex(gweet.GetId())
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"))
}
// create empty struct
data := &gwitterItem{}
filter := bson.M{"_id": oid}
res := collection.FindOne(context.Background(), filter)
if err := res.Decode(data); err != nil {
return nil, status.Errorf(
codes.NotFound,
fmt.Sprintf("cannnot fing gweet with this id: %v", err),
)
}
data.UserID = gweet.GetUserId()
data.Content = gweet.GetContent()
_, updateErr := collection.ReplaceOne(context.Background(), filter, data)
if updateErr != nil {
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Cannot update object in MongoDB: %v", updateErr),
)
}
return &gwitterpb.UpdateGwitterResponse{
Gweet: dataToGweetPb(data),
}, nil
}
- Client側
newGweet := &gwitterpb.Gweet{
Id: gweetID,
UserId: "changeMan",
Content: "Editted content",
}
updateRes, updateErr := c.UpdateGwitter(context.Background(), &gwitterpb.UpdateGwitterRequest{Gweet: newGweet})
if updateErr != nil {
fmt.Printf("Error happened WHILE updateting: %v \n", readGweetError)
}
fmt.Printf("Gweet was updated: %v \n", updateRes)
Delete
- Server側
func (*server) DeleteGwitter(ctx context.Context, req *gwitterpb.DeleteGwitterRequest) (*gwitterpb.DeleteGwitterResponse, error) {
fmt.Println("Delete Gweet invoked")
oid, err := primitive.ObjectIDFromHex(req.GetGweetId())
if err != nil {
return nil,
status.Error(
codes.InvalidArgument,
fmt.Sprintf("Cannnot parse your gweet id"))
}
filter := bson.M{"_id": oid}
res, err := collection.DeleteOne(context.Background(), filter)
if err != nil {
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Internal error: %v", err),
)
}
if res.DeletedCount == 0 {
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Internal error: %v", err),
)
}
return &gwitterpb.DeleteGwitterResponse{GweetId: req.GetGweetId()}, nil
}
- Client側
fmt.Println("Deleting the gwitter")
deleteGweetRes, deleteGweetError := c.DeleteGwitter(context.Background(), &gwitterpb.DeleteGwitterRequest{GweetId: gweetID})
if deleteGweetError != nil {
fmt.Printf("Error happened WHILE READING: %v \n", deleteGweetError)
}
fmt.Printf("Gweet was deleted: %v \n", deleteGweetRes)
List
ここではServer Streaming方式を用いることになります。
- Server側
//引数であるstreamがNつのレスポンスを返す役割を担います。
func (*server) ListGwitter(req *gwitterpb.ListGwitterRequest, stream gwitterpb.GweetService_ListGwitterServer) error {
fmt.Println("List gwitter request")
list, err := collection.Find(context.Background(), primitive.D{{}})
if err != nil {
return status.Errorf(
codes.Internal,
fmt.Sprintf("Unknown internal error: %v", err),
)
}
defer list.Close(context.Background())
//for文を回して一つ一つ項目を取得して行きます。
for list.Next(context.Background()) {
data := &gwitterItem{}
err := list.Decode(data)
if err != nil {
return status.Errorf(codes.Internal,
fmt.Sprintf("Error while decoding data from MongoDB: %v", err),
)
}
stream.Send(&gwitterpb.ListGwitterResponse{Gweet: dataToGweetPb(data)})
if err := list.Err(); err != nil {
return status.Errorf(codes.Internal,
fmt.Sprintf("Error while decoding data from MongoDB: %v", err),
)
}
}
return nil
}
- Client側
stream, err := c.ListGwitter(context.Background(), &gwitterpb.ListGwitterRequest{})
for {
res, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("somethig wrong things happened: %v", err)
}
fmt.Println(res.GetGweet())
}
終わりに
ここで実装は終わりです。実際に動作を行うのは、Server側、Client側それぞれでrunさせてみてください。
MongoDBへデータのやり取りが行われるはずです。
長い記事でしたが最後まで読んでくださりありがとうございました。
想定よりもコード量が多くなってしまいましたので、今回記載したコードはまとめてこちらに掲載します。
https://github.com/waytkheming/gwitter-proto
よろしければ参考にしてみてください。
参考サイト
https://grpc.io/
https://qiita.com/muroon/items/1c9ad59653c00d8d5e3d
https://www.udemy.com/course/grpc-golang/