본문 바로가기
자습

go gRPC Server & gRPC Gateway

by litaro 2021. 9. 20.

https://grpc.io/

 

gRPC

A high-performance, open source universal RPC framework

grpc.io

서버를 연동하거나 개발하면서 항상 REST API로 HTTP(S) 프로토콜로만 사용했었는데, 최근 세미나를 통해 gRPC에 대해 한번 사용해보자는 생각이 들었다. 

gRPC란?


구글이 초기 개발한 오픈 소스 원격 프로시져 호출 remote procedure call 프레임워크로, Protocol Buffer 형태의 메시지를 HTTP/2 프로토콜로 전달하여 기존 REST에 비해 성능이 빠르고, local 함수 호출의 형식이라 직관적이다.

아래 그림을 보면 확~!! 와닿는데, 

Client 가 Stub을 통해 Server API를 마치 Local 함수 호출하듯이 호출하게 된다. 이를 위해서는 .proto 파일에 API를 정의하여 각 언어에 맞게 Complie한 파일을 Client와 Server 모두가 가지고 있어야하는 단점?이 있다. (API가 바뀌면... 둘다 업데이트 Client/Server 모두 업데이트된 파일을 가지고 있어야한다.) 

https://medium.com/@akshitjain_74512/inter-service-communication-with-grpc-d815a561e3a1

 

하지만 성능이 REST에 비해 좋기때문에 (7배정도 빠르다는데...), 단점에도 불구하고 MSA 구조에서는 내부 통신으로는 많이 사용되는것 같다. 

그렇다면 나도 한번 도전~!!

1. Define : .proto 파일 정의

참고: https://github.com/grpc/grpc-go/tree/master/examples

syntax = "proto3";

option go_package = "helloworld/helloworld";

package helloworld;

// The greeting service definition.
service Greeter { ✅ 호출한 method 명세로 (one input one output)
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}

// The request message containing the user's name.
message HelloRequest { ✅ 요청 응답 메세지 포맷 
  string name = 1; ✅ 고유번호가 할당되고 이 번호가 바뀌면 업데이트 해야한다.
  string extra = 2;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

2. Compile with protoc

사전 준비  

참고: https://grpc.io/docs/languages/go/quickstart/

1.  Protocol Buffer compiler 설치

$ brew install protobuf
$ protoc --version  # Ensure compiler version is 3+
libprotoc 3.17.3

2. Go 언어용 Plugin 설치

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
 
// path 설정
$ export PATH="$PATH:$(go env GOPATH)/bin"

3. protoc로 compile

$ protoc -I ./proto \
  --go_out ./proto --go_opt paths=source_relative \
  --go-grpc_out ./proto --go-grpc_opt paths=source_relative \
  ./proto/helloworld/helloworld.proto

 

3. Implement

Server Side

<hello_grpc/main.go> 

// Package main implements a server for Greeter service.
package main

import (
..
	"google.golang.org/grpc"
	pb "hello_grpc/proto/helloworld"
)

const (
	port = ":50052"
)

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v %v", in.GetName(), in.GetExtra())
	return &pb.HelloReply{Message: "Hello " + in.GetName() + "! " + in.GetExtra()}, nil
}

func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v %v", in.GetName(), in.GetExtra())
	return &pb.HelloReply{Message: "Hello again " + in.GetName() + "! " + in.GetExtra()}, nil
}

func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Client Side

<hello_client/main.go>

package main

import (
...
	"google.golang.org/grpc"
	pb "hello_client/helloworld"
)

const (
	address      = "localhost:50052"
	defaultName  = "Bob"
	defaultExtra = "Nice weather, huh?"
)

func main() {
	// Set up a connection to the server.
	conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	name := defaultName
	extra := defaultExtra
	if len(os.Args) > 1 {
		name = os.Args[1]
		extra = os.Args[2]
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name, Extra: extra})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
	r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name, Extra: extra})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

4. Test

/hello_client
$ go run main.go "키키" "즐거운 월요일이야"
2021/09/10 11:36:57 Greeting: Hello 키키! 즐거운 월요일이야
2021/09/10 11:36:57 Greeting: Hello again 키키! 즐거운 월요일이야

