この記事はGo Advent Calendar 2024の16日目の記事です!
こんにちは!株式会社Schoo 新卒1年目の @hiroto_0411です!
研修が終わり、初めて弊社のgRPC実装を見た際に「これはREST APIを実装してる?」と疑問に思いました。そこで調べてみるとConnectを使ってgRPCとREST APIの両方を実装していると分かりました。そこで、今回はConnectって何?をまとめてみました!
この記事でわかること
- Connectってなに?
- Connectを使ってAPIサーバーを作る流れ
gRPCについて理解が曖昧な方は以下の記事にgRPCについてまとめたのでこちらを先に見ていただくと、この記事が理解しやすくなると思います。
Connectってなに?
ConnectはgRPCのプロトコルをベースに、gRPCとREST APIを構築できるライブラリです。
Connectには以下のような特徴があります
- Connect独自のプロトコル(HTTP/1.1、HTTP/2、HTTP/3対応)に加え、gRPCとgRPC-Webの3つのプロトコルをサポートしています。このため、grpc-gatewayなどの追加ツールを使用せずに、1つのサーバーでgRPCとHTTP/1.1ベースのAPI(REST APIなど)の両方の形式のリクエストを受ける事ができます
記事執筆時点 (2024/12)ではGo、TypeScript、JavaScript、Swift、Kotlinに対応しています。
Connectを使うことで、簡単にgRPCとREST APIの実装を同時にできるので、実際に実装してみましょう!
Connectを使ってサーバーを実装してみる
上記のチュートリアルを参考にしながらConnectを使ってサーバーを実装してみました。
作成したコードは以下で見られます。
ディレクトリ構成はこんな感じになる予定です。
.
├── gen
│ ├── greetconnect
│ │ └── helloworld.connect.go
│ └── helloworld.pb.go
├── handler
│ └── helloworld.go
├── proto
│ └── helloworld.proto
├── buf.gen.yaml
├── buf.yaml
├── go.mod
├── go.sum
└── main.go
1. 準備
モジュールの作成
go mod init github.com/user-name/project-name
必要なツールのインストール
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
PATHを更新 (pathが通っていない場合のみ)
export PATH="$PATH:$(go env GOPATH)/bin"
2 .protoファイルの作成
Protocol Buffersというフォーマットを使用してgRPCのサービス名やメソッド名、リクエストやレスポンスを定義します。
syntax = "proto3";
option go_package = "github.com/user-name/project-name/gen;greet";
package hello;
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse){ }
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
- この後、.protoファイルを元にコードを自動生成します。その時に出力したいpackage名をgo_packageで指定する事ができます。今回はgen。
-
;greet
をつけることで、greetconnect
というpackage名でhelloworld.connect.go
が作成される。;greet
を指定しないと、go_package
で指定しているpackage名+connect(今回の場合はgenconnect
)というpackage名で作成されます。
├── gen
│ ├── greetconnect
│ │ └── helloworld.connect.go
│ └── helloworld.pb.go
3.コードの自動生成
buf.yamlをルートディレクトリに作成
buf config init
buf.gen.yamlをルートディレクトリに作成
version: v2
plugins:
- local: protoc-gen-go
out: gen
opt:
- module=github.com/user-name/project-name/gen
- local: protoc-gen-connect-go
out: gen
opt:
- module=github.com/user-name/project-name/gen
コードを自動生成
以下のコマンドを実行することで、.protoファイルとbuf.gen.yamlを元に2種類のファイルが自動生成されます。
buf generate
├── gen
│ ├── greetconnect
│ │ └── helloworld.connect.go
│ └── helloworld.pb.go
モジュールの依存関係を整理
go mod tidy
4. 自動生成されたinterfaceを使い、サーバーを実装する
自動生成されたinterface
type GreeterHandler interface {
SayHello(context.Context, *connect.Request[gen.HelloRequest]) (*connect.Response[gen.HelloResponse], error)
}
func NewGreeterHandler(svc GreeterHandler, opts ...connect.HandlerOption) (string, http.Handler) {
greeterSayHelloHandler := connect.NewUnaryHandler(
GreeterSayHelloProcedure,
svc.SayHello,
connect.WithSchema(greeterSayHelloMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
return "/hello.Greeter/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case GreeterSayHelloProcedure:
greeterSayHelloHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
type UnimplementedGreeterHandler struct{}
func (UnimplementedGreeterHandler) SayHello(context.Context, *connect.Request[gen.HelloRequest]) (*connect.Response[gen.HelloResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hello.Greeter.SayHello is not implemented"))
}
./gen/greetconnect/helloworld.connect.goに定義されているinterfaceを実装し、main.goから使います。
package handler
import (
"context"
"fmt"
"log"
"connectrpc.com/connect"
greet "github.com/user-name/project-name/gen"
)
type GreetHandler struct{}
func NewGreetHandler() *GreetHandler {
return &GreetHandler{}
}
func (s *GreetHandler) SayHello(ctx context.Context, req *connect.Request[greet.HelloRequest]) (*connect.Response[greet.HelloResponse], error) {
log.Println("Request headers: ", req.Header())
// 本来ならここから、別の層のメソッドを呼び出すなどする
res := connect.NewResponse(&greet.HelloResponse{
Message: fmt.Sprintf("Hello, %s!", req.Msg.Name),
})
res.Header().Set("Greet-Version", "v1")
return res, nil
}
package main
import (
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/user-name/project-name/gen/greetconnect"
"github.com/user-name/project-name/handler"
)
func main() {
greeter := handler.NewGreetHandler()
path, handler := greetconnect.NewGreeterHandler(greeter)
mux := http.NewServeMux()
mux.Handle(path, handler)
http.ListenAndServe(
"localhost:8080",
// Use h2c so we can serve HTTP/2 without TLS.
h2c.NewHandler(mux, &http2.Server{}),
)
}
Connect使って書くことで、net/httpを使ってサーバーを作成することができREST APIを実装する時と似たような感じのコードで書く事ができます。Connectを使わない場合はgrpc-goを使ったgRPC特有の書き方になります。
Connectを使わないでgRPCを実装する場合のmain.go
package main
import (
"flag"
"fmt"
"log"
"net"
pb "github.com/user-name/project-name/gen"
"github.com/user-name/project-name/server"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
var (
port = flag.Int("port", 50051, "The server port")
)
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server.GreeterServer{})
log.Printf("server listening at %v", lis.Addr())
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
5.サーバーにリクエストを送ってみる
サーバーの起動
go run main.go
REST APIとgRPCそれぞれの方法でリクエストを送ってみる
curl
curl \
--header "Content-Type: application/json" \
--data '{"name": "パブりん"}' \
http://localhost:8080/hello.Greeter/SayHello
{
"message": "Hello, パブりん!"
}
grpcurl
grpcurl -protoset <(buf build -o -) -plaintext -d '{"name": "パブりん"}' localhost:8080 hello.Greeter/SayHello
{
"message": "Hello, パブりん!"
}
参考
Schooでは一緒に働く仲間を募集しています!