9
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?

Connectを使ってgPRCとHTTP/1.1のリクエストを受け取れるサーバーを作ってみよう

Last updated at Posted at 2024-12-16

この記事は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のサービス名やメソッド名、リクエストやレスポンスを定義します。

./proto/helloworld.proto
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をルートディレクトリに作成

./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

./gen/greetconnect/helloworld.connect.go
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から使います。

./handler/helloworld.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
}
./main.go
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では一緒に働く仲間を募集しています!

9
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
9
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?