image

Введение


В сегодняшнем туториале по 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, чтобы подробнее разобраться в этой теме.

Комментарии (18)


  1. web3_Venture
    00.00.0000 00:00
    -4

    ребят go, c++, rust слабее .net .

    Парадигма "производительность - выбирай более низкоуровневый язык" , всё?



    1. Gorthauer87
      00.00.0000 00:00
      +2

      Разница в пределах стат погрешности. Но в целом, .net видимо довольно хорошо написали реализацию grpc.


    1. GerrAlt
      00.00.0000 00:00
      +1

      производительность - выбирай более низкоуровневый язык

      Приведенная "парадигма" очевидно никогда не была истинной - для того чтобы писать на чем-то "более низкоуровневом" и иметь от этого какой-то прирост производительности надо уметь писать на этом "более низкоуровневом" как минимум не хуже чем те кто писал "более высокоуровневые" инструменты


    1. k-morozov
      00.00.0000 00:00
      +7

      ох уже эти свидетели языков, с gc, который не дает никакого overhead.


    1. domix32
      00.00.0000 00:00
      +5

      А откуда дровишки-то? Я таких в паинте тоже нарисовать могу


    1. GoodGod
      00.00.0000 00:00
      +2

      Не совсем. Дело в том, что в тестах techempower, который показывает самый производительный Web фреймворк используется не коробочное решение, а максимально затюненое решение. Затюненое до такой степени, что читать код невозможно.

      Был статья на хабре об этом.

      https://habr.com/ru/post/701352/

      Т.е. в реальной жизни все так - именно в классической парадигме.

      https://www.techempower.com/benchmarks/


    1. k-morozov
      00.00.0000 00:00
      +1

      Есть хорошая статья здесь же на Хабре, которая описывает как команда .NET добилась таких результатов и в целом грамотный обзор этих сравнений.
      https://habr.com/ru/post/701352/


  1. Stas911
    00.00.0000 00:00
    +2

    Неплохо бы репо с кодом на гитхабе добавить


  1. mkpankov
    00.00.0000 00:00
    +4

    Эхо-сервер gRPC в статье на 6 минут с пометкой "Сложный"?) Я не понимаю эту категоризацию сложностей материалов


    1. domix32
      00.00.0000 00:00
      +2

      Люди отдалённые от микросервисов не очень понимают про grpc, а далёкие от раст что за derive/Box<dyn BlaBla>/[tokio::main].То бишь для понимания нужна "инсайдерская" инфа про всё это, потому и сложный.


  1. AlexeyK77
    00.00.0000 00:00
    +1

    Просвятите пожалуйста за место классического rpc в 2023г
    Не программирую уже очень давно, лет 15 наверное. Но последнее что писал, так это была CORBA на С++. И как раз застал времена окончания корбы и появления SOAP (и всего этого околодинамического через веб).

    Были жуткие баттлы и дискусии преимущества и недостатков разных подходов. Вкратце сторонники RPC-style (корба, dcom, и т.п.) упирали на эффективность работы решения в части общей производительности, сетевого трафика, которая минимум раз в 10 была быстрее тогда. У сторонников динамического подхода основным аргументом была гибкость, и то , что в отличие от корбы трафик инкапсулировался в хттп, т.е. не надо было пилить дырки в фаерволе.

    Если отвлечься от неудобства самой корбы (особенно ее жуткого маппинга на C++), то вцелом RPC-технологии именно и предполагали статический подход: описываем интерфейсы, потом отдельным компилятором файл интерфефсов превращается в C++ исходник с заглушками, который далее надо было наполнять изнутри. И все было хорошо, пока не было нужды меняьт этот файл интерфейсов и заново генерировать реализацию. Адаптация старого кода в новый - уже головная боль разработчика.

    В общем статический подход уступил пусть неэффективному, но динамическому в век интернета и зарождения веб-сервисов. Потом соап уступил место более вмеyяемому json (ну эту эпоху я уже пропустил).

    А как дела обстоят сейчас. к чему пришло?

    (P.S. в качестве ностальгии вспоминаю уютный форум corba.kubsu.ru, вдруг кто из олдскульщиков его помнит)


    1. domix32
      00.00.0000 00:00

      В эпоху микросервисов проще пересобрать всю статику и не давать разрастаться микро до размеров монолита. В итоге адаптация занимает минимум времени. Плюс ко всему ещё всякие интеграционные тесты пишут, чтобы взаимодействие с остальными микросервисами проверять. Поэтому, если не забывать всё это делать, то головная боль не настолько великая.


      1. AlexeyK77
        00.00.0000 00:00

        т.е. круг замкнулся и подход: .idl->idl-compiler->interface_stub.cpp/.->comilier снова в тренде?
        а как сейчас принято, когда надо менять интерфейсы и вносить изменения дальше по цепочке?


        1. domix32
          00.00.0000 00:00
          +1

          Круг замкнулся, правда в модерновых языках сборка интерфейсов делается не выходя из языка.

          Для остального есть semver. Делаем версию микро с изменениями, прогоняем тесты, пушим в местный регистри в качестве зависимости. Берём зависимый микро, поднимаем версию для него до актуальной, чиним что сломалось, прогоняем тесты, интеграцию, пушим в местный регистри с новой версией. Ну и так далее. Когда последний консумер удовлетворён поднимаем версию микро в CI конфигах.


    1. mayorovp
      00.00.0000 00:00
      +1

      SOAP — это такая же "статика", отличие тут в текстовом формате против бинарного.


      И погубила Корбу не её статичность, а непонятные усложнения на пустом месте (позже они же погубили SOAP). Задача клиента RPC — просто вызвать метод и получить ответ. Задача сервера — просто получить запрос и вернуть значение. А вместо этого надо настраивать какие-то брокеры запросов, предварительно выбрав нужный. Там, среди тех брокеров, хоть были in-process реализации, или в то время до такого примитивизма даже не додумались?


      А ещё есть проблема своевременной реакции вендоров — с этими закрытыми брокерами любая проблема грозит стать неразрешимой.


      Возьмём необходимость обратных вызовов. Даже такой древний протокол, как FTP, умел работать через NAT. GIOP научился использовать TCP соединение в обе стороны только в 1.2 версии, вышедшей в 1998 году. Ну, это спека в 1998 году вышла, а вот в брокере от Sun двунаправленные соединения не поддерживались даже в 2004м году.


      Кстати, SOAP поверх HTTP эту проблему не решает никак. К счастью, SOAP оказался куда более гибким, его можно хоть в тот самый двусторонний TCP завернуть, хоть в более поздние веб-сокеты.


  1. Inobelar
    00.00.0000 00:00

    Интересно было бы посмотреть на rust gRpc с flatbuffers вместо protobuf (for perfomance reason). gRpc + flatbuffers на c++ вроде можно (хотя были казусы, когда тима gRpc ломала возможность вставить flatbuffers заместо protobuf) - соответственно, интересно, можно ли так-же на rust?


    1. domix32
      00.00.0000 00:00

      Дока про Rust есть, соотвественнно и крейт где-то должен быть.