1. Wprowadzenie
W tym laboratorium wykorzystasz gRPC-Rust do utworzenia klienta i serwera, które będą stanowić podstawę aplikacji do mapowania tras napisanej w języku Rust.
Po ukończeniu tego samouczka będziesz mieć klienta, który łączy się z serwerem zdalnym za pomocą gRPC, aby uzyskać nazwę lub adres pocztowy miejsca znajdującego się pod określonymi współrzędnymi na mapie. W pełni funkcjonalna aplikacja może korzystać z tego modelu klient-serwer, aby wyliczać lub podsumowywać ważne miejsca na trasie.
Usługa jest zdefiniowana w pliku Protocol Buffers, który będzie używany do generowania kodu szablonowego dla klienta i serwera, aby mogły się ze sobą komunikować. Dzięki temu zaoszczędzisz czas i wysiłek potrzebny na wdrożenie tej funkcji.
Wygenerowany kod obsługuje nie tylko złożoność komunikacji między serwerem a klientem, ale także serializację i deserializację danych.
Czego się nauczysz
- Jak używać buforów protokołu do definiowania interfejsu API usługi.
- Jak utworzyć klienta i serwer oparte na gRPC na podstawie definicji Protocol Buffers za pomocą automatycznego generowania kodu.
- Znajomość komunikacji klient-serwer z użyciem gRPC.
Te ćwiczenia z programowania są przeznaczone dla programistów języka Rust, którzy dopiero zaczynają korzystać z gRPC lub chcą sobie przypomnieć jego działanie, a także dla wszystkich innych osób zainteresowanych tworzeniem systemów rozproszonych. Nie musisz mieć wcześniejszego doświadczenia z gRPC.
2. Zanim zaczniesz
Wymagania wstępne
Sprawdź, czy masz zainstalowane te elementy:
- GCC. Postępuj zgodnie z instrukcjami tutaj
- Rust w wersji 1.89.0. Postępuj zgodnie z instrukcjami instalacji, które znajdziesz tutaj.
Pobierz kod
Aby nie trzeba było zaczynać od zera, w tym ćwiczeniu znajdziesz szkielet kodu źródłowego aplikacji, który możesz uzupełnić. Z tych instrukcji dowiesz się, jak dokończyć aplikację, w tym jak użyć wtyczek kompilatora buforów protokołów do wygenerowania kodu gRPC.
Najpierw utwórz katalog roboczy z ćwiczeniami i przejdź do niego:
mkdir grpc-rust-getting-started && cd grpc-rust-getting-started
Pobierz i rozpakuj ćwiczenia z programowania:
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
Możesz też pobrać plik ZIP zawierający tylko katalog z instrukcjami i rozpakować go ręcznie.
Jeśli nie chcesz wpisywać implementacji, gotowy kod źródłowy jest dostępny na GitHubie.
3. Określ usługę
Pierwszym krokiem jest zdefiniowanie usługi gRPC aplikacji, jej metody RPC oraz typów wiadomości żądania i odpowiedzi za pomocą buforów protokołu. Twoja usługa będzie zapewniać:
- Metoda RPC o nazwie
GetFeature
, która jest zaimplementowana na serwerze i wywoływana przez klienta. - Typy wiadomości
Point
iFeature
to struktury danych wymieniane między klientem a serwerem podczas korzystania z metodyGetFeature
. Klient podaje współrzędne mapy jakoPoint
w żądaniuGetFeature
wysyłanym do serwera, a serwer odpowiada, przesyłając odpowiedniFeature
, który opisuje wszystko, co znajduje się pod tymi współrzędnymi.
Ta metoda RPC i jej typy wiadomości będą zdefiniowane w pliku proto/route_guide.proto
podanego kodu źródłowego.
Protokoły buforów są powszechnie znane jako protobufy. Więcej informacji o terminologii gRPC znajdziesz w artykule Podstawowe koncepcje, architektura i cykl życia.
Metoda usługi
Najpierw zdefiniujmy metody usługi, a potem typy wiadomości Point
i Feature
. Plik proto/routeguide.proto
ma strukturę service
o nazwie RouteGuide
, która definiuje co najmniej 1 metodę udostępnianą przez usługę aplikacji.
Dodaj metodę rpc
GetFeature
w definicji RouteGuide
. Jak wspomnieliśmy wcześniej, ta metoda wyszukuje nazwę lub adres lokalizacji na podstawie podanego zestawu współrzędnych, więc w przypadku danego Point
zwraca GetFeature
Feature
:
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
Jest to metoda RPC typu unary: prosta procedura RPC, w której klient wysyła żądanie do serwera i czeka na odpowiedź, podobnie jak w przypadku lokalnego wywołania funkcji.
Rodzaje wiadomości
W proto/route_guide.proto
pliku kodu źródłowego najpierw zdefiniuj Point
typ wiadomości. Symbol Point
reprezentuje parę współrzędnych szerokości i długości geograficznej na mapie. W tym ćwiczeniu używaj liczb całkowitych jako współrzędnych:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Numery 1
i 2
to unikalne identyfikatory poszczególnych pól w strukturze message
.
Następnie określ Feature
typ wiadomości. Feature
używa pola string
na nazwę lub adres pocztowy czegoś w lokalizacji określonej przez Point
:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
4. Generowanie kodu klienta i serwera
Wygenerowany kod z pliku .proto
w wygenerowanym katalogu został już Ci przekazany.
Jak w przypadku każdego projektu, musimy pomyśleć o zależnościach, które są niezbędne dla naszego kodu. W przypadku projektów w Rust zależności będą się znajdować w pliku Cargo.toml
. Wymagane zależności zostały już wymienione w pliku Cargo.toml
.
Jeśli chcesz dowiedzieć się, jak samodzielnie wygenerować kod z pliku .proto
, zapoznaj się z tymi instrukcjami.
Wygenerowany kod zawiera:
- Definicje struktur dla typów wiadomości
Point
iFeature
. - Cechę usługi, którą musimy wdrożyć:
route_guide_server::RouteGuide
. - Typ klienta, którego użyjemy do wywołania serwera:
route_guide_client::RouteGuideClient<T>
.
Następnie zaimplementujemy metodę GetFeature
po stronie serwera, aby gdy klient wyśle żądanie, serwer mógł odpowiedzieć.
5. Wdrażanie usługi
W src/server/server.rs
możemy uwzględnić wygenerowany kod za pomocą makra include_generated_proto!
gRPC i zaimportować cechę RouteGuide
oraz Point
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
pub use grpc_pb::{
route_guide_server::{RouteGuideServer, RouteGuide},
Point, Feature,
};
Zacznijmy od zdefiniowania struktury reprezentującej naszą usługę. Na razie możemy to zrobić w src/server/server.rs
:
#[derive(Debug)]
pub struct RouteGuideService {
features: Vec<Feature>,
}
Teraz musimy zaimplementować cechę route_guide_server::RouteGuide
z wygenerowanego kodu.
Unary RPC
RouteGuideService
wdraża wszystkie nasze metody obsługi. Funkcja get_feature
po stronie serwera wykonuje główną pracę: pobiera wiadomość Point
od klienta i zwraca w wiadomości Feature
odpowiednie informacje o lokalizacji z listy znanych miejsc. Oto implementacja funkcji w języku 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()))
}
}
W tej metodzie wypełnij obiekt Feature
odpowiednimi informacjami dla danego Point
, a następnie go zwróć.
Po wdrożeniu tej metody musimy też uruchomić serwer gRPC, aby klienci mogli korzystać z naszej usługi. Zastąp main()
tym tekstem.
#[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(())
}
Oto, co dzieje się w main()
krok po kroku:
- Określ port, którego mamy używać do nasłuchiwania żądań klientów
- Utwórz
RouteGuideService
z cechami wczytanymi przez wywołanie funkcji pomocniczejload()
. - Utwórz instancję serwera gRPC za pomocą
RouteGuideServer::new()
, korzystając z utworzonej usługi. - Zarejestruj implementację usługi na serwerze gRPC.
- Wywołaj funkcję
serve()
na serwerze, podając szczegóły portu, aby zablokować oczekiwanie do momentu zakończenia procesu.
6. Tworzenie klienta
W tej sekcji przyjrzymy się tworzeniu klienta Rust dla naszej usługi RouteGuide w src/client/client.rs
.
Podobnie jak w src/server/server.rs
, możemy wprowadzić wygenerowany kod do zakresu za pomocą makra include_generated_code!
gRPC i zaimportować typ RouteGuideClient
.
mod grpc_pb {
grpc::include_generated_proto!("generated", "routeguide");
}
use grpc_pb::{
route_guide_client::RouteGuideClient,
Point,
};
Wywoływanie metod usługi
W gRPC-Rust wywołania RPC działają w trybie blokującym/synchronicznym, co oznacza, że wywołanie RPC czeka na odpowiedź serwera i zwraca odpowiedź lub błąd.
Aby wywołać metody usługi, musimy najpierw utworzyć kanał do komunikacji z serwerem. Najpierw tworzymy punkt końcowy, łączymy się z nim i przekazujemy utworzony w trakcie połączenia kanał do RouteGuideClient::new()
w ten sposób:
#[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(())
}
W tej funkcji podczas tworzenia klienta opakowujemy ogólny kanał utworzony powyżej wygenerowanym fragmentem kodu, który implementuje konkretne metody zdefiniowane w usłudze .proto.
Proste wywołanie RPC
Wywołanie prostego RPC GetFeature
jest prawie tak proste jak wywołanie metody lokalnej. Dodaj to w: 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(())
Jak widać, wywołujemy metodę na otrzymanym wcześniej obiekcie zastępczym. W parametrach metody tworzymy i wypełniamy obiekt bufora protokołu żądania (w naszym przypadku Point
). Jeśli wywołanie nie zwróci błędu, możemy odczytać informacje o odpowiedzi z serwera z pierwszej wartości zwracanej.
println!("Response = Name = \"{}\", Latitude = {}, Longitude = {}",
response.name(),
response.location().latitude(),
response.location().longitude());
Funkcja main()
klienta powinna wyglądać tak:
#[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. Wypróbuj
Najpierw, aby uruchomić klienta i serwer, dodajmy je jako binarne środowiska docelowe do naszego pakietu. Musimy odpowiednio zmodyfikować nasze Cargo.toml
i dodać te informacje:
[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"
[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"
Następnie wykonaj te polecenia w katalogu roboczym:
- Uruchom serwer w jednym terminalu:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server
- Uruchom klienta w innym terminalu:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client
Zobaczysz dane wyjściowe podobne do tych (sygnatury czasowe zostały pominięte dla przejrzystości):
*** SIMPLE RPC *** FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906
8. Co dalej?
- Dowiedz się, jak działa gRPC, z artykułów Wprowadzenie do gRPC i Podstawowe pojęcia.
- Zapoznaj się z samouczkiem dotyczącym podstaw.