2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoでgRPCのクライアントストリーミングをする

Last updated at Posted at 2024-09-26

はじめに

gRPCでクライアントストリーミングするサンプルです.

サンプルコード内でところどころモジュール名github.com/Tsuyopon-1067/grpc-client-streaming-testが登場しますがよしなに読み替えてください.

このサンプルでは以下のような通信をします.

通信のイメージ 通信するデータ

このサンプルのGitHubリポジトリ

準備

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

スキーマを以下のように定義します.ファイルは上のディレクトリ図の場所に作成します.

scantext/scantext.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の外(後で作るserverclientと同じディレクトリ)で実行して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

↑これは実行しないので注意(多分実行しても悪いことは起きないけど)

サーバ

サーバのコードを以下のように書きます.ファイルは上のディレクトリ図の場所に作成します.

server/main.go
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)
	}
}

クライアント

クライアントのコードを以下のように書きます.ファイルは上のディレクトリ図の場所に作成します.

client/main.go
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関数が何者かに実行されるイメージです.

実行

  1. ターミナルを2つ起動する(それぞれターミナル1,ターミナル2と呼ぶ).
  2. どちらかのターミナルで以下のコマンドを実行(依存関係の追加).
    $ go mod tidy
    
  3. ターミナル1では以下のコマンドを実行(サーバを立ち上げる).
    $ go run ./server/main.go
    
  4. ターミナル2では以下のコマンドを実行(クライアントを立ち上げる).
    $ go run ./client/main.go
    
  5. ターミナル2に好きなメッセージを入力し,Enterを押して送信する.そして,ターミナル1にメッセージが届いていることを確認する.
  6. 気が済んだらターミナル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
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?