背景 #
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などを使いインストールしましょう。
:::message
Goのサポートについて
「最新2つのメジャーリリースのうちの一つが必要」という文が聞き慣れず、違和感があったので調べてみるとGoのサポートは過去2つのバージョンのみが対象みたいですね。例えば1.18.x
が最新の安定版の場合は、1.17.x
と1.18.x
がサポート対象です。
:::
ツールをインストールする #
プロジェクトルートで以下を実行します。
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
:::message
lintの除外ルール
私はVSCode拡張でLocal History
というものを使っているのですが、この.history
以下のファイルがlintされてしまい困りました。version: v2
ではlintの除外対象をbuf.yamlではできないようです。Local History
にはhistoryの保存先を絶対パスにする設定があったのでプロジェクト外に移動して事無きを得ました。
:::
次に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ハンドラとクライアントインターフェース及びコンストラクターがあります。
:::message なぜprotocではなくbufを使うのか bufはprotocのlintやgenerate breaking changeなどを検知するためのツールでプラグイン管理なども簡単になるそうです。リンク先が詳しいです。
:::
ハンドラを実装する #
生成されたコードは多くのボイラープレートを作ってくれますが、ビジネスロジックの実装は別途必要です。生成されたコードの例は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
を使わずに実装することもできます。
:::message ジェネリクスを使っているのは
req *connect.Request[greetv1.GreetRequest]
) (*connect.Response[greetv1.GreetResponse], error) {
のあたりですね。[]に囲まれた部分が型として使われます。 :::
以下のコマンドでサーバーを開始できます。
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!"}
:::message
-protoset <(buf build -o -)
はbuf buildした結果を-protosetの入力として使うという意味になります。ですので、実行する場所が大事で、プロジェクトルートで実行する必要があります。
:::
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の回答は以下
:::message
ご指摘の通り、
/greet.v1.GreetService/Greet
のようなエンドポイントも「URLの階層構造」を持っています。
ただし、ここで言う「URL階層設計が不要」というのは、**RESTのように「リソースの意味や関係性に基づいてURLパスを設計する必要がない」**という意味です。
RESTの階層設計との違い #
-
RESTの場合
設計者が「/users/{userId}/posts/{postId}」のように、リソースの関係性や階層を考えてURLを設計します。
例:/users/123/posts/456
ここでは「ユーザー→投稿」というリソースの階層・関係性を意識して設計します。
-
gRPC/Connectの場合
サービス名・メソッド名が自動的にURLにマッピングされるだけで、
「リソースの意味的な階層」や「業務的な関係性」を設計者が考える必要がありません。
例:/greet.v1.GreetService/Greet
これは「サービス名/メソッド名」という機械的な階層であり、リソース設計の意図や意味を持たせるものではありません。
まとめ #
- gRPC/Connectのエンドポイントも「/」で区切られた階層構造を持ちますが、
それは「サービス名・メソッド名」を自動で並べただけの機械的な階層です。 - RESTのように「リソースの意味や関係性」を考えてURL階層を設計する必要がない、という点が大きな違いです。
要点
Reply by EmailgRPC/ConnectのURLは階層構造を持ちますが、それはサービス定義から自動生成されるものであり、RESTのような意味的・業務的なURL設計は不要です。 :::