Pierwsze kroki z gRPC-Rust

1. Wprowadzenie

W tym ćwiczeniu dowiesz się, jak za pomocą gRPC-Rust utworzyć klienta i serwer, które będą stanowić podstawę aplikacji do mapowania tras napisanej w Rust.

Po ukończeniu tego samouczka będziesz mieć klienta, który łączy się z serwerem zdalnym za pomocą oficjalnej implementacji protokołu gRPC w Rust, aby pobrać nazwę lub adres pocztowy lokalizacji o określonych współrzędnych na mapie. W pełni funkcjonalna aplikacja może używać tego projektu klient-serwer do wyliczania lub podsumowywania ważnych miejsc na trasie.

Usługa jest zdefiniowana w pliku Protocol Buffers, który będzie używany do generowania powtarzalnego kodu dla klienta i serwera, aby mogły się ze sobą komunikować. Dzięki temu zaoszczędzisz czas i wysiłek na implementację tej funkcji.

Wygenerowany kod zajmuje się nie tylko złożonością komunikacji między serwerem a klientem, ale także serializacją i deserializacją danych.

Czego się nauczysz

  • Jak używać Protocol Buffers do definiowania interfejsu API usługi.
  • Jak utworzyć klienta opartego na gRPC na podstawie definicji Protocol Buffer za pomocą automatycznego generowania kodu.
  • Jak działa komunikacja klient-serwer za pomocą gRPC.

Te ćwiczenia są przeznaczone dla deweloperów Rust, którzy dopiero zaczynają korzystać z gRPC lub chcą sobie przypomnieć, jak działa gRPC, oraz dla wszystkich innych osób zainteresowanych tworzeniem systemów rozproszonych. Nie jest wymagane wcześniejsze doświadczenie z gRPC.

2. Zanim zaczniesz

Wymagania wstępne

Upewnij się, że masz zainstalowane te elementy:

  • GCC. Postępuj zgodnie z instrukcjami podanymi tutaj.
  • Git: instrukcje instalacji znajdziesz tutaj.
  • Rust w wersji 1.88.0. Postępuj zgodnie z instrukcjami instalacji podanymi 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 podanych niżej instrukcji dowiesz się, jak dokończyć aplikację, w tym jak używać wtyczek kompilatora buforów protokołu do generowania kodu szablonowego gRPC.

Najpierw utwórz katalog roboczy ćwiczenia i przejdź do niego:

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

Pobierz i rozpakuj ćwiczenie:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/2026.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-2026/codelabs/grpc-rust-getting-started/start_here

Możesz też pobrać plik ZIP zawierający tylko katalog z ćwiczeniem i rozpakować go ręcznie.

Jeśli nie chcesz wpisywać implementacji, gotowy kod źródłowy jest dostępny na GitHubie.

3. Zdefiniuj usługę

Pierwszym krokiem jest zdefiniowanie usługi gRPC aplikacji, jej metody RPC oraz typów wiadomości żądania i odpowiedzi za pomocą Protocol Buffers. Twoja usługa będzie udostępniać:

  • Metodę RPC o nazwie GetFeature, którą implementuje serwer i wywołuje klient.
  • Typy wiadomości Point i Feature, które są strukturami danych wymienianymi między klientem a serwerem podczas korzystania z metody GetFeature. Klient podaje współrzędne mapy jako Point w żądaniu GetFeature do serwera, a serwer odpowiada odpowiednią Feature, która opisuje wszystko, co znajduje się w tych współrzędnych.

Ta metoda RPC i jej typy wiadomości zostaną zdefiniowane w pliku proto/routeguide.proto dostarczonego kodu źródłowego.

Protocol Buffers są powszechnie znane jako protobufs. Więcej informacji o terminologii gRPC znajdziesz w artykule Podstawowe koncepcje, architektura i cykl życia gRPC.

Metoda usługi

