こんにちは。ナナオです。
今回は触ろうと思って触ってなかったgRPCに入門してみようと思います。
gRPCとは?
gRPCとは、GoogleがRPCを実現するために作った技術のことです。
詳細については以下のサイトがかなり細かく説明してくれています。
gRPCサーバーを動かす
まずはgRPCのリクエストとレスポンスを定義するためのprotoファイルを作成します。
適当なディレクトリを作成して、Goモジュールとして初期化しておきましょう。
mkdir go_grpc_playground && cd go_grpc_playground
go mod init go_grpc_playground
次に、protoファイルを作成します。
(参考にしてるサイトをパクってるだけですが。。)
// protoのバージョンの宣言
syntax = "proto3";
// protoファイルから自動生成させるGoのコードの置き先
option go_package = "pkg/grpc";
// packageの宣言
package myapp;
// サービスの定義
service GreetingService {
// サービスが持つメソッドの定義
rpc Hello (HelloRequest) returns (HelloResponse);
}
// 型の定義
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
次にこのprotoファイルからgoファイルを作成するためのモジュールをインストールします。
brewがあれば以下のコマンドで一発インストールできます。
brew install protobuf
インストールするとprotoc
というコマンドが使用可能になります。
続けて、必要なモジュールを依存関係に追加しておきます。
go get -u google.golang.org/grpc
go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
さて、今のディレクトリはこうなっています。
.
├── go.mod
├── go.sum
└── main.proto
この状態でprotoc
コマンドを使用してgoファイルを生成しようとすると…
❯ protoc main.proto
Missing output directives.
怒られてしまいました。
これはproto
ファイルに定義したoption go_package = "pkg/grpc";
のpkg/grpc
というディレクトリが存在しないからですね。
ディレクトリを作って再度挑戦してみます。
❯ mkdir pkg && mkdir pkg/grpc
❯ protoc main.proto
Missing output directives.
…どうやら関係なかったようです。
--go_out
オプションを指定しないとダメっぽいですね。
protobufとgrpcのGoコード生成先ディレクトリの指定を、protocコマンドのオプションで行う
ということで、指定して再チャレンジ!
❯ protoc --go_out=./pkg/grpc main.proto;
protoc-gen-go: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--go_out: protoc-gen-go: Plugin failed with status code 1.
今度は違うエラーが出てしまいました。
protoc-gen-go
というプログラムがインストールされていないからのようです。
これもbrewでインストールしておきましょう。
brew install protoc-gen-go
では改めて再チャレンジ!
protoc --go_out=./pkg/grpc main.proto
今度は何も出ないので成功したようです。
ディレクトリ構成を見てみましょう。
.
├── go.mod
├── go.sum
├── main.proto
└── pkg
└── grpc
└── pkg
└── grpc
└── main.pb.go
おお、できてる。
でもなんか変な感じで出来ちゃったので、一回消しましょう。
❯ rm -rf p
❯ protoc --go_out=. main.proto
❯ tree
.
├── go.mod
├── go.sum
├── main.proto
└── pkg
└── grpc
└── main.pb.go
うん、今度はいい感じ。
さて、このmain.pb.goの中身はどうなっているでしょう?
// protoのバージョンの宣言
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.2
// protoc v5.28.3
// source: main.proto
// packageの宣言
package grpc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// 型の定義
type HelloRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *HelloRequest) Reset() {
*x = HelloRequest{}
mi := &file_main_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HelloRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloRequest) ProtoMessage() {}
func (x *HelloRequest) ProtoReflect() protoreflect.Message {
mi := &file_main_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
func (*HelloRequest) Descriptor() ([]byte, []int) {
return file_main_proto_rawDescGZIP(), []int{0}
}
func (x *HelloRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type HelloResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *HelloResponse) Reset() {
*x = HelloResponse{}
mi := &file_main_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HelloResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloResponse) ProtoMessage() {}
func (x *HelloResponse) ProtoReflect() protoreflect.Message {
mi := &file_main_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead.
func (*HelloResponse) Descriptor() ([]byte, []int) {
return file_main_proto_rawDescGZIP(), []int{1}
}
func (x *HelloResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_main_proto protoreflect.FileDescriptor
var file_main_proto_rawDesc = []byte{
0x0a, 0x0a, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6d, 0x79,
0x61, 0x70, 0x70, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x29, 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x32, 0x45, 0x0a, 0x0f, 0x47, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x13,
0x2e, 0x6d, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6d, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x48, 0x65, 0x6c, 0x6c,
0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0a, 0x5a, 0x08, 0x70, 0x6b, 0x67,
0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_main_proto_rawDescOnce sync.Once
file_main_proto_rawDescData = file_main_proto_rawDesc
)
func file_main_proto_rawDescGZIP() []byte {
file_main_proto_rawDescOnce.Do(func() {
file_main_proto_rawDescData = protoimpl.X.CompressGZIP(file_main_proto_rawDescData)
})
return file_main_proto_rawDescData
}
var file_main_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_main_proto_goTypes = []any{
(*HelloRequest)(nil), // 0: myapp.HelloRequest
(*HelloResponse)(nil), // 1: myapp.HelloResponse
}
var file_main_proto_depIdxs = []int32{
0, // 0: myapp.GreetingService.Hello:input_type -> myapp.HelloRequest
1, // 1: myapp.GreetingService.Hello:output_type -> myapp.HelloResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_main_proto_init() }
func file_main_proto_init() {
if File_main_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_main_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_main_proto_goTypes,
DependencyIndexes: file_main_proto_depIdxs,
MessageInfos: file_main_proto_msgTypes,
}.Build()
File_main_proto = out.File
file_main_proto_rawDesc = nil
file_main_proto_goTypes = nil
file_main_proto_depIdxs = nil
}
…想像以上に複雑でしたね。
protoファイルに書かれていた内容を具現化するとこうなるということでしょう。
では次に、この定義からgRPCサーバーを起動させてみましょう。
package main
import (
"fmt"
"log"
"net"
"os"
"os/signal"
"google.golang.org/grpc"
)
func main() {
// 1. 8080番portのListenerを作成
port := 8080
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
panic(err)
}
// 2. gRPCサーバーを作成
s := grpc.NewServer()
// 3. 作成したgRPCサーバーを、8080番ポートで稼働させる
go func() {
log.Printf("start gRPC server port: %v", port)
s.Serve(listener)
}()
// 4.Ctrl+Cが入力されたらGraceful shutdownされるようにする
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("stopping gRPC server...")
s.GracefulStop()
}
これをmain.go
で保存しておきます。
ただ、google.golang.org/grpc
を依存関係に追加する際、Goのバージョンが>=1.22じゃないと怒られてしまったので、goenvでバージョンを固定しておきました。
goenv install 1.22.9
goenv local 1.22.9
また、go.modの定義も変更しておきます。
module go_grpc_playground
go 1.22 # 1.20 -> 1.22に変更
これでパッケージの追加がうまくいくはずです。
go add google.golang.org/grpc
ではサーバーを実行してみましょう。
❯ go run .
2024/11/18 22:37:56 start gRPC server port: 8080
^C2024/11/18 22:38:46 stopping gRPC server...
無事起動しました。
ただ今のままでは何もしないサーバーなので、ハンドラを追加します。
ここにきて気づいたのですが、サービスを実装した構造体が今のままだとないので追加します。
構造体を作成するために必要なパッケージをインストールします。
brew install protoc-gen-go-grpc
(私は一旦brewに統一していますが、go installでもできるっぽいです)
再度生成コマンドを実行します。その際にオプションとして--go-grpc_out
をつけてあげないといけないみたいです。
protoc --go_out=. --go-grpc_out=. main.proto
これでサービスを実装した構造体が出来上がりました。
.
├── go.mod
├── go.sum
├── main.go
├── main.proto
└── pkg
└── grpc
├── main.pb.go
└── main_grpc.pb.go # このファイルが追加された
では動く形にしていきましょう。
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"
pb "go_grpc_playground/pkg/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
type myServer struct {
pb.UnimplementedGreetingServiceServer
}
func NewMyServer() *myServer {
return &myServer{}
}
func (s *myServer) Hello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
// リクエストからnameフィールドを取り出して
// "Hello, [名前]!"というレスポンスを返す
return &pb.HelloResponse{
Message: fmt.Sprintf("Hello, %s!", req.GetName()),
}, nil
}
func main() {
// 8080番portのListenerを作成
port := 8080
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
panic(err)
}
// gRPCサーバーを作成
s := grpc.NewServer()
// サービスを登録
pb.RegisterGreetingServiceServer(s, NewMyServer())
// gRPCurlのためにサーバーリフレクションを設定
reflection.Register(s)
go func() {
log.Printf("start gRPC server port: %v", port)
s.Serve(listener)
}()
// 4.Ctrl+Cが入力されたらGraceful shutdownされるようにする
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("stopping gRPC server...")
s.GracefulStop()
}
これで動く形になりました。
再度サーバーを実行します。
❯ go run .
2024/11/18 23:16:42 start gRPC server port: 8080
起動しました。
ではgRPCをcurlのように確認できるツールをインストールして、レスポンスを見てみます。
brew install grpcurl
では早速レスポンスが返却されるか見てみます。
❯ grpcurl -plaintext -d '{"name": "hsaki"}' localhost:8080 myapp.GreetingService.Hello
{
"message": "Hello, hsaki!"
}
動いていますね!!!
感想
ということで、結構駆け足でしたが初めて動かしてみました。
次回はクライアントの作成をしてみるよ。お楽しみに~