protobuf install

brew install protobuf

 

mod init

go mod init go

 

Install protocol compiler

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

 

update PATH

export PATH="$PATH:$(go env GOPATH)/bin"

 

protocol buffer

syntax = "proto3";

package grpc;
option go_package = "./grpc/hello";

// 테스트
service Test {
    rpc Show(TestRequest) returns (TestResponse);
}

// 요청
message TestRequest {
    string requestString = 1;
}

// 응답
message TestResponse {
    string ResponseString = 1;
}

 

grpc 의존성 추가

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go
@저는 GOPATH를 바꿨다가 go get 명령어가 permission 문제로 돌아가지 않았는데 아래 명령어로 GOPATH를 정의해주니 잘 돌아갑니다.
export GOPATH="$HOME/go"

이유는 GOPATH는 GO의 workspace 위치를 정의하는 환경변수인데, Unix 시스템은 workspace 기본디렉터로 $HOME/go를 제공한다는 글을 읽고 위와 같이 바꾸어주니까 잘 작동하네요.

// 아마도 맨 처음 "brew install go"를 하면  GOPATH가 $HOME/go로 되어있을 것이라 추정

 

 

protoc로 컴파일

protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    grpc/grpc.proto

위 명령어로 컴파일을 하면 grpc.pb.go 파일이 생성된다. 후에 이 파일의 인터페이스로 코드를 작성하면 된다.

생성된 파일

생성된 코드를 확인해보면 다음과 같다.

// 테스트
service Test {
    rpc Show(TestRequest) returns (TestResponse);
}

 .proto 파일에서 작성한 service가 client와 server 각각 interface로 생성되어있다.

// TestClient is the client API for Test service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TestClient interface {
	Show(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error)
}

// TestServer is the server API for Test service.
// All implementations must embed UnimplementedTestServer
// for forward compatibility
type TestServer interface {
	Show(context.Context, *TestRequest) (*TestResponse, error)
	mustEmbedUnimplementedTestServer()
}

그리고 아래와 같이 rpc로 생성한 기능이 생성된 것을 볼 수 있다. gRPC Client와 Server로 함수처럼 이용이 가능하며 내부 로직을 몰라도 사용이 가능하다. 매우 Simple하다.

// Client
type testClient struct {
	cc grpc.ClientConnInterface
}

func NewTestClient(cc grpc.ClientConnInterface) TestClient {
	return &testClient{cc}
}

func (c *testClient) Show(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error) {
	out := new(TestResponse)
	err := c.cc.Invoke(ctx, "/grpc.Test/Show", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// Server
// UnimplementedTestServer must be embedded to have forward compatible implementations.
type UnimplementedTestServer struct {
}

func (UnimplementedTestServer) Show(context.Context, *TestRequest) (*TestResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method Show not implemented")
}
func (UnimplementedTestServer) mustEmbedUnimplementedTestServer() {}

그렇다면, gRPC Client와 Server의 코드를 작성해보자.

파일 구조

파일의 구조는 위와 같다. 

 

server.go

package main

import (
	"context"
	"log"
	"net"

	pb "go/grpc"
	"google.golang.org/grpc"
)

type server struct {
	pb.TestServer
}

func (s *server) Show(ctx context.Context, in *pb.TestRequest) (*pb.TestResponse, error) {
	log.Printf("Received: %v", in.GetRequestString())
	return &pb.TestResponse{ResponseString: "Hello " + in.GetRequestString()}, nil
}

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

server 측에선 특정 port로 연결을 대기해놓는다. 참고로 REST API의 경우 요청마다 연결을 성립하지만 gRPC의 경우 한번 연결을 해놓으면 해당 연결을 재사용하며 API를 주고 받는다.

그렇다면? gRPC는 하나의 server당 하나의 client만 연결할 수 있는가?? (후에 정리)

 

client.go

package main

import (
	"context"
	"log"
	"time"

	pb "go/grpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	// Set up a connection to the server.
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewTestClient(conn)
	
	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.Show(ctx, &pb.TestRequest{RequestString: "KYJ"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Test: %s", r.GetResponseString())
}

Client에서는 주소:port로 connection을 만들고 connection으로 Client를 생성합니다. 그리고 해당 Client의 interface에 정의된 함수로 요청을 Server측으로 보낼 수 있습니다.

 

 

테스트

server 실행

go run ./server/server.go

client 실행

go run ./client/client.go

server 출력

client 출력

 

 

참고자료

'Go' 카테고리의 다른 글

Import/Export  (0) 2023.12.24
package와 main  (0) 2023.12.24
Go에서 의존성 주입 (wire, fx 사용방법)  (0) 2023.05.14
gRPC란?  (0) 2023.05.01
Golang 설정 및 실행  (0) 2023.04.30

+ Recent posts