Najpierw zdefiniujmy metody usługi, a potem typy wiadomości Point i Feature. Plik proto/routeguide.proto zawiera strukturę service o nazwie RouteGuide, która definiuje co najmniej jedną metodę udostępnianą przez usługę aplikacji.

Dodaj metodę rpc o nazwie GetFeature w definicji RouteGuide. Jak już wspomnieliśmy, ta metoda będzie wyszukiwać nazwę lub adres lokalizacji na podstawie podanego zestawu współrzędnych, więc niech GetFeature zwraca Feature dla danego Point:

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 metoda RPC, w której klient wysyła żądanie do serwera i czeka na odpowiedź, tak jak w przypadku lokalnego wywołania funkcji.

Rodzaje wiadomości

W pliku proto/routeguide.proto kodu źródłowego najpierw zdefiniuj typ wiadomości Point. Point reprezentuje parę współrzędnych szerokości i długości geograficznej na mapie. W tym ćwiczeniu użyj liczb całkowitych jako współrzędnych:

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

Liczby 1 i 2 to unikalne numery identyfikacyjne każdego pola w strukturze message.

Następnie zdefiniuj typ wiadomości Feature. Feature używa pola string do określania nazwy lub adresu pocztowego 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. Wygeneruj kod klienta i serwera

Wygenerowany kod z pliku .proto znajduje się już w katalogu generated/, w tym wszystkie dodane przez Ciebie elementy. Chcemy jednak poświęcić chwilę na wyjaśnienie, jak działa generowanie kodu.

Nasz plik .proto opisuje wszystkie struktury i funkcje, których używa klient lub serwer. Do automatycznego generowania tego kodu używamy skryptu kompilacji Cargo (build.rs) wraz z pakietem grpc-protobuf-build.

W pliku Cargo.toml dodajemy grpc-protobuf-build w sekcji [build-dependencies].

W pliku build.rs konfigurujemy grpc_protobuf_build::CodeGen, aby skompilować plik proto/routeguide.proto do katalogu generated/. Kluczowe wiersze są tutaj:

grpc_protobuf_build::CodeGen::new()
    .include("proto")
    .input("routeguide.proto")
    .output_dir("generated")
    .compile()
    .unwrap();

Wywołuje to generowanie kodu pakietu grpc_protobuf_build, przekazując mu plik routeguide.proto. Zawinęliśmy to w kod, aby uruchamiał się tylko wtedy, gdy zostanie przekazana flaga funkcji, dzięki czemu będzie się on regenerować tylko wtedy, gdy tego chcesz. Nie musisz teraz tego robić, ponieważ kod został już wygenerowany.

cargo build --bin routeguide-server --features regenerate_proto

Gdy uruchomisz polecenie cargo build, skrypt build.rs skompiluje definicje bufora protokołu do katalogu generate/, w tym:

  • Definicje struktur dla typów wiadomości Point i Feature.
  • Cechę usługi Tonic, którą będziemy musieli zaimplementować na serwerze: route_guide_server::RouteGuide.
  • Typ klienta gRPC-Rust, którego będziemy używać do wywoływania serwera: route_guide_client::RouteGuideClient<T>.

Więcej informacji znajdziesz w przewodniku protoc-gen-rust-grpc.

Następnie zaimplementujemy metody usługi na serwerze.

5. Zaimplementuj usługę

W pliku src/server/server.rs możemy wprowadzić wygenerowany kod do zakresu za pomocą makra include_generated_proto! gRPC i zaimportować cechę RouteGuide oraz Point.

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

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

Możemy zacząć od zdefiniowania struktury reprezentującej naszą usługę. Na razie możemy to zrobić w pliku src/server/server.rs:

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

Teraz musimy zaimplementować cechę route_guide_server::RouteGuide z wygenerowanego kodu.

Prosta metoda RPC typu unary

RouteGuideService implementuje wszystkie metody naszej usł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 pliku 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()))
    }
}

Po zaimplementowaniu tej metody musimy też uruchomić serwer gRPC, aby klienci mogli korzystać z naszej usługi. Zastąp main() tym kodem.

#[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 chcemy używać do nasłuchiwania żądań klientów.
  2. Utwórz RouteGuideService z funkcjami wczytanymi przez wywołanie funkcji pomocniczej load().
  3. Utwórz instancję serwera gRPC za pomocą RouteGuideServer::new() przy użyciu utworzonej przez nas usługi.
  4. Zarejestruj implementację usługi na serwerze gRPC.
  5. Wywołaj serve() na serwerze z informacjami o porcie, aby zablokować oczekiwanie do momentu zakończenia procesu.

6. Utwórz klienta

W tej sekcji przyjrzymy się tworzeniu klienta Rust dla naszej usługi RouteGuide w pliku src/client/client.rs.

Podobnie jak w przypadku pliku src/server/server.rs, możemy wprowadzić wygenerowany kod do zakresu za pomocą makra include_generated_proto! gRPC i zaimportować typ RouteGuideClient.

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

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

Wywołuj metody usługi

W gRPC-Rust metody RPC są asynchroniczne i nieblokujące, a do oczekiwania na odpowiedzi z serwera używają składni async/await Rust.

Aby wywołać metody usługi, najpierw tworzymy Channel za pomocą Channel::builder(), określając adres serwera (dns:///[::1]:10000) i dane logowania (LocalChannelCredentials). Następnie przekazujemy kanał do RouteGuideClient::new(), aby utworzyć instancję klienta:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a new gRPC channel
    let channel = Channel::builder(
        "dns:///[::1]:10000",
        Arc::new(LocalChannelCredentials::new()),
    )
    .build();

    // Create a new client
    let client = RouteGuideClient::new(channel);



}

W tej funkcji RouteGuideClient::new() wiąże kanał ogólny z wygenerowanym stubem klienta, który implementuje metody zdefiniowane w naszej definicji usługi .proto.

Prosta metoda RPC

Wywołanie prostej metody RPC GetFeature jest 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(point)
    .await
    .expect("RPC error");

W gRPC-Rust przekazujemy wiadomość protobuf Point bezpośrednio do client.get_feature(point). Oczekiwanie na zwróconą przyszłość daje bezpośrednio odpowiedź Feature, bez konieczności wykonywania dalszych wywołań metody.

Następnie wydrukuj pola z odpowiedzi:

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

W sumie funkcja main() klienta powinna wyglądać tak:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Create a new gRPC channel
    let channel = Channel::builder(
        "dns:///[::1]:10000",
        Arc::new(LocalChannelCredentials::new()),
    )
    .build();

    // 2. Instantiate the generated RouteGuideClient
    let client = RouteGuideClient::new(channel);

    println!("*** SIMPLE RPC ***");
    let point = proto!(Point {
        latitude: 409_146_138,
        longitude: -746_188_906,
    });
    let response = client
        .get_feature(point)
        .await
        .expect("RPC error");

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

7. Wypróbuj

Aby uruchomić klienta i serwer, najpierw sprawdź, czy w pliku Cargo.toml są zdefiniowane oba cele binarne:

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

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

Następnie wykonaj te polecenia z katalogu roboczego:

  1. Uruchom serwer w jednym terminalu:
cargo run --bin routeguide-server
  1. Uruchom klienta w innym terminalu:
cargo run --bin routeguide-client

Zobaczysz dane wyjściowe podobne do tych (sygnatury czasowe zostały pominięte w celu uniknięcia wątpliwości):

*** SIMPLE RPC ***
Response = Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Latitude = 409146138, Longitude = -746188906

8. Co dalej?

9. Współautorzy tego ćwiczenia

  • Cathy Zhao
  • Lucio Franco
  • Arvind Bright
  • Nathaniel Ford