Primeiros passos com o gRPC-Rust

1. Introdução

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

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 do Rust que não conhecem o gRPC ou querem relembrar o gRPC, ou 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:

  • GCC. Siga as instruções aqui.
  • Rust, versão 1.89.0. Siga as instruções de instalação aqui.

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.

Primeiro, crie o diretório de trabalho do codelab e use cd para acessar ele:

mkdir grpc-rust-getting-started && cd grpc-rust-getting-started

Faça o download e extraia o codelab:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-rust-getting-started/start_here

Como alternativa, baixe o arquivo .zip que contém apenas o diretório do codelab e descompacte-o manualmente.

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

Método de serviço

Primeiro, vamos definir os métodos de serviço e, em seguida, os tipos de mensagem Point e Feature. O arquivo proto/routeguide.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.

Tipos de mensagem

No arquivo proto/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;
}

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

Já fornecemos o código gerado do arquivo .proto no diretório gerado.

Como em qualquer projeto, precisamos pensar nas dependências necessárias para nosso código. Para projetos Rust, as dependências estarão em Cargo.toml. Já listamos as dependências necessárias no arquivo Cargo.toml.

Se quiser saber como gerar código do arquivo .proto, consulte estas instruções.

O código gerado contém:

  • Definições de struct para tipos de mensagem Point e Feature.
  • Um traço de serviço que precisaremos implementar: route_guide_server::RouteGuide.
  • Um tipo de cliente que vamos usar para chamar o servidor: route_guide_client::RouteGuideClient<T>.

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

Em src/server/server.rs, podemos trazer o código gerado para o escopo usando a macro include_generated_proto! do gRPC e importar a característica RouteGuide e Point.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

pub use grpc_pb::{
    route_guide_server::{RouteGuideServer, RouteGuide},
    Point, Feature,
};

Podemos começar definindo uma struct para representar nosso serviço. Por enquanto, vamos fazer isso em src/server/server.rs:

#[derive(Debug)]
pub struct RouteGuideService {
    features: Vec<Feature>,
}

Agora, precisamos implementar a característica route_guide_server::RouteGuide do código gerado.

RPC unário

O RouteGuideService implementa todos os nossos métodos de serviço. A função get_feature 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 src/server/server.rs:

#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
    async fn get_feature(&self, request: Request<Point>) -> Result<Response<Feature>, Status> {
        println!("GetFeature = {:?}", request);
        let requested_point = request.get_ref();
        for feature in self.features.iter() {
            if feature.location().latitude() == requested_point.latitude() {
                if feature.location().longitude() == requested_point.longitude(){
                    return Ok(Response::new(feature.clone()))
                };
            };    
        }
        Ok(Response::new(Feature::default()))
    }
}

No método, preencha um objeto Feature com as informações adequadas para o Point especificado e retorne-o.

Depois de implementar esse método, também precisamos iniciar um servidor gRPC para que os clientes possam usar nosso serviço. Substitua main() por isto.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:10000".parse().unwrap();
    println!("RouteGuideServer listening on: {addr}");
    let route_guide = RouteGuideService {
        features: load(),
    };
    let svc = RouteGuideServer::new(route_guide);
    Server::builder().add_service(svc).serve(addr).await?;
    Ok(())
}

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

  1. Especifique a porta que queremos usar para detectar solicitações do cliente.
  2. Crie um RouteGuideService com recursos carregados chamando a função auxiliar load().
  3. Crie uma instância do servidor gRPC usando RouteGuideServer::new() com o serviço que criamos.
  4. Registre nossa implementação de serviço com o servidor gRPC.
  5. Chame serve() no servidor com os detalhes da porta para fazer uma espera de bloqueio até que o processo seja encerrado.

6. Criar o cliente

Nesta seção, vamos criar um cliente Rust para nosso serviço RouteGuide em src/client/client.rs.

Como fizemos em src/server/server.rs, podemos trazer o código gerado para o escopo usando a macro include_generated_code! do gRPC e importar o tipo RouteGuideClient.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

use grpc_pb::{
    route_guide_client::RouteGuideClient,
    Point,
};

Chamar métodos de serviço

No gRPC-Rust, 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.

Para chamar métodos de serviço, primeiro precisamos criar um canal para se comunicar com o servidor. Para isso, primeiro criamos um endpoint, nos conectamos a ele e transmitimos o canal criado ao se conectar a RouteGuideClient::new() da seguinte maneira:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create endpoint to connect to
    let endpoint = Endpoint::new("http://[::1]:10000")?; 
    let channel = endpoint.connect().await?;             

    // Create a new client
    let mut client = RouteGuideClient::new(channel);
    Ok(())
}

Nessa função, ao criar o cliente, encapsulamos o canal genérico criado acima com o stub de código gerado que implementa os métodos específicos definidos no serviço .proto.

RPC simples

Chamar o RPC simples GetFeature é quase tão simples quanto chamar um método local. Adicione isso em main().

println!("*** SIMPLE RPC ***");
let point = proto!(Point{
    latitude: 409_146_138,
    longitude: -746_188_906
});
let response = client
    .get_feature(Request::new(point))
    .await?.into_inner();
Ok(())

Como você pode ver, chamamos o método no stub que recebemos antes. Nos parâmetros do método, criamos e preenchemos um objeto de buffer de protocolo de solicitação (no nosso caso, Point). Se a chamada não retornar um erro, poderemos ler as informações de resposta do servidor no primeiro valor de retorno.

println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
    response.name(),
    response.location().latitude(),
    response.location().longitude());

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    //Create endpoint to connect to
    let endpoint = Endpoint::new("http://[::1]:10000")?; 
    let channel = endpoint.connect().await?;             

    // Create a new client
    let mut client = RouteGuideClient::new(channel); 

    println!("*** SIMPLE RPC ***");
    let point = proto!(Point{
        latitude: 409_146_138,
        longitude: -746_188_906
    });
    let response = client
        .get_feature(Request::new(point))
        .await?.into_inner();

    println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
        response.name(),
        response.location().latitude(),
        response.location().longitude());
    Ok(())
}

7. Faça um teste

Primeiro, para executar o cliente e o servidor, vamos adicioná-los como destinos binários ao nosso crate. Precisamos editar nosso Cargo.toml de acordo e adicionar o seguinte:

[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"

[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"

Em seguida, execute os seguintes comandos no diretório de trabalho:

  1. Execute o servidor em um terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. Execute o cliente em outro terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

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

*** SIMPLE RPC ***

FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906

8. A seguir