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が型安全なサーバーとクライアントのコードを作ります。サーバーのビジネスロジックを実装すればいいだけで、データ解釈も、ルーティングも、クライアント次一層もいりません。
事前準備
- 最新2つのリリースの内の一つが必要です。Getting Startedを見てください。
- cURLも必要です。package managerなどを使いインストールしましょう。
ツールをインストールする
プロジェクトルートで以下を実行します。
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パッケージ、GreetService
、Greet
関数を作成します。これらの名前は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
によって作られた型GreetRequest
とGreetResponse
及び、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.Request
とconnect.Response
型は、headerとtrailerに直接アクセスできるだけではなく、型強制力を持ったgreetv1.GreetRequest
とgreetv1.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-go
はconnect-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プロトコルを追加いますが、WithGRPC
やWithGRPCWeb
のプションで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