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

1. Введение

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

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

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

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

Что вы узнаете

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

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

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

Предварительные требования

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

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

Получите код

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

Сначала создайте рабочую директорию codelab и перейдите в неё с помощью команды cd:

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

Скачайте и распакуйте 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

В качестве альтернативы вы можете скачать ZIP-архив, содержащий только папку codelab, и распаковать его вручную.

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

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

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

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

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

Протоколы Protocol Buffers обычно называются protobufs. Для получения дополнительной информации о терминологии 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 мы можем включить сгенерированный код в область видимости с помощью макроса include_generated_proto! в gRPC и импортировать трейт 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 в сгенерированном коде.

Унарный РПК

Класс 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 , мы можем включить сгенерированный код в область видимости с помощью макроса include_generated_code! в gRPC и импортировать тип 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. Что дальше?