/hello_grpc
$ go run main.go
2021/09/10 11:36:57 Received: 키키 즐거운 월요일이야
2021/09/10 11:36:57 Received: 키키 즐거운 월요일이야

 

gRPC Gateway


참고: https://github.com/grpc-ecosystem/grpc-gateway

대부분 외부에서 들어오는 요청은 REST 이기 때문에 REST요청을 중간에 받아서 Backend gRPC 서버로 전달해줄 Gateway 가 필요하다.

gRPC Gateway 는 google protocol buffer protoc 의 plugin으로 REST API로 들어온 요청을 gRPC 포맷으로 변환하는 reverse-proxy 역할을 하는 서버 코드를 생성해준다.

1. .proto 파일에 HTTP mapping 추가

google에서 정의한 규칙 google/api/http.proto 에 맞게 .proto service 정의에 HTTP를 mapping 한다. 

  • path 와 body 정보를 mapping 할수 있다.
  • header 는 별도의 API로 가져와야 한다.
syntax = "proto3";

option go_package = "helloworld/helloworld";

package helloworld;
import "google/api/annotations.proto"; ✅ annotations 추가

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/say/hello" ✅ HTTP mapping 정보 추가
      body: "*"
    };
  }
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/say/hello/again"
      body: "*"
    };
  }
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
  string extra = 2;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

2. Compile with grpc-gateway

protoc 의 gateway plugin 인 grpc-gateway를 통해 reverse proxy server 코드 (.pb.gw.go) 를 생성한다. 

참고: https://github.com/grpc-ecosystem/grpc-gateway

사전 준비

$ go install \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
    google.golang.org/protobuf/cmd/protoc-gen-go \
    google.golang.org/grpc/cmd/protoc-gen-go-grpc

3. Gateway 서버 구현

Gateway 서버는 해당 .pb.gw.go 파일의 RegisterxxxFromEndpoint 함수를 통해서 grpc 서버와 연동한다.

<hello_proxy/main.go>

package main

import (
	"context"
	"flag"
	"net/http"

	"github.com/golang/glog"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"

	gw "hello_proxy/helloworld" // Update
)

var (
	// command-line options:
	// gRPC server endpoint
	grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:50052", "gRPC server endpoint")
)

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Register gRPC server endpoint
	// Note: Make sure the gRPC server is running properly and accessible
	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
	err := gw.RegisterGreeterHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
	if err != nil {
		glog.Fatal(err)
	}

	// Start HTTP server (and proxy calls to gRPC server endpoint)
	if err := http.ListenAndServe(":8081", mux); err != nil {
		glog.Fatal(err)
	}
}

4. Test

hello_proxy
$ curl --request POST \
--url http://localhost:8081/say/hello \
--header 'Content-Type: application/json' \
--data '{
"name":"키키",  
"extra":"즐거운 화요일 아침이야~"
}'
{"message":"Hello 키키! 즐거운 화요일 아침이야~"}

hello_grpc
$ 2021/09/10 19:45:24 Received: 키키 즐거운 화요일 아침이야~

 

정리

  • gRPC는 Protocol Buffer 형태의 메시지를 HTTP/2 프로토콜로 전달하여 기존 REST에 비해 성능이 빠르고, local 함수 호출의 형식이라 직관적이다.
  • gRPC 통신 방법은 1) .proto 정의 2) protoc 로 원하는 언어 (go) 로 compile 3) compile 한 .go 파일을 server와 client에 복사하여 server는 해당 .proto에 정의한 service interface를 구현하고, client는 이 method를 호출한다.
  • 외부의 REST 호출을 위해서는 HTTP 요청을 gRPC 로 변환시켜주는  reverse proxy 서버가 필요하고, grpc-gateway라는 protoc plugin을 통해 쉽게 해당 서버 구현이 가능하다.

'자습' 카테고리의 다른 글

Go Thread-Safety : sync.Mutex, sync.Map  (0) 2023.08.26
Spring Native  (0) 2022.05.14
Kong Gateway + Konga  (0) 2021.08.29
Jaeger with Go  (1) 2021.07.20
Zipkin Go  (0) 2021.07.17