ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • gRPC (1)
    grpc 2023. 6. 4. 22:18

    계기

    본인은 server / client간의 양방향 통신을 위해서 NATS 라는 Protocol을 사용했다. NATS는 오픈 소스 메세징 시스템으로 publish / subscribe 방식, request / reply 방식 그리고 queue 을 사용한다.

    장비를 관리하는 업무를 하다보니 각각의 client에 따로 명령을 전달해야 할 필요가 있었고 이에 사용하게 되었다. 사용방법이 매우 편리하고 빠르기 때문에 문제 없이 사용하고 있었는데 사용 간 type 에러 문제와 실시간성을 보장해야하는 기능이 필요할 때 양방향 스트링이 되지 않는 불편함이 생겼다. 이를 해결하기 위해 grpc를 찾아보게 되었다.

    gRPC란?

    gRPC는 Google에서 개발되었고 고성능 RPC 프레임워크이다. HTTP/2 에서 동작하며 양방향 스트리밍 및 흐름 제어를 제공한다.

    gRPC는 원격으로 호출할 수 있는 메서드를 지정하여 서비스를 정의하는 개념이 기반이며 protobufs(IDL로 사용)를 기본 메세지 형식으로 사용한다. 이 덕분에 효율적인 직렬화가 가능하고 Type 을 명시적으로 체크할 수 있다.

    ※IDL: Interface Definition Language

    gRPC의 특징

    1. 언어 독립성: gRPC는 다양한 언어를 지원한다. 클라이언트와 서버가 서로 다른 언어로 작성되어 있어도 상호 작용이 가능하다.
    2. 양방향 스트리밍: HTTP/2를 기반으로 하는 gRPC는 클라이언트와 서버 간에 양방향 스트리밍을 지원한다.
    3. 강력한 타입 체크: 메시지 형식을 프로토콜 버퍼로 정의하면, gRPC는 Type 체크를 제공한다.
    4. 높은 성능: gRPC는 HTTP/2와 프로토콜 버퍼를 활용하여 높은 성능을 제공한다.
    5. Google API 연동: Google API에는 인터페이스의 gRPC 버전이 제공되므로 애플리케이션에 Google 기능을 쉽게 빌드할 수 있다.

    gRPC에서는 클라이언트 애플리케이션이 로컬 객체처럼 다른 컴퓨터의 서버 애플리케이션에 있는 메서드를 직접 호출할 수 있기 때문에 분산 app과 서비스를 더 만들기 쉽다. 많은 RPC 시스템에서와 마찬가지로 gRPC는 서비스를 정의하고 매개변수와 반환 유형으로 원격으로 호출할 수 있는 메서드를 지정한다. 서버 측에서는 서버가 이 인터페이스를 구현하고 클라이언트 호출을 처리하기 위해 gRPC 서버를 실행합니다. 클라이언트 측에서는 클라이언트가 서버와 동일한 메서드를 제공하는 stub을 가지고 있다.

    예를 들어, 만약 Server에서 SayHello라는 함수를 제공하면 client는 server와 연결한 뒤 conn.GetUser 호출을 통해 서버가 제공하는 SayHello라는 함수를 실행할 수 있다. 이에 대한 명세는 .proto라는 파일을 생성하여 정의한 뒤 protobuf로 빌드하면 생성할 수 있다.

    Protobuf

    protobuf는 구글에서 개발한 언어와 플랫폼에 상관 없이 직렬화를 가능하게 하는 데이터 구조이다. 서로 다른 시스템끼리 데이터를 공유 / 저장하기 위해 사용된다. protobuf는 XML, JSON과 같이 잘 알려져 있는 다른 데이터 형식보다 더 효율적인 방식으로 데이터를 관리하기 때문에 전송시 / 저장 시 데이터를 더 적게 사용할 수 있다.

    프로토콜 버퍼로 작업하기 위해서는 우선 데이터의 구조를 정의해야 한다. 확장자가 .proto인 일반 텍스트 파일을 통해서 정의할 수 있으며 프로토콜 버퍼 데이터는 메시지로 구조화되고, 각 메시지는 필드라고 하는 일련의 이름-값 쌍을 포함한다.

    아래는 예제코드에서 가져온 .proto의 예시이다.

    git clone -b v1.55.0 --depth 1 https://github.com/grpc/grpc-go

    완전 동일하지는 않고 본인의 환경에서 하기 위해서 go_package만 변경하였다.

    syntax = "proto3";
    
    option go_package = "grpc/helloworld";
    
    package helloworld;
    
    // The greeting service definition.
    service Greeter {
      // Sends a greeting
      rpc SayHello (HelloRequest) returns (HelloReply) {}
    }
    
    // The request message containing the user's name.
    
    message HelloRequest {
      string name = 1;
    }
    
    // The response message containing the greetings
    
    message HelloReply {
      string message = 1;
    }

    위와 같이 .proto 파일을 생성한 뒤 버퍼 컴파일러인 protoc을 사용해서 사용하는 언어에 맞는 코드를 생성할 수 있다. 사용법에 대해서는 기본적인 설명 후 다루도록 하겠다.

    gRPC의 4가지 서비스 정의 방법

    1. 클라이언트가 서버에 단일 요청을 보내고 일반 함수 호출과 마찬가지로 단일 응답을 받는 단항 RPC
      rpc SayHello(HelloRequest) returns (HelloResponse);
    2. 클라이언트가 서버에 요청을 보내고 일련의 메시지를 읽을 수 있는 스트림을 다시 받는 서버 스트리밍 RPC
      rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
    3. 클라이언트가 일련의 메시지를 작성하고 제공된 스트림을 사용하여 다시 서버로 보내는 클라이언트 스트리밍 RPC
      rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
    4. 양쪽이 읽기-쓰기 스트림을 사용하여 일련의 메시지를 보내는 양방향 스트리밍 RPC
      rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

    서비스 예제 코드 작성하기

    본인은 간단하게 https://github.com/zzihyeon/grpc-server에 작성해 놓았으니 참고하면 될 듯 하다.

    위의 1에 대해서 설명하고 마무리 하려고 한다. 이 경우 결국 평범한 당방향 통신이다. 이에 대한 예제 코드를 작성해 보자.

    1. 우선 proto file을 생성해야 한다. 위에 선언한 것과 동일한 proto file을 사용하겠다.
    syntax = "proto3";
    
    option go\_package = "grpc/helloworld"; //내 package 경로
    
    package helloworld;
    
    // Greeting 서비스 정의
    service Greeter {  
    	rpc SayHello (HelloRequest) returns (HelloReply) {}  
    }
    
    // 요청은 name이라는 string이 있어야 한다.
    message HelloRequest {
    	string name = 1;  
    }
    
    // 응답은 message라는 string이 있어야 한다.  
    message HelloReply {
    	string message = 1;  
    }
    1. proto file 생성이 완료되었으면 이를 build하여야 한다.
      protoc --go\_out=. --go\_opt=paths=source\_relative --go-grpc\_out=. --go-grpc\_opt=paths=source\_relative helloworld/helloworld.proto
    2. 이제 server 코드를 작성해 보자.
    package main
    
    import (
    	"context"
    	"flag"
    	"fmt"
    	pb "grpc/helloworld"
    	"log"
    	"net"
    
    	"google.golang.org/grpc"
    )
    
    var (
    	port = flag.Int("port", 50051, "The server port")
    )
    
    // server is used to implement helloworld.GreeterServer.
    type server struct {
    	pb.UnimplementedGreeterServer //이부분은 안하면 에러가 발생한다. protobuf generate시 생성됨
    }
    
    // SayHello implements helloworld.GreeterServer
    func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    	log.Printf("Received: %v", in.GetName())
    	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
    }
    
    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{}) //client가 사용할 수 있도록 등록
    	log.Printf("server listening at %v", lis.Addr())
    	if err := s.Serve(lis); err != nil {
    		log.Fatalf("failed to serve: %v", err)
    	}
    }

    protobuf 빌드 시 mustEmbedUnimplementedGreeterServer method가 생성된다.

    1. 이제 client 코드를 작성해보자 
    package main
    
    import (
    	"context"
    	"flag"
    	"log"
    	"time"
    
    	pb "grpc/helloworld"
    
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/credentials/insecure"
    )
    
    const (
    	defaultName = "world"
    )
    
    var (
    	addr = flag.String("addr", "localhost:50051", "the address to connect to")
    	name = flag.String("name", defaultName, "Name to greet")
    )
    
    func main() {
    	flag.Parse()
    	// Set up a connection to the server.
    	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		log.Fatalf("did not connect: %v", err)
    	}
    	defer conn.Close()
    	c := pb.NewGreeterClient(conn) //서버의 method를 사용할 수 있게 해줌
    
    	// Contact the server and print out its response.
    	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    	defer cancel()
    	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
    	if err != nil {
    		log.Fatalf("could not greet: %v", err)
    	}
    	log.Printf("Greeting: %s", r.GetMessage())
    }

    댓글

Designed by Tistory.