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:
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
eFeature
, que são estruturas de dados trocadas entre o cliente e o servidor ao usar o métodoGetFeature
. O cliente fornece coordenadas do mapa como umPoint
na solicitaçãoGetFeature
ao servidor, e o servidor responde com umFeature
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
eFeature
. - 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:
- Especifique a porta que queremos usar para detectar solicitações do cliente.
- Crie um
RouteGuideService
com recursos carregados chamando a função auxiliarload()
. - Crie uma instância do servidor gRPC usando
RouteGuideServer::new()
com o serviço que criamos. - Registre nossa implementação de serviço com o servidor gRPC.
- 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:
- Execute o servidor em um terminal:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- 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
- Saiba como o gRPC funciona em Introdução ao gRPC e Conceitos principais.
- Siga as etapas do tutorial de noções básicas.