💨

gRPC - connect - Render でwebサービスを作ってみる:server side connect

に公開

背景

gRPCもRemixも触ったことがないので、触ってみたいと思います。
gRPCの環境構築は以下で実施しました。

今回はconnectを使って通信をするように変更してみたいと思います。connectを使うとclientからgRPCへの通信が簡単にできるそうです。

ベースとして以下にコードがあります。

本記事では以下の内容を記載しています

  • gRPCのサービス定義
  • Bufを使ったコード生成
  • サーバのハンドラ実装
  • ターミナルからサーバへの疎通
  • クライアントからサーバへの疎通

環境

動作確認した環境は以下です。

  • MacBook Pro
  • 14インチ 2021
  • チップ:Apple M1 Pro
  • メモリ:32GB
  • macOS:15.5(24F74)

参照情報

Connect は、gRPC を利用した高速・型安全な通信に対応し、ブラウザからHTTP API(JSON や gRPC-Web)によるアクセスにも対応した 軽量なライブラリです。前回はサーバー側にgatewayサーバーを立てたのでそれが不要になるという認識です。Protocol Bufferのスキーマでサービスを定義すると、Connectが型安全なサーバーとクライアントのコードを作ります。サーバーのビジネスロジックを実装すればいいだけで、データ解釈も、ルーティングも、クライアント次一層もいりません。

事前準備

ツールをインストールする

プロジェクトルートで以下を実行します。

go install github.com/bufbuild/buf/cmd/buf@latest
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest

buf, protoc-gen-go, protoc-gen-connect-go が必要でPATHが通っている必要があります。

サービスを定義する

.protoファイルを作成します。

mkdir -p greet/v1
touch greet/v1/greet.proto

greet/v1/greet.proto は以下の内容とします。

syntax = "proto3";

package greet.v1;

// `example/gen/greet/v1`パスに`greetv1`パッケージを出漁kします。
option go_package = "example/gen/greet/v1;greetv1"

message GreetRequest {
	string name = 1;
}

message GreetResponse {
	string greeting = 1;
}

service GreetService {
	rpc Greet(GreetRequest) returns (GreetResponse) {}
}

これでgreet.v1というProtobufパッケージ、GreetServiceGreet関数を作成します。これらの名前はHTTP API URLで登場します。

コードを生成する

Bufを使ってコードを生成します。Googleのprotobufコンパイラの置き換えのソリューションです。先程Bufをインストールしましたが、続けるには少し設定が必要です。(protoc-gen-connect-goの代わりにprotocを使うこともできます)

まず、buf config initで足場となるbuf.yamlを作ります。以下のファイルが生成されます。lint/use/STANDARDは lintルールとしてSTANDARDを使う、breaking/use/FILE は 破壊的な変更を探すルールとしてFILEを使う。という意味のようです。

# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
  use:
	- STANDARD
breaking:
  use:
	- FILE

次にbuf.gen.yamlに以下の設定を追加して、コード生成の方法をBufに伝えます。protoc-gen-goで型を出力しproto-gen-connect-goでconnnect実装を出力する感じでしょうか。

version: v2
plugins:
  - local: protoc-gen-go
	out: gen
	opt: path=source_relative
  - local: proto-gen-connect-go
	out: gen
	opt: paths=source_relative

あとはlint generateしてコードを生成します。

buf lint
buf generate

gen ディレクトリに以下のファイルが作られているはずです。

gen
└── greet
	└── v1
		├── greet.pb.go
		└── greetv1connect
			└── greet.connect.go

greet.pb.goにはprotoc-gen-goによって作られた型GreetRequestGreetResponse及び、connectと接続するためのデータ解釈のコードを含みます。

greet.connect.goにはprotoc-gen-connect-goによって作られたHTTPハンドラとクライアントインターフェース及びコンストラクターがあります。

ハンドラを実装する

生成されたコードは多くのボイラープレートを作ってくれますが、ビジネスロジックの実装は別途必要です。生成されたコードの例はgreetv1connect.GreetServiceHandlerインタフェース等です。インタフェースはとても小さいので一つのGoパッケージですべてを実施できます。mkdir -p cmd/serverでフォルダを作りcnd/server/main.goを次の内容で追加しましょう。

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"connectrpc.com/connect"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"

	greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
	"example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)

type GreetServer struct{}

func (s *GreetServer) Greet(
	ctx context.Context,
	req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
	log.Println("Request headers: ", req.Header())
	res := connect.NewResponse(&greetv1.GreetResponse{
		Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
	})
	res.Header().Set("Greet-Version", "v1")
	return res, nil
}

func main() {
	// greeterサービスを生成
	greeter := &GreetServer{}
	// マルチプレクサ(ルータ)を生成
	mux := http.NewServeMux()
	// サービスハンドラにgreeterサービス登録、
	// ルーティング用のpathと関数呼び出し用のハンドラを作成
	path, handler := greetv1connect.NewGreetServiceHandler(greeter)
	fmt.Println("gRPC endpoint:", path) // pathの値を確認
	// マルチプレクサ(ルータ)にパスとハンドラを追加
	mux.Handle(path, handler)
	// httpサーバーを起動
	http.ListenAndServe(
		"localhost:8080",
		// Use h2c so we can serve HTTP/2 without TLS.
		h2c.NewHandler(mux, &http2.Server{}),
	)
}

Greet関数はジェネリクスを使っています。その結果connect.Requestconnect.Response型は、headerとtrailerに直接アクセスできるだけではなく、型強制力を持ったgreetv1.GreetRequestgreetv1.GreetResponseも提供します。ジェネリクスでConnectの多くがシンプルになりますが、protoc-gen-connect-goを使わずに実装することもできます。

以下のコマンドでサーバーを開始できます。

go get golang.org/x/net/http2
go get connectrpc.com/connect
go run ./cmd/server/main.go

curlでリクエストを送ります。

curl \
	--header "Content-Type: application/json" \
	--data '{"name": "Jane"}' \
	http://localhost:8080/greet.v1.GreetService/Greet

以下のレスポンスが返ります。

{"greeting": "Hello, Jane!"}

curl以外にgRPCのリクエストもサポートします。

grpcurl \
	-plaintext \
	-protoset <(buf build -o -) \
	-d '{"name": "Jane"}' \
	localhost:8080 greet.v1.GreetService/Greet

同様に以下のレスポンスが返ります。

{"greeting": "Hello, Jane!"}

Connectによって生成されたクライアントを使ってリクエストすることもできます。mkdir -p cmd/clientでディレクトリを作り、以下の内容のmain.goファイルを配置しましょう。

package main
import (
	"context"
	"log"
	"net/http"

	greetv1 "example/gen/greet/v1"
	"example/gen/greet/v1/greetv1connect"
	"connectrpc.com/connect"
)

func main() {
	// Connectが作ったClientを使ってclientインスタンスを作る
	client := greetv1connect.NewGreetServiceClient(
		http.DefaultClient,
		"http://localhost:8080",
	)
	// Greet関数呼び出し
		res, err := client.Greet(
		// ルートコンテキストで実行
		context.Background(),
		// GreetRequest型を使い、NewRequestを実行
		connect.NewRequest(&greetv1.GreetRequest{Name: "Jane"}),
	)
	if err != nil {
		log.Println(err)
		return
	}
	log.Println(res.Msg.Greeting)
}

以下でGoのプログラムを実行してクライアントからアクセスできます。

$ go run ./cmd/client/main.go

Connectの代わりにgRPCプロトコルを使う

connect-goは以下のプロトコルをサポートしています。

  • gRPCプロトコル。connect-goで複数のgPRC実装と簡単に接続できます。grpc-goconnect-goサーバーと連携して動きます。この前までは全てBuf CLIで実施していました。
  • gRPC-Webプロトコル。grpc/grpc-web によって使用されます。これによりconnect-goサーバーはEnvoy等の中間プロキシを必要とせずにgrpc-webフロントエンドと通信できます。
  • 新しいConnectプロトコル。HTTP1.1あるいはHTTP2で動作するシンプルなHTTPベースのプロトコルです。gRPCとgRPC-Webのいいとこ取りです。ストリーミングも含めてパッケージ化し、ブラウザでもモノリスサービスでもマイクロサービスでも動作します。デフォルトでJSONとバイナリエンコードされたprotobugをサポートします。

connect-goサーバーはこれらのプロトコルに対してデフォルトでIngressを許可します。connect-goはデフォルトでConnectプロトコルを追加いますが、WithGRPCWithGRPCWebのプションでgRPCやgRPC-Webを使うこともできます。

WithGRPCを使うようにGreetServiceClientを使うようにcmd/client/main.goを編集しましょう。

client := greetv1connect.NewGreetServiceClient(
  http.DefaultClient,
  "http://localhost:8080",
  connect.WithGRPC(),
)

以下で実行できます。

go run ./cmd/client/main.go

結果は同じですがgRPCプロトコルを使うようになっています。

まとめ

gPRCとConnectプロトコルをサポートしたAPIサーバーを作ることができました。従来のRESTとは異なり、URLの階層設計やリクエストレスポンスの構造、データの解釈やクエリパラメータのパースが不要になります。何よりも、クライアントは型安全な理想的なクライアントを使うことができるようになります。

補足 : URLの階層設計が不要になるとは?

「URLの階層設計が不要になる」が分かりませんでした。/greet.v1.GreetService/GreetってAPI階層ですよね?chatGPTにきいてみました。RESTは「リソースの関係性や構造を考えてURLの設定が必要」gRPCでは「サービス思考でメソッドを考える」という感じのようです。chatGPTの回答は以下

Discussion