Pierwsze kroki z gRPC-Rust

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 PointFeature to struktury danych wymieniane między klientem a serwerem podczas korzystania z metody GetFeature. Klient podaje współrzędne mapy jako Point w żądaniu GetFeature wysyłanym do serwera, a serwer odpowiada, przesyłając odpowiedni Feature, 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 PointFeature. 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

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 12 to unikalne identyfikatory poszczególnych pól w strukturze message.

Następnie określ Featuretyp 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 PointFeature.
  • 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

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:

  1. Określ port, którego mamy używać do nasłuchiwania żądań klientów
  2. Utwórz RouteGuideService z cechami wczytanymi przez wywołanie funkcji pomocniczej load().
  3. Utwórz instancję serwera gRPC za pomocą RouteGuideServer::new(), korzystając z utworzonej usługi.
  4. Zarejestruj implementację usługi na serwerze gRPC.
  5. 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:

  1. Uruchom serwer w jednym terminalu:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. 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?