はじめに
gRPCでクライアントストリーミングするサンプルです.
サンプルコード内でところどころモジュール名github.com/Tsuyopon-1067/grpc-client-streaming-test
が登場しますがよしなに読み替えてください.
このサンプルでは以下のような通信をします.
準備
Go プラグインをインストール
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Protocol Buffers をインストール.
$ brew install protobuf
好きな作業用ディレクトリも用意しておいてください.例ではコマンドでディレクトリを作成していますがもちろんGUIで作っても問題ありません.
mkdir hogehoge
Goのモジュールを作成
$ go mod init <好きなモジュール名>
この先の例は次のコマンドでモジュールを作った前提です.別の名前のモジュールを作った場合はその名前に読み替えてください(つまりコピペオンリーだと動かない).
$ go mod init github.com/Tsuyopon-1067/grpc-client-streaming-test
プログラム
最終的にはこのようなディレクトリになります.
.
├── client
│ └── main.go
├── go.mod
├── go.sum
├── scantext
│ ├── scantext.pb.go
│ ├── scantext.proto
│ └── scantext_grpc.pb.go
└── server
└── main.go
proto
スキーマを以下のように定義します.ファイルは上のディレクトリ図の場所に作成します.
syntax = "proto3";
option go_package = "github.com/Tsuyopon-1067/grpc-client-streaming-test/scantext";
package scantext;
import "google/protobuf/empty.proto";
service Sender {
rpc SendText(ScanText) returns (google.protobuf.Empty) {}
}
message ScanText { string content = 1; }
以下のコマンドをscantext
の外(後で作るserver
やclient
と同じディレクトリ)で実行してgoファイルを生成します.
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
scantext/scantext.proto
開くとコード生成コマンドの意味が見れます
-
--go_out=
- goコード生成
-
.
- 出力先はカレントディレクトリ
-
--go_opt=
- コード生成オプション
-
paths=source_relative
- 生成されるファイルのパスを
.proto
ファイルからの相対パスにする
- 生成されるファイルのパスを
-
--go-grpc_out=
- gRPC用のGoコードを生成する
-
.
- 出力先はカレントディレクトリ
-
-go-grpc_opt=
- gRPC Go コードの生成オプション
-
paths=source_relative
- 生成されるファイルのパスを
.proto
ファイルからの相対パスにする
- 生成されるファイルのパスを
-
scantext/scantext.proto
-
.proto
ファイルの場所
-
わざわざ通常のgo用コードとgRPC用のコード両方を生成しているので行かのようにgRPC用のコードだけ生成すれば良さそうに見えますが,通常のGoコードも使うのでgRPC用のコードの生成だけでは足りません.
$ protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative \
scantext/scantext.proto
↑これは実行しないので注意(多分実行しても悪いことは起きないけど)
サーバ
サーバのコードを以下のように書きます.ファイルは上のディレクトリ図の場所に作成します.
package main
import (
"context"
"fmt"
"log"
"net"
"github.com/Tsuyopon-1067/grpc-client-streaming-test/scantext"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
type server struct {
scantext.UnimplementedSenderServer
}
// クライアントから呼び出される関数
func (s *server) SendText(ctx context.Context, in *scantext.ScanText) (*emptypb.Empty, error) {
log.Printf("Received: %v", in.Content)
return &emptypb.Empty{}, nil
}
func main() {
lis, err := net.Listen("tcp", ":8080") // 接続を待ち受ける
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer() // gRPCサーバーを作成
scantext.RegisterSenderServer(s, &server{}) // サーバーにサービスを登録
fmt.Println("Server is running on :8080")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
クライアント
クライアントのコードを以下のように書きます.ファイルは上のディレクトリ図の場所に作成します.
package main
import (
"context"
"fmt"
"log"
"github.com/Tsuyopon-1067/grpc-client-streaming-test/scantext"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// grpc.WithInsecure() を指定するとTLSで暗号化を行わずに通信する
conn, err := grpc.NewClient("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close() // main関数終了時に接続を閉じる
client := scantext.NewSenderClient(conn) // クライアントを作成
for {
var text string // 標準入力受取用
fmt.Println("Enter text: (quit to exit)") // ただの指示
fmt.Scan(&text) // 標準入力を受け取る
if text == "quit" { // quit と入力されたらループを抜けて終了
break
}
scanText := scantext.ScanText{ // 送信するメッセージを作成
Content: text,
}
_, err := client.SendText(context.Background(), &scanText) // メッセージを送信
if err != nil { // エラーが発生したらログ出力して終了
log.Fatalf("Error sending message: %v", err)
}
fmt.Printf("Sent message: %s\n", text) // ただのログ出力
}
fmt.Println("All messages sent successfully")
}
ScanText
関数について
サーバ・クライントどちらでもScanText
関数が呼び出されています.しかし,これらは別物です(関数のとる引数やバインドされる先が異なる).クライアントでScanText
関数を実行すると,サーバで別のScanText
関数が何者かに実行されるイメージです.
実行
- ターミナルを2つ起動する(それぞれターミナル1,ターミナル2と呼ぶ).
- どちらかのターミナルで以下のコマンドを実行(依存関係の追加).
$ go mod tidy
- ターミナル1では以下のコマンドを実行(サーバを立ち上げる).
$ go run ./server/main.go
- ターミナル2では以下のコマンドを実行(クライアントを立ち上げる).
$ go run ./client/main.go
- ターミナル2に好きなメッセージを入力し,Enterを押して送信する.そして,ターミナル1にメッセージが届いていることを確認する.
- 気が済んだらターミナル1ではCtrl+Cを,ターミナル2では"quit"と入力してプログラムを終了する.
- サーバの出力例
$ go run ./server/main.go
Server is running on :8080
2024/09/26 17:25:47 Received: message
2024/09/26 17:25:47 Received: 1
2024/09/26 17:25:50 Received: message
2024/09/26 17:25:50 Received: 2
2024/09/26 17:25:52 Received: message
2024/09/26 17:25:52 Received: 3
2024/09/26 17:25:53 Received: hoge
2024/09/26 17:25:54 Received: fuga
2024/09/26 17:25:55 Received: piyo
- クライアントの出力例
$ go run ./client/main.go
Enter text: (quit to exit)
message 1
Sent message: message
Enter text: (quit to exit)
Sent message: 1
Enter text: (quit to exit)
message 2
Sent message: message
Enter text: (quit to exit)
Sent message: 2
Enter text: (quit to exit)
message 3
Sent message: message
Enter text: (quit to exit)
Sent message: 3
Enter text: (quit to exit)
hoge
Sent message: hoge
Enter text: (quit to exit)
fuga
Sent message: fuga
Enter text: (quit to exit)
piyo
Sent message: piyo
Enter text: (quit to exit)
quit
All messages sent successfully