Hello world!
Начало работы
Это руководство представляет собой введение в Tonic и предполагает, что у вас есть базовый опыт работы с Rust, а также понимание того, что такое Protocol Buffers. Если это не так, можете ознакомиться со страницами, ссылки на которые приведены в этом параграфе, и вернуться к этому руководству, когда будете готовы!
Предварительные требования
Для запуска примеров кода и прохождения руководства единственным предварительным требованием является сам Rust. rustup — удобный инструмент для его установки, если у вас его ещё нет.
Настройка проекта
Для этого руководства мы начнём с создания нового Rust-проекта с помощью Cargo:
$ cargo new helloworld-tonic
$ cd helloworld-tonic
tonic работает на Rust 1.39 и выше, так как требует поддержки функциональности async_await.
$ rustup update
Определение сервиса HelloWorld
Наш первый шаг — определить gRPC-сервис, а также типы запросов и ответов методов, используя Protocol Buffers. Мы будем хранить наши .proto-файлы в директории в корне проекта. Обратите внимание, что Tonic не особо важен путь, по которому лежат наши .proto-определения.
$ mkdir proto
$ touch proto/helloworld.proto
Затем вы определяете RPC-методы внутри определения вашего сервиса, указывая их типы запросов и ответов. gRPC позволяет определять четыре вида методов сервиса, и все они поддерживаются Tonic. В этом руководстве мы будем использовать только простой RPC. Если вы хотите увидеть пример Tonic, использующий все четыре вида, пожалуйста, прочитайте руководство по routeguide.
Сначала мы определяем имя нашего пакета, которое Tonic ищет при подключении ваших protobuf-файлов в клиентские и серверные приложения. Дадим ему имя helloworld.
syntax = "proto3";
package helloworld;
Далее нам нужно определить наш сервис. Этот сервис будет содержать фактические RPC-вызовы, которые мы будем использовать в нашем приложении. RPC содержит идентификатор, тип запроса и возвращает тип ответа. Вот наш сервис Greeter, который предоставляет RPC-метод SayHello.
service Greeter {
// Наш RPC SayHello принимает HelloRequests и возвращает HelloReplies
rpc SayHello (HelloRequest) returns (HelloReply);
}
Наконец, мы должны определить те типы, которые использовали выше в нашем RPC-методе SayHello. Типы RPC определяются как сообщения (messages), которые содержат типизированные поля. Вот как это будет выглядеть для нашего приложения HelloWorld:
message HelloRequest {
// Сообщение запроса содержит имя для приветствия
string name = 1;
}
message HelloReply {
// Ответ содержит приветственное сообщение
string message = 1;
}
Отлично! Теперь наш .proto-файл должен быть завершён и готов к использованию в нашем приложении. Вот как он должен выглядеть в готовом виде:
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
Настройка приложения
Теперь, когда мы определили protobuf для нашего приложения, мы можем начать писать наше приложение на Tonic! Давайте сначала добавим необходимые зависимости в Cargo.toml.
[package]
name = "helloworld-tonic"
version = "0.1.0"
edition = "2021"
[[bin]] # Исполняемый файл для запуска gRPC-сервера HelloWorld
name = "helloworld-server"
path = "src/server.rs"
[[bin]] # Исполняемый файл для запуска gRPC-клиента HelloWorld
name = "helloworld-client"
path = "src/client.rs"
[dependencies]
tonic = "*"
prost = "0.14"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
[build-dependencies]
tonic-build = "*"
Мы включаем tonic-build как удобный способ интегрировать генерацию нашего клиентского и серверного gRPC-кода в процесс сборки нашего приложения. Теперь мы настроим этот процесс сборки:
Генерация кода Сервера и Клиента
В корне вашего проекта (не в /src) создайте файл build.rs и добавьте следующий код:
fn main() -> Result<(), Box<dyn std::error::Error>> { tonic_build::compile_protos("proto/helloworld.proto")?; Ok(()) }
Это указывает tonic-build скомпилировать ваши protobuf-файлы при сборке вашего Rust-проекта. Хотя вы можете настроить этот процесс сборки различными способами, мы не будем вдаваться в подробности в этом вводном руководстве. Пожалуйста, ознакомьтесь с документацией по tonic-build для получения сведений о конфигурации.
Написание нашего Сервера
Теперь, когда процесс сборки написан и все наши зависимости настроены, мы можем начать писать интересные вещи! Нам нужно импортировать всё, что мы будем использовать на нашем сервере, включая protobuf-файл. Начните с создания файла server.rs в вашей директории /src и напишите следующий код:
#![allow(unused)] fn main() { use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); // Строка, указанная здесь, должна совпадать с именем proto-пакета } }
Далее давайте реализуем сервис Greeter, который мы ранее определили в нашем .proto-файле. Вот как это может выглядеть:
#![allow(unused)] fn main() { #[derive(Debug, Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request<HelloRequest>, // Принимаем запрос типа HelloRequest ) -> Result<Response<HelloReply>, Status> { // Возвращаем экземпляр типа HelloReply println!("Получен запрос: {:?}", request); let reply = HelloReply { // Мы должны использовать .into_inner(), так как поля gRPC-запросов и ответов приватны message: format!("Привет, {}!", request.into_inner().name), }; Ok(Response::new(reply)) // Отправляем обратно наше отформатированное приветствие } } }
Наконец, давайте определим рантайм Tokio, на котором фактически будет работать наш сервер. Это требует добавления Tokio в качестве зависимости, так что убедитесь, что вы его включили!
#[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse()?; let greeter = MyGreeter::default(); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) }
В целом ваш сервер должен выглядеть примерно так, когда вы закончите:
use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Debug, Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request<HelloRequest>, ) -> Result<Response<HelloReply>, Status> { println!("Получен запрос: {:?}", request); let reply = HelloReply { message: format!("Привет, {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse()?; let greeter = MyGreeter::default(); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) }
Теперь вы должны иметь возможность запустить ваш gRPC-сервер HelloWorld с помощью команды cargo run --bin helloworld-server. Это использует [[bin]], который мы определили ранее в нашем Cargo.toml, чтобы запустить именно сервер.
Если у вас есть GUI-клиент для gRPC, такой как Postman, вы сможете отправлять запросы на сервер и получать приветствия обратно!
Или, если вы используете grpcurl, то вы можете просто попробовать отправить запросы так:
$ grpcurl -plaintext -import-path ./proto -proto helloworld.proto -d '{"name": "Tonic"}' '[::1]:50051' helloworld.Greeter/SayHello
И получать ответы вида:
{
"message": "Hello Tonic!"
}
Написание нашего Клиента
Итак, теперь у нас есть работающий gRPC-сервер, и это здорово, но как наше приложение может с ним взаимодействовать? Здесь нам пригодится наш клиент. Tonic поддерживает реализации как клиента, так и сервера. Аналогично серверу, мы начнём с создания файла client.rs в нашей директории /src и импорта всего, что нам понадобится:
#![allow(unused)] fn main() { use hello_world::greeter_client::GreeterClient; use hello_world::HelloRequest; pub mod hello_world { tonic::include_proto!("helloworld"); } }
Клиент намного проще сервера, так как нам не нужно реализовывать какие-либо методы сервиса, а только делать запросы. Вот рантайм Tokio, который сделает наш запрос и выведет ответ в ваш терминал:
#[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("ОТВЕТ={:?}", response); Ok(()) }
Вот и всё! Наш завершённый клиентский файл должен выглядеть примерно так, как показано ниже. Если это не так, вернитесь и убедитесь, что вы всё сделали правильно:
use hello_world::greeter_client::GreeterClient; use hello_world::HelloRequest; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("ОТВЕТ={:?}", response); Ok(()) }
Собираем всё вместе
На этом этапе мы написали наш protobuf-файл, файл сборки для компиляции наших protobuf-файлов, сервер, который реализует наш сервис SayHello, и клиент, который делает запросы к нашему серверу. У вас должен быть файл proto/helloworld.proto, файл build.rs в корне проекта, а также файлы src/server.rs и src/client.rs.
Чтобы запустить сервер, выполните cargo run --bin helloworld-server.
Чтобы запустить клиент, выполните cargo run --bin helloworld-client в другом окне терминала.
Вы должны увидеть запрос, залогированный сервером в его окне терминала, а также ответ, залогированный клиентом в его окне.
Поздравляем с прохождением этого вводного руководства! Мы надеемся, что это пошаговое руководство помогло вам понять основы Tonic и то, как начать писать высокопроизводительные, совместимые и гибкие gRPC-серверы на Rust. Для более углублённого руководства, демонстрирующего продвинутый gRPC-сервер на Tonic, пожалуйста, ознакомьтесь с [руководством по routeguide].