![image](https://habrastorage.org/webt/16/_f/vx/16_fvxl8lcvp1epln9yocxivzfq.png)
❯ Введение
В сегодняшнем туториале по Rust мы откроем для себя мир gRPC. Для этого создадим очень простой микросервис с единственной конечной точкой, который будет отзеркаливать то сообщение, что мы ему пошлем. Чтобы протестировать наш микросервис, мы также напишем простой клиент на Rust.
Перед изучением этого поста также будет полезно посмотреть предыдущие публикации автора по Rust:
https://blog.ediri.io/lets-build-a-cli-in-rust
https://blog.ediri.io/how-to-asyncawait-in-rust-an-introduction
❯ Предпосылки
Прежде, чем приступить к делу, нужно убедиться, что у нас установлены следующие инструменты:
- Rust
- IDE или текстовый редактор на ваш выбор
- Компилятор буфера протоколов (protoc)
❯ Установка protoc
Чтобы сгенерировать код gRPC, необходимо установить компилятор
protoc
. Инструкции по установке на вашей платформе можете посмотреть здесь.Если вы работаете в macOS, то установку можно выполнить при помощи Homebrew:
brew install protobuf
Убедитесь, что компилятор protoc доступен в вашем пути PATH:
protoc --version # should print the version
# libprotoc 3.21.9
Теперь, когда мы всё обустроили, давайте немного обсудим вопрос: что такое gRPC 📡?
❯ Что такое gRPC 📡?
В gRPC клиентское приложение может непосредственно вызывать методы непосредственно в серверном приложении на другой машине, как если бы это был локальный объект. Так упрощается создание распределённых приложений и сервисов. На стороне сервера реализуется сервис и выполняется сервер gRPC, обрабатывающий клиентские вызовы. На стороне клиента стоит заглушка, предоставляющая те же методы, что и сервер.
❯ Буферы протоколов
Буферы протоколов (Protocol Buffers) – это разработанный Google расширяемый механизм, нейтральный на уровне языка и платформы, предназначенный для сериализации структурированных данных. Эти буферы используются в gRPC по умолчанию.
Приведу пример, демонстрирующий, как работают Protocol Buffers. На первом этапе мы определяем структуру данных в файле с расширением .proto. Данные буфера протоколов структурированы в сообщениях, представляющих собой коллекции именованных полей. Вот очень упрощённый пример такого сообщения:
message Weather {
string city = 1;
int32 temperature = 2;
}
Когда мы определили наше сообщение, можно воспользоваться компилятором
protoc
, чтобы сгенерировать классы доступа к данным на том языке, что вам нравится (на основании proto-определения). В сгенерированных классах будут методы доступа к каждому из полей в сообщении.Можно определить gRPC-сервисы в том же .proto-файле, что и сообщения, с теми же RPC-методами, что используют эти сообщения.
service WeatherService {
rpc GetWeather (WeatherRequest) returns (WeatherResponse) {}
}
message WeatherRequest {
string city = 1;
}
message WeatherResponse {
string forecast = 1;
}
Затем можно воспользоваться компилятором
protoc
, чтобы сгенерировать интерфейсы gRPC-клиента и сервера из .proto-сервиса.❯ Создаем микросервис на Rust
Создаём новый проект
Для начала создадим новый проект при помощи команды
cargo
:cargo new
Добавляем поддержку CLI
Мы собираемся воспользоваться контейнером clap, чтобы добавить поддержку CLI к нашему микросервису и клиенту. Добавим в наш проект зависимость при помощи следующей команды:
cargo add clap --features derive
Создаём Proto-файл
Далее создадим новый каталог под названием
proto
, и в этом каталоге положим новый файл echo.proto
. Далее определим наш сервис и те сообщения, которые собираемся использовать:syntax = "proto3";
package api;
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse);
}
❯ Сгенерируем код Rust из Proto-файла
Чтобы сгенерировать код Rust из proto-файла, воспользуемся контейнером tonic-build. Нам потребуется добавить его в наш проект как зависимость сборки при помощи следующей команды:
cargo add tonic-build --build
Теперь можно добавить следующий код в наш файл
build.rs
:fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/echo.proto")?;
Ok(())
}
Как правило, мы выполняем команду
cargo build
, чтобы сгенерировать код Rust
из proto-файла. В IntelliJ IDEA вам, возможно, придётся активировать org.rust.cargo.evaluate.build.scripts
в разделе настроек «Experimental Features
» (Экспериментальные возможности), чтобы всё заработало. tonic-build
входит в состав контейнера tonic, который представляет собой реализацию gRPC поверх HTTP/2; эта реализация заточена на высокую производительность, интероперабельность и гибкость. В основе этой реализации лежат hyper
, tokio
и prost
.Вот некоторые возможности
tonic
:- Двунаправленная потоковая передача
- Высокопроизводительный асинхронный ввод/вывод
- Интероперабельность
- TLS, поддерживаемая rustls
- Балансировка нагрузки
- Пользовательские метаданные
- Аутентификация
- Проверка работоспособности
Наконец, нам потребуется добавить
tokio
к нашему проекту в качестве зависимости:cargo add tokio --features macros, rt-multi-thread
Теперь, когда сгенерирован весь код gRPC 📡, можно приступать к реализации нашего микросервиса.
❯ Реализация микросервиса
Для начала создадим в каталоге src новый файл под названием
server.rs.rs
. Здесь мы собираемся реализовать нашу серверную логику.Сначала нам потребуется импортировать сгенерированный код из нашего proto-файла, а также из контейнеров
tonic
и clap
:use tonic::{transport::Server, Request, Response, Status};
use api::echo_service_server::{EchoService, EchoServiceServer};
use api::{EchoRequest, EchoResponse};
use ::clap::{Parser};
Также понадобится включить сгенерированные из proto элементы для клиента и сервера, воспользовавшись для этого макросом
include_proto!
:pub mod api {
tonic::include_proto!("api");
}
Теперь можно реализовать сервисную логику нашего микросервиса. Мы собираемся реализовать метод
Echo
для нашего сервиса, и этот метод будет отзеркаливать то сообщение, которое мы отправили сервису. Здесь мы применяем ключевое слово async
, чтобы функция стала асинхронной, а также #[tonic::async_trait]
, чтобы обеспечить совместимость с tonic
.#[derive(Debug, Default)]
pub struct Echo {}
#[tonic::async_trait]
impl EchoService for Echo {
async fn echo(&self, request: Request<EchoRequest>) -> Result<Response<EchoResponse>, Status> {
println!("Got a request: {:?}", request);
let reply = EchoResponse {
message: format!("{}", request.into_inner().message),
};
Ok(Response::new(reply))
}
}
Теперь можно запустить наш сервер и слушать входящие запросы. Чтобы сконфигурировать хост и порт нашего сервера, воспользуемся
clap
. #[derive(Parser)]
#[command(author, version)]
#[command(about = "echo-server - a simple echo microservice", long_about = None)]
struct ServerCli {
#[arg(short = 's', long = "server", default_value = "127.0.0.1")]
server: String,
#[arg(short = 'p', long = "port", default_value = "50052")]
port: u16,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = ServerCli::parse();
let addr = format!("{}:{}", cli.server, cli.port).parse()?;
let echo = Echo::default();
println!("Server listening on {}", addr);
Server::builder()
.add_service(EchoServiceServer::new(echo))
.serve(addr)
.await?;
Ok(())
}
Далее определим в файле
Cargo.toml
целевой бин:[[bin]]
name = "echo-server"
path = "src/server.rs"
Запустим сервер:
cargo run --bin echo-server
После чего должен получиться следующий вывод:
Server listening on 127.0.0.1:50052
Можно сконфигурировать хост и порт сервера при помощи флагов
--server
и --port
:cargo run --bin echo-server -- --server 0.0.0.0 --port 50051
❯ Реализация клиента
Для реализации клиента добавим следующие строки в имеющийся у нас файл
main.rs
. Сначала нужно импортировать сгенерированный код из нашего proto-файла и из контейнера clap
, чтобы разобрать аргументы командной строки:use api::echo_service_client::EchoServiceClient;
use api::EchoRequest;
use ::clap::{Parser};
pub mod api {
tonic::include_proto!("api");
}
Как и в случае с сервером, воспользуемся
clap
, чтобы сконфигурировать хост и порт для нашего клиента. Здесь мы задействуем аргумент message
, чтобы отправить на сервер выбранное нами сообщение: #[derive(Parser)]
#[command(author, version)]
#[command(about = "echo - a simple CLI to send messages to a server", long_about = None)]
struct ClientCli {
#[arg(short = 's', long = "server", default_value = "127.0.0.1")]
server: String,
#[arg(short = 'p', long = "port", default_value = "50052")]
port: u16,
/// The message to send
message: String,
}
Нам осталось написать только главную функцию, которая будет создавать клиент и отправлять сообщение на сервер. Поскольку мы используем режим
async/await
, нам потребуется среда выполнения tokio
. Для этого следует добавить атрибут #[tokio::main]
к нашей главной функции:#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = ClientCli::parse();
let mut client = EchoServiceClient::connect(format!("http://{}:{}", cli.server, cli.port)).await?;
let request = tonic::Request::new(EchoRequest {
message: cli.message,
});
let response = client.echo(request).await?;
println!("RESPONSE={:?}", response.into_inner().message);
Ok(())
}
Также определим в нашем файле Cargo.toml целевой бин и для нашего клиента:
[[bin]]
name = "echo-client"
path = "src/main.rs"
Запустим клиент:
cargo run --bin echo-client -- "Hello World!"
Должен получиться следующий вывод:
RESPONSE="Hello World"
Сконфигурировать хост и порт для клиента можно при помощи флагов
--server
и --port
, примерно как и в случае с сервером.❯ Заключение
В этой статье было рассказано, как при помощи
tonic
и clap
написать простой gRPC-микросервис на Rust. Также мы узнали, как написать proto-файл и сгенерировать код для клиента и сервера при помощи tonic-build
посредством build.rs
Эта техническая статья, как и большинство ей подобных, позволяет рассмотреть тему только в самом общем виде. Такие технологии как
gRPC
гораздо сложнее, и я настоятельно рекомендую вам почитать официальную документацию по tonic
и gRPC
, чтобы подробнее разобраться в этой теме.Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
![](https://habrastorage.org/webt/r8/ms/jc/r8msjcfet9mgza3ybpor_sdgrt0.jpeg)