Primeiros passos com o gRPC-Go

1. Introdução

Neste codelab, você vai usar o gRPC-Go para criar um cliente e um servidor que formam a base de um aplicativo de mapeamento de rotas escrito em Go.

Ao final do tutorial, você terá um cliente que se conecta a um servidor remoto usando o gRPC para receber o nome ou o endereço postal do que está localizado em coordenadas específicas em um mapa. Um aplicativo completo pode usar esse design cliente-servidor para enumerar ou resumir pontos de interesse ao longo de um trajeto.

O serviço é definido em um arquivo Protocol Buffers, que será usado para gerar código boilerplate para o cliente e o servidor, permitindo que eles se comuniquem entre si e economizando tempo e esforço na implementação dessa funcionalidade.

Esse código gerado cuida não apenas das complexidades da comunicação entre o servidor e o cliente, mas também da serialização e desserialização de dados.

O que você vai aprender

  • Como usar buffers de protocolo para definir uma API de serviço.
  • Como criar um cliente e um servidor baseados em gRPC com uma definição de buffers de protocolo usando a geração automática de código.
  • Entendimento da comunicação cliente-servidor com gRPC.

Este codelab é destinado a desenvolvedores Go que não conhecem o gRPC ou querem relembrar o assunto, além de qualquer pessoa interessada em criar sistemas distribuídos. Não é necessário ter experiência com gRPC.

2. Antes de começar

Pré-requisitos

Verifique se você instalou o seguinte:

  • A versão 1.24.5 ou mais recente da cadeia de ferramentas do Go. Para instruções de instalação, consulte Primeiros passos do Go.
  • O compilador de buffers de protocolo, protoc, versão 3.27.1 ou mais recente. Para instruções de instalação, consulte o guia de instalação do compilador.
  • Os plug-ins do compilador de buffer de protocolo para Go e gRPC. Para instalar esses plug-ins, execute os seguintes comandos:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Atualize a variável PATH para que o compilador de buffer de protocolo possa encontrar os plug-ins:

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

Acessar o código

Para que você não precise começar do zero, este codelab oferece um scaffold do código-fonte do aplicativo para você concluir. As etapas a seguir mostram como concluir o aplicativo, incluindo o uso dos plug-ins do compilador de buffer de protocolo para gerar o código gRPC boilerplate.

Faça o download do código-fonte como um arquivo .ZIP no GitHub e descompacte o conteúdo.

Como alternativa, o código-fonte completo está disponível no GitHub se você quiser pular a digitação de uma implementação.

3. Definir o serviço

A primeira etapa é definir o serviço gRPC do aplicativo, o método RPC e os tipos de mensagens de solicitação e resposta usando buffers de protocolo. Seu serviço vai oferecer:

  • Um método RPC chamado GetFeature que o servidor implementa e o cliente chama.
  • Os tipos de mensagem Point e Feature, que são estruturas de dados trocadas entre o cliente e o servidor ao usar o método GetFeature. O cliente fornece coordenadas do mapa como um Point na solicitação GetFeature ao servidor, e o servidor responde com um Feature correspondente que descreve o que está localizado nessas coordenadas.

Esse método RPC e os tipos de mensagem dele serão definidos no arquivo routeguide/route_guide.proto do código-fonte fornecido.

Os buffers de protocolo são conhecidos como protobufs. Para mais informações sobre a terminologia do gRPC, consulte Conceitos principais, arquitetura e ciclo de vida do gRPC.

Tipos de mensagem

No arquivo routeguide/route_guide.proto do código-fonte, primeiro defina o tipo de mensagem Point. Um Point representa um par de coordenadas de latitude e longitude em um mapa. Neste codelab, use números inteiros para as coordenadas:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Os números 1 e 2 são IDs exclusivos para cada um dos campos na estrutura message.

Em seguida, defina o tipo de mensagem Feature. Um Feature usa um campo string para o nome ou endereço postal de algo em um local especificado por um Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Método de serviço

O arquivo route_guide.proto tem uma estrutura service chamada RouteGuide que define um ou mais métodos fornecidos pelo serviço do aplicativo.

Adicione o método rpc GetFeature à definição RouteGuide. Como explicado anteriormente, esse método vai pesquisar o nome ou endereço de um local em um determinado conjunto de coordenadas. Portanto, faça com que GetFeature retorne um Feature para um determinado Point:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Esse é um método RPC unário: um RPC simples em que o cliente envia uma solicitação ao servidor e aguarda uma resposta, assim como uma chamada de função local.

4. Gerar o código do cliente e do servidor

Em seguida, gere o código gRPC boilerplate para o cliente e o servidor do arquivo .proto usando o compilador de buffer de protocolo. No diretório routeguide, execute:

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

Esse comando gera os seguintes arquivos:

  • route_guide.pb.go, que contém funções para criar os tipos de mensagens do aplicativo e acessar os dados deles.
  • route_guide_grpc.pb.go, que contém funções usadas pelo cliente para chamar o método gRPC remoto do serviço e funções usadas pelo servidor para fornecer esse serviço remoto.

Em seguida, vamos implementar o método GetFeature no lado do servidor para que, quando o cliente enviar uma solicitação, o servidor possa responder com uma resposta.

5. Implementar o serviço

A função GetFeature no lado do servidor é onde o trabalho principal é feito: ela recebe uma mensagem Point do cliente e retorna em uma mensagem Feature as informações de local correspondentes de uma lista de lugares conhecidos. Confira a implementação da função em server/server.go:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

Quando esse método é invocado após uma solicitação de um cliente remoto, a função recebe um objeto Context que descreve a chamada de RPC e um objeto de buffer de protocolo Point da solicitação do cliente. A função retorna um objeto de buffer de protocolo Feature para o local pesquisado e um error, conforme necessário.

No método, preencha um objeto Feature com as informações adequadas para o Point especificado e, em seguida, return com um erro nil para informar ao gRPC que você terminou de lidar com o RPC e que o objeto Feature pode ser retornado ao cliente.

O método GetFeature exige que um objeto routeGuideServer seja criado e registrado para que as solicitações de clientes para pesquisas de local possam ser encaminhadas a essa função. Isso é feito em main():

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

Veja o que está acontecendo em main(), etapa por etapa:

  1. Especifique a porta TCP a ser usada para detectar solicitações de clientes remotos usando lis, err := net.Listen(...). Por padrão, o aplicativo usa a porta TCP 50051, conforme especificado pela variável port ou transmitindo a chave --port na linha de comando ao executar o servidor. Se não for possível abrir a porta TCP, o aplicativo será encerrado com um erro fatal.
  2. Crie uma instância do servidor gRPC usando grpc.NewServer(...) e nomeie essa instância como grpcServer.
  3. Crie um ponteiro para routeGuideServer, uma estrutura que representa o serviço de API do aplicativo, nomeando o ponteiro s..
  4. Use s.loadFeatures() para preencher a matriz s.savedFeatures com locais que podem ser pesquisados usando GetFeature.
  5. Registre o serviço de API com o servidor gRPC para que as chamadas de RPC para GetFeature sejam encaminhadas para a função apropriada.
  6. Chame Serve() no servidor com os detalhes da porta para fazer uma espera de bloqueio para solicitações do cliente. Isso continua até que o processo seja encerrado ou Stop() seja chamado.

A função loadFeatures() recebe os mapeamentos de coordenadas para localizações de server/testdata.go.

6. Criar o cliente

Agora edite client/client.go, que é onde você vai implementar o código do cliente.

Para chamar os métodos do serviço remoto, primeiro precisamos criar um canal gRPC para se comunicar com o servidor. Para isso, transmitimos a string URI de destino do servidor (que, neste caso, é simplesmente o endereço e o número da porta) para grpc.NewClient() na função main() do cliente da seguinte maneira:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

O endereço do servidor, definido pela variável serverAddr, é localhost:50051 por padrão e pode ser substituído pela chave --addr na linha de comando ao executar o cliente.

Se o cliente precisar se conectar a um serviço que exige credenciais de autenticação, como TLS ou JWT, ele poderá transmitir um objeto DialOptions como parâmetro para grpc.NewClient que contenha as credenciais necessárias. O serviço RouteGuide não exige credenciais.

Depois que o canal gRPC é configurado, precisamos de um stub de cliente para realizar RPCs por chamadas de função Go. Recebemos esse stub usando o método NewRouteGuideClient fornecido pelo arquivo route_guide_grpc.pb.go gerado do arquivo .proto do aplicativo.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

Chamar métodos de serviço

No gRPC-Go, os RPCs operam em um modo de bloqueio/síncrono, o que significa que a chamada de RPC aguarda a resposta do servidor e retorna uma resposta ou um erro.

RPC simples

Chamar a RPC simples GetFeature é quase tão simples quanto chamar um método local, neste caso client.GetFeature:

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

O cliente chama o método no stub criado anteriormente. Para os parâmetros do método, o cliente cria e preenche um objeto de buffer de protocolo de solicitação Point. Você também transmite um objeto context.Context, que permite mudar o comportamento da RPC, se necessário, como definir um limite de tempo para a chamada ou cancelar uma RPC em andamento. Se a chamada não retornar um erro, o cliente poderá ler as informações de resposta do servidor no primeiro valor de retorno:

log.Println(feature)

No total, a função main() do cliente vai ficar assim:

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. Faça um teste

Confirme se o servidor e o cliente estão funcionando corretamente executando os seguintes comandos no diretório de trabalho do aplicativo:

  1. Execute o servidor em um terminal:
cd server
go run .
  1. Execute o cliente em outro terminal:
cd client
go run .

Você vai ver uma saída como esta, com carimbos de data/hora omitidos para maior clareza:

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. A seguir