Pierwsze kroki z gRPC-Rust – przesyłanie strumieniowe

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ą gRPC, aby uzyskać informacje o funkcjach na trasie klienta, utworzyć podsumowanie trasy klienta i wymieniać informacje o trasie, takie jak aktualizacje ruchu, z serwerem i innymi klientami.

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 podczas implementowania 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 za pomocą automatycznego generowania kodu utworzyć klienta i serwer oparte na gRPC na podstawie definicji Protocol Buffers.
  • Jak działa komunikacja strumieniowa klient-serwer za pomocą gRPC.

To ćwiczenie jest przeznaczone dla deweloperów Rust, którzy dopiero zaczynają korzystać z gRPC lub chcą sobie przypomnieć jego działanie, 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 tych 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 cdprzejdź do niego:

mkdir streaming-grpc-rust-getting-started && cd streaming-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-streaming/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. Definiowanie wiadomości i usług

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

  • Metody RPC o nazwach ListFeatures, RecordRoute i RouteChat, które są implementowane przez serwer i wywoływane przez klienta.
  • Typy wiadomości Point, Feature, Rectangle, RouteNote i RouteSummary, które są strukturami danych wymienianymi między klientem a serwerem podczas wywoływania metod powyżej.

Te metody RPC i ich typy wiadomości zostaną zdefiniowane w pliku proto/routeguide.proto w dostarczonym kodzie źródłowym.

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

Definiowanie typów wiadomości

Najpierw zdefiniujmy wiadomości, które będą używane przez nasze RPC. W pliku proto/routeguide.proto w kodzie źródłowym 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 na potrzeby 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;
}

Następnie wiadomość Rectangle, która reprezentuje prostokąt szerokości i długości geograficznej, przedstawiony jako 2 punkty po przekątnej „lo” i „hi”.

message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

Również wiadomość RouteNote, która reprezentuje wiadomość wysłaną w danym punkcie.

message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

Będziemy też potrzebować wiadomości RouteSummary. Ta wiadomość jest odbierana w odpowiedzi na RPC RecordRoute, które zostało opisane w następnej sekcji. Zawiera liczbę odebranych punktów, liczbę wykrytych funkcji i łączną odległość pokonaną jako sumę odległości między poszczególnymi punktami.

message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}

Definiowanie metod usługi

Najpierw zdefiniujmy usługę, a potem zdefiniujemy wiadomości. Aby zdefiniować usługę, w pliku .proto określ nazwaną usługę. Plik proto/routeguide.proto zawiera strukturę service o nazwie RouteGuide, która definiuje co najmniej jedną metodę udostępnianą przez usługę aplikacji.

Zdefiniuj metody RPC w definicji usługi, określając ich typy żądań i odpowiedzi. W tej sekcji ćwiczenia zdefiniujemy:

ListFeatures

Pobiera Feature dostępne w danym Rectangle. Wyniki są przesyłane strumieniowo, a nie zwracane od razu (np. w wiadomości odpowiedzi z polem powtarzanym), ponieważ prostokąt może obejmować duży obszar i zawierać ogromną liczbę funkcji.

Odpowiednim typem dla tego RPC jest strumieniowe RPC po stronie serwera: klient wysyła żądanie do serwera i otrzymuje strumień, z którego może odczytać sekwencję wiadomości. Klient odczytuje dane ze zwróconego strumienia, dopóki nie ma już żadnych wiadomości. Jak widać w naszym przykładzie, metodę strumieniową po stronie serwera określasz, umieszczając słowo kluczowe stream przed typem odpowiedzi.

rpc ListFeatures(Rectangle) returns (stream Feature) {}

RecordRoute

Akceptuje strumień Point na pokonywanej trasie i zwraca RouteSummary po zakończeniu pokonywania trasy.

W tym przypadku odpowiednie wydaje się strumieniowe RPC po stronie klienta: klient zapisuje sekwencję wiadomości i wysyła je do serwera, ponownie używając podanego strumienia. Gdy klient skończy zapisywać wiadomości, czeka, aż serwer je wszystkie odczyta i zwróci odpowiedź. Metodę strumieniową po stronie klienta określasz, umieszczając słowo kluczowe stream przed typem żądania.

rpc RecordRoute(stream Point) returns (RouteSummary) {}

RouteChat

Akceptuje strumień RouteNote wysyłanych podczas pokonywania trasy, a jednocześnie odbiera inne RouteNote (np. od innych użytkowników).

Jest to dokładnie przypadek użycia strumieniowania dwukierunkowego. W przypadku strumieniowego RPC dwukierunkowego obie strony wysyłają sekwencję wiadomości za pomocą strumienia odczytu i zapisu. Oba strumienie działają niezależnie, więc klienci i serwery mogą odczytywać i zapisywać dane w dowolnej kolejności.

Na przykład serwer może czekać na otrzymanie wszystkich wiadomości od klienta, zanim zapisze odpowiedzi, lub może na przemian odczytywać i zapisywać wiadomości albo stosować inną kombinację odczytów i zapisów.

Kolejność wiadomości w każdym strumieniu jest zachowywana. Ten typ metody określasz, umieszczając słowo kluczowe stream przed żądaniem i odpowiedzią.

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

4. Generowanie kodu klienta i serwera

Wygenerowany kod z pliku .proto znajduje się już w katalogu generated/, w tym wszystkie wprowadzone przez Ciebie zmiany. 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 dodaliśmy już grpc-protobuf-build jako zależność kompilacji.

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ć go tylko wtedy, gdy zostanie przekazana flaga funkcji, dzięki czemu będzie on ponownie generowany tylko wtedy, gdy tego chcesz. Nie musisz tego teraz robić, ponieważ kod został już wygenerowany.

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

Gdy uruchomisz polecenie cargo build, plik build.rs skompiluje definicje buforów 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. Implementowanie usługi

Najpierw zobaczmy, jak utworzyć serwer RouteGuide. Aby usługa RouteGuide działała prawidłowo, musisz wykonać 2 czynności:

  • Zaimplementować interfejs usługi wygenerowany na podstawie definicji usługi: wykonać rzeczywistą „pracę” naszej usługi.
  • Uruchomić serwer gRPC, aby nasłuchiwać żądań od klientów i przekazywać je do odpowiedniej implementacji metody.

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");
}

pub use grpc_pb::{
    route_guide_server::{RouteGuideServer, RouteGuide},
    Point, Feature, Rectangle, RouteNote, RouteSummary
};

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.

Implementowanie RouteGuide

Musimy zaimplementować wygenerowany interfejs RouteGuide. Oto jak wyglądałaby implementacja. Jest to już w szablonie.

#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
    async fn list_features(
        &self,
        request: Request<Rectangle>,
    ) -> Result<Response<ListFeaturesStream>, Status> {
        ...
    }

    async fn record_route(
        &self,
        request: Request<tonic::Streaming<Point>>,
    ) -> Result<Response<RouteSummary>, Status> {
        ...
    }

    async fn route_chat(
        &self,
        request: Request<tonic::Streaming<RouteNote>>,
    ) -> Result<Response<RouteChatStream>, Status> {
        ...
    }
}

Przyjrzyjmy się szczegółowo każdej implementacji RPC.

Strumieniowe RPC po stronie serwera: ListFeatures

Zacznijmy od ListFeatures. Jest to strumieniowe RPC po stronie serwera (klient wyśle 1 wiadomość, a serwer odpowie wieloma), więc musimy wysłać do klienta wiele Feature.

async fn list_features(
        &self,
        request: Request<Rectangle>,
    ) -> Result<Response<ListFeaturesStream>, Status> {
    println!("ListFeatures = {:?}", request);

    let (tx, rx) = mpsc::channel(4);
    let features = self.features.clone();

    tokio::spawn(async move {
        for feature in &features[..] {
            if in_range(&feature.location().to_owned(), request.get_ref()) {
                println!("  => send {feature:?}");
                tx.send(Ok(feature.clone())).await.unwrap();
            }
        }
        println!(" /// done sending");
    });

    let output_stream = ReceiverStream::new(rx);
    Ok(Response::new(Box::pin(output_stream)))
}

Jak widać, otrzymujemy obiekt żądania (Rectangle, w którym nasz klient chce znaleźć Features). Tym razem musimy zwrócić strumień wartości. Tworzymy kanał i uruchamiamy nowe zadanie asynchroniczne, w którym wykonujemy wyszukiwanie, wysyłając funkcje spełniające nasze ograniczenia do kanału. Strumień połowy kanału jest zwracany do elementu wywołującego, opakowany w tonic::Response.

Strumieniowe RPC po stronie klienta: RecordRoute

Teraz przyjrzyjmy się nieco bardziej skomplikowanej kwestii: metodzie strumieniowej po stronie klienta RecordRoute, w której otrzymujemy strumień Points od klienta i zwracamy pojedynczy RouteSummary z informacjami o jego podróży. Otrzymuje strumień jako dane wejściowe, którego serwer może używać zarówno do odczytywania, jak i zapisywania wiadomości. Może iterować po wiadomościach klienta za pomocą metody next() i zwracać pojedynczą odpowiedź.

async fn record_route(
        &self,
        request: Request<tonic::Streaming<Point>>,
    ) -> Result<Response<RouteSummary>, Status> {
    println!("RecordRoute");
    let mut stream = request.into_inner();
    let mut summary = RouteSummary::default();
    let mut last_point = None;
    let now = Instant::now();

    while let Some(point) = stream.next().await {
        let point = point?;
        println!("  ==> Point = {point:?}");

        // Increment the point count
        summary.set_point_count(summary.point_count() + 1);

        // Find features
        for feature in &self.features[..] {
            if feature.location().latitude() == point.latitude() {
                if feature.location().longitude() == point.longitude(){
                    summary.set_feature_count(summary.feature_count() + 1);
                }
            }
        }

        // Calculate the distance
        if let Some(ref last_point) = last_point {
            let new_dist = summary.distance() + calc_distance(last_point, &point);
            summary.set_distance(new_dist);
        }
        last_point = Some(point);
    }
    summary.set_elapsed_time(now.elapsed().as_secs() as i32);
    Ok(Response::new(summary))
}

W treści metody używamy metody next() strumienia, aby wielokrotnie odczytywać żądania klienta do obiektu żądania (w tym przypadku Point), dopóki nie będzie już żadnych wiadomości. Jeśli jest to None, strumień jest nadal prawidłowy i można kontynuować odczytywanie.

Strumieniowe RPC dwukierunkowe: RouteChat

Na koniec przyjrzyjmy się naszemu strumieniowemu RPC dwukierunkowemu RouteChat().

async fn route_chat(
        &self,
        request: Request<tonic::Streaming<RouteNote>>,
    ) -> Result<Response<RouteChatStream>, Status> {
    println!("RouteChat");

    let mut notes: HashMap<(i32, i32), Vec<RouteNote>> = HashMap::new();
    let mut stream = request.into_inner();

    let output = async_stream::try_stream! {
        while let Some(note) = stream.next().await {
            let note = note?;
            let location = note.location();
            let key = (location.latitude(), location.longitude());
            let location_notes = notes.entry(key).or_insert(vec![]);
            location_notes.push(note);
            for note in location_notes {
                yield note.clone();
            }
        }
    };
    Ok(Response::new(Box::pin(output)))
}

Tym razem otrzymujemy strumień, który, podobnie jak w naszym przykładzie strumieniowania po stronie klienta, może służyć do odczytywania i zapisywania wiadomości. Tym razem jednak zwracamy wartości za pomocą strumienia metody, gdy klient nadal zapisuje wiadomości w swoim strumieniu wiadomości. Składnia odczytu i zapisu jest tu bardzo podobna do naszej metody strumieniowania po stronie klienta, z wyjątkiem tego, że serwer zwraca RouteChatStream. Chociaż każda strona zawsze będzie otrzymywać wiadomości od drugiej strony w kolejności, w jakiej zostały zapisane, zarówno klient, jak i serwer mogą odczytywać i zapisywać dane w dowolnej kolejności – strumienie działają całkowicie niezależnie.

Strumień wyjściowy tworzymy za pomocą try_stream!, co oznacza, że strumień może zwracać błędy.

Uruchamianie serwera

Po zaimplementowaniu tej metody musimy też uruchomić serwer gRPC, aby klienci mogli korzystać z naszej usługi. Uzupełnij 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(())
}

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 załadowanymi funkcjami.
  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. Tworzenie klienta

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

Najpierw wprowadź wygenerowany kod do zakresu.

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

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

Wywoływanie metod usługi

Teraz zobaczmy, jak wywołujemy metody usługi. W gRPC-Rust strumieniowe RPC są asynchroniczne i nieblokujące, używają składni async/await Rust oraz strumieni Tokio.

Strumieniowe RPC po stronie serwera: PrintFeatures

W przypadku strumieniowych RPC po stronie serwera klient wysyła do serwera pojedynczą wiadomość żądania i otrzymuje strumień wiadomości odpowiedzi. W tym miejscu w pliku client.rs wywołujemy metodę strumieniową po stronie serwera list_features() (która odpowiada deklaracji rpc ListFeatures w naszym proto). Serwer z kolei wyśle strumień wiadomości Feature:

async fn print_features(client: &RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let rectangle = proto!(Rectangle {
        lo: proto!(Point {
            latitude: 400_000_000,
            longitude: -750_000_000,
        }),
        hi: proto!(Point {
            latitude: 420_000_000,
            longitude: -730_000_000,
        }),
    });

    let mut stream = client.list_features(rectangle).await;

    while let Some(feature) = stream.recv().await {
        println!(
            "FEATURE: Name = \"{}\", Lat = {}, Lon = {}",
            feature.name(),
            feature.location().latitude(),
            feature.location().longitude()
        );
    }
    let status = stream.status().await;
    assert!(status.is_ok(), "{:?}", status);
    Ok(())
}

Strumieniowe RPC po stronie klienta: RecordRoute

Gdy używamy strumieniowania po stronie klienta, klient otworzy strumień do serwera i wyśle sekwencję wiadomości. Po zakończeniu strumienia otrzyma pojedynczą wiadomość odpowiedzi.

Tutaj inicjujemy wywołanie za pomocą client.record_route().await, wysyłamy kolejno kilka wygenerowanych współrzędnych Point przez strumień za pomocą stream.send(point).await, a następnie zamykamy strumień za pomocą stream.close_and_recv().await, aby otrzymać pojedynczą wiadomość RouteSummary serwera.

async fn run_record_route(client: &RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let mut rng = rand::rng();
    let point_count: i32 = rng.random_range(2..100);

    let mut points = vec![];
    for _ in 0..=point_count {
        points.push(random_point(&mut rng));
    }

    println!("Traversing {} points", points.len());
    let mut stream = client.record_route().await;

    for point in &points {
        if stream.send(point).await.is_err() {
            break;
        }
    }

    match stream.close_and_recv().await {
        Ok(response) => {
            println!(
                "SUMMARY: Feature Count = {}, Distance = {}",
                response.feature_count(),
                response.distance()
            );
        }
        Err(e) => println!("something went wrong: {e:?}"),
    }
    Ok(())
}

Strumieniowe RPC dwukierunkowe: RouteChat

Na koniec przyjrzyjmy się naszemu strumieniowemu RPC dwukierunkowemu RouteChat(). W tym przypadku zarówno klient, jak i serwer będą przesyłać sekwencję wiadomości w obie strony. Uruchamiamy zadanie tokio, aby stale wysyłać wiadomości do serwera za pomocą tx.send(note).await.is_err(). W międzyczasie rx.recv().await nasłuchuje wiadomości odpowiedzi z serwera i wyświetla je w miarę ich nadejścia.

async fn run_route_chat(client: &RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let (mut tx, mut rx) = client.route_chat().await;

    let start = time::Instant::now();
    tokio::spawn(async move {
        let mut interval = time::interval(Duration::from_millis(50));
        for _ in 0..10 {
            let time = interval.tick().await;
            let elapsed = time.duration_since(start);
            let note = proto!(RouteNote {
                location: proto!(Point {
                    latitude: 409146138 + elapsed.as_millis() as i32,
                    longitude: -746188906,
                }),
                message: format!("at {elapsed:?}"),
            });
            if tx.send(note).await.is_err() {
                return;
            }
        }
        tx.close();
    });

    while let Some(note) = rx.recv().await {
        println!(
            "Note: Latitude = {}, Longitude = {}, Message = \"{}\"",
            note.location().latitude(),
            note.location().longitude(),
            note.message()
        );
    }
    let status = rx.status().await;
    assert!(status.is_ok(), "{:?}", status);
    Ok(())
}

Chociaż każda strona zawsze będzie otrzymywać wiadomości od drugiej strony w kolejności, w jakiej zostały zapisane, zarówno klient, jak i serwer mogą odczytywać i zapisywać dane w dowolnej kolejności – strumienie działają całkowicie niezależnie.

Tworzenie i przekazywanie klienta

Aby wywoływać metody usługi, musimy najpierw utworzyć kanał do komunikacji z serwerem. Tworzymy go, najpierw tworząc punkt końcowy, łącząc się z tym punktem końcowym i przekazując kanał utworzony po połączeniu z RouteGuideClient::new() w ten sposób:

// Create channel to connect to server
let channel = Channel::builder(
    "dns:///[::1]:10000",
    Arc::new(LocalChannelCredentials::new()),
)
.build();

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

Po utworzeniu tego klienta możemy wywoływać napisane przez nas metody, przekazując do nich klienta. Cały ten kod dodajemy do main(), który używa asynchronicznego środowiska wykonawczego Tokio. Oto pełny kod:

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

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

    println!("\n*** SERVER STREAMING ***");
    print_features(&client).await?;

    println!("\n*** CLIENT STREAMING ***");
    run_record_route(&client).await?;

    println!("\n*** BIDIRECTIONAL STREAMING ***");
    run_route_chat(&client).await?;

    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 w katalogu roboczym wykonaj te polecenia:

  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:

*** SERVER STREAMING ***
FEATURE: Name = "Patriots Path, Mendham, NJ 07945, USA", Lat = 407838351, Lon = -746143763
FEATURE: Name = "101 New Jersey 10, Whippany, NJ 07981, USA", Lat = 408122808, Lon = -743999179
FEATURE: Name = "U.S. 6, Shohola, PA 18458, USA", Lat = 413628156, Lon = -749015468
...
*** CLIENT STREAMING ***
Traversing 86 points
SUMMARY: Feature Count = 0, Distance = 803709356

*** BIDIRECTIONAL STREAMING ***
Note: Latitude = 409146138, Longitude = -746188906, Message = "at 112.45µs"
Note: Latitude = 409146139, Longitude = -746188906, Message = "at 1.00011245s"
Note: Latitude = 409146140, Longitude = -746188906, Message = "at 2.00011245s"

8. Co dalej?

9. Współtwórcy tego ćwiczenia

  • Cathy Zhao
  • Arvind Bright
  • Nathaniel Ford