Начало работы с gRPC-Rust

1. Введение

В этой лабораторной работе вы будете использовать gRPC-Rust для создания клиента и сервера, которые составят основу приложения для сопоставления маршрутов, написанного на Rust.

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

Служба определена в файле Protocol Buffers, который будет использоваться для генерации шаблонного кода для клиента и сервера, чтобы они могли взаимодействовать друг с другом, экономя ваше время и усилия при реализации этой функциональности.

Сгенерированный код учитывает не только сложности взаимодействия между сервером и клиентом, но также сериализацию и десериализацию данных.

Чему вы научитесь

  • Как использовать Protocol Buffers для определения API сервиса.
  • Как создать клиент и сервер на основе gRPC из определения Protocol Buffers с использованием автоматической генерации кода.
  • Понимание клиент-серверного взаимодействия с помощью gRPC.

Эта практическая работа предназначена для разработчиков Rust, впервые использующих gRPC или желающих освежить свои знания, а также для всех, кто интересуется разработкой распределённых систем. Опыт работы с gRPC не требуется.

2. Прежде чем начать

Предпосылки

Убедитесь, что у вас установлено следующее:

  • GCC. Следуйте инструкциям здесь.
  • Rust , версия 1.89.0. Инструкции по установке приведены здесь .

Получить код

Чтобы вам не пришлось начинать всё с нуля, эта лабораторная работа предоставляет вам заготовку исходного кода приложения. Следующие шаги покажут вам, как завершить приложение, включая использование плагинов компилятора буфера протокола для генерации шаблонного кода gRPC.

Сначала создайте рабочий каталог codelab и перейдите в него:

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

Загрузите и распакуйте кодовую лабораторию:

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

Кроме того, вы можете загрузить .zip-файл, содержащий только каталог codelab, и вручную распаковать его.

Если вы не хотите вводить реализацию вручную, готовый исходный код доступен на GitHub .

3. Определите услугу

Первым шагом будет определение службы gRPC приложения, её метода RPC и типов сообщений запросов и ответов с помощью Protocol Buffers . Ваша служба будет предоставлять:

  • Метод RPC, называемый GetFeature , который реализует сервер и вызывает клиент.
  • Типы сообщений Point и Feature представляют собой структуры данных, которыми обмениваются клиент и сервер при использовании метода GetFeature . Клиент предоставляет координаты карты в качестве Point в своём запросе GetFeature к серверу, а сервер отвечает соответствующим Feature , описывающим то, что находится в этих координатах.

Этот метод RPC и его типы сообщений будут определены в файле proto/route_guide.proto предоставленного исходного кода.

Буферы протоколов обычно называются protobuf. Подробнее о терминологии gRPC см. в разделе «Основные концепции, архитектура и жизненный цикл gRPC».

Метод обслуживания

Давайте сначала определим методы нашей службы, а затем типы сообщений: Point и Feature . Файл proto/routeguide.proto содержит структуру service с именем RouteGuide , которая определяет один или несколько методов, предоставляемых службой приложения.

Добавьте метод rpc GetFeature в определение RouteGuide . Как объяснялось ранее, этот метод ищет название или адрес местоположения по заданному набору координат, поэтому GetFeature должен возвращать Feature для заданной Point :

service RouteGuide {
  // Definition of the service goes here

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

Это унарный метод RPC: простой RPC , при котором клиент отправляет запрос серверу и ждет ответа, как при локальном вызове функции.

Типы сообщений

В файле proto/route_guide.proto исходного кода сначала определите тип сообщения Point . Point представляет собой пару координат (широта-долгота) на карте. В этой практической работе используйте целые числа в качестве координат:

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

Цифры 1 и 2 — это уникальные идентификационные номера для каждого из полей в структуре message .

Затем определите тип сообщения Feature . Feature использует string поле для имени или почтового адреса объекта, расположенного в месте, указанном Point :

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

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

4. Сгенерируйте клиентский и серверный код.

Мы уже предоставили вам сгенерированный код из файла .proto в сгенерированном каталоге.

Как и в любом проекте, нам нужно продумать зависимости, необходимые для нашего кода. Для проектов Rust зависимости будут находиться в Cargo.toml . Мы уже перечислили необходимые зависимости в файле Cargo.toml .

Если вы хотите узнать, как самостоятельно сгенерировать код из файла .proto , обратитесь к этим инструкциям .

Сгенерированный код содержит:

  • Определения структур для типов сообщений Point и Feature .
  • Сервисный признак, который нам необходимо реализовать: route_guide_server::RouteGuide .
  • Тип клиента, который мы будем использовать для вызова сервера: route_guide_client::RouteGuideClient<T> .

Далее мы реализуем метод GetFeature на стороне сервера, чтобы сервер мог ответить, когда клиент отправляет запрос.

5. Реализуйте услугу

В src/server/server.rs мы можем добавить сгенерированный код в область действия с помощью макроса gRPC include_generated_proto! и импортировать трейт RouteGuide и Point .

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

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

Начнем с определения структуры, представляющей нашу службу. На данный момент это можно сделать в src/server/server.rs :

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

Теперь нам нужно реализовать черту route_guide_server::RouteGuide из нашего сгенерированного кода.

Унарный RPC

RouteGuideService реализует все наши сервисные методы. Основная работа выполняется функцией get_feature на стороне сервера: она принимает сообщение Point от клиента и возвращает в сообщении Feature соответствующую информацию о местоположении из списка известных мест. Вот реализация функции в 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()))
    }
}

В методе заполните объект Feature соответствующей информацией для заданной Point , а затем верните его.

После реализации этого метода нам также необходимо запустить gRPC-сервер, чтобы клиенты могли использовать наш сервис. Замените main() на это.

#[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(())
}

Вот что происходит в main() , шаг за шагом:

  1. Укажите порт, который мы хотим использовать для прослушивания клиентских запросов.
  2. Создайте RouteGuideService с загруженными функциями, вызвав вспомогательную функцию load()
  3. Создайте экземпляр сервера gRPC с помощью RouteGuideServer::new() используя созданную нами службу.
  4. Зарегистрируйте реализацию нашей службы на сервере gRPC.
  5. Вызовите serve() на сервере с данными нашего порта, чтобы выполнить блокирующее ожидание, пока процесс не будет завершен.

6. Создайте клиента

В этом разделе мы рассмотрим создание клиента Rust для нашей службы RouteGuide в src/client/client.rs .

Как мы это сделали в src/server/server.rs , мы можем включить сгенерированный код в область действия с помощью макроса gRPC include_generated_code! и импортировать тип RouteGuideClient .

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

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

Методы обслуживания вызовов

В gRPC-Rust RPC работают в блокирующем/синхронном режиме, что означает, что вызов RPC ждет ответа сервера и либо возвращает ответ, либо ошибку.

Для вызова методов сервиса необходимо сначала создать канал для взаимодействия с сервером. Для этого сначала создаём конечную точку, подключаемся к ней и передаём созданный при подключении канал в RouteGuideClient::new() следующим образом:

#[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(())
}

В этой функции при создании клиента мы оборачиваем универсальный канал, созданный выше, сгенерированной заглушкой кода, которая реализует конкретные методы, определенные в службе .proto.

Простой RPC

Вызов простого RPC GetFeature почти так же прост, как вызов локального метода. Добавьте его в 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(())

Как видите, мы вызываем метод на полученной ранее заглушке. В параметрах метода создаём и заполняем объект буфера протокола запроса (в нашем случае Point ). Если вызов не возвращает ошибку, мы можем прочитать информацию об ответе сервера из первого возвращаемого значения.

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

В целом функция main() клиента должна выглядеть так:

#[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. Попробуйте

Для начала, чтобы запустить наш клиент и сервер, добавим их в качестве бинарных целей в наш контейнер. Необходимо отредактировать файл Cargo.toml и добавить следующее:

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

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

Затем выполните следующие команды из нашего рабочего каталога:

  1. Запустите сервер в одном терминале:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. Запустите клиент с другого терминала:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

Вы увидите примерно такой вывод, при этом временные метки опущены для ясности:

*** SIMPLE RPC ***

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

8. Что дальше?