Erste Schritte mit gRPC-Rust – Streaming

1. Einführung

In diesem Codelab verwenden Sie gRPC-Rust, um einen Client und einen Server zu erstellen, die die Grundlage einer in Rust geschriebenen Routenplanungsanwendung bilden.

Am Ende des Tutorials haben Sie einen Client, der über gRPC eine Verbindung zu einem Remote-Server herstellt, um Informationen zu Funktionen auf der Route eines Clients abzurufen, eine Zusammenfassung der Route eines Clients zu erstellen und Routeninformationen wie Verkehrsaktualisierungen mit dem Server und anderen Clients auszutauschen.

Der Dienst wird in einer Protocol Buffers-Datei definiert, die zum Generieren von Boilerplate-Code für den Client und den Server verwendet wird, damit sie miteinander kommunizieren können. So sparen Sie Zeit und Aufwand bei der Implementierung dieser Funktion.

Dieser generierte Code kümmert sich nicht nur um die Komplexität der Kommunikation zwischen Server und Client, sondern auch um die Serialisierung und Deserialisierung von Daten.

Lerninhalte

  • Wie Sie Protocol Buffers zum Definieren einer Dienst-API verwenden.
  • Hier erfahren Sie, wie Sie einen gRPC-basierten Client und Server aus einer Protocol Buffers-Definition mithilfe der automatischen Codegenerierung erstellen.
  • Sie haben ein grundlegendes Verständnis der Client-Server-Streaming-Kommunikation mit gRPC.

Dieses Codelab richtet sich an Rust-Entwickler, die neu in gRPC sind oder ihr Wissen zu gRPC auffrischen möchten, sowie an alle anderen, die sich für die Entwicklung verteilter Systeme interessieren. Es sind keine Vorkenntnisse in gRPC erforderlich.

2. Hinweis

Vorbereitung

Achten Sie darauf, dass Folgendes installiert ist:

Code abrufen

Damit Sie nicht ganz von vorn anfangen müssen, enthält dieses Codelab ein Gerüst des Quellcodes der Anwendung, das Sie vervollständigen können. In den folgenden Schritten erfahren Sie, wie Sie die Anwendung fertigstellen, einschließlich der Verwendung der Protocol Buffer-Compiler-Plug-ins zum Generieren des Boilerplate-gRPC-Codes.

Erstellen Sie zuerst das Arbeitsverzeichnis für das Codelab und wechseln Sie zu cd:

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

Laden Sie das Codelab herunter und extrahieren Sie es:

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-streaming/start_here

Alternativ können Sie die ZIP-Datei herunterladen, die nur das Codelab-Verzeichnis enthält, und sie manuell entpacken.

Der vollständige Quellcode ist auf GitHub verfügbar, wenn Sie die Eingabe einer Implementierung überspringen möchten.

3. Nachrichten und Dienste definieren

Als Erstes definieren Sie den gRPC-Dienst der Anwendung, die RPC-Methoden sowie die Anfrage- und Antwortnachrichtentypen mit Protokollpuffern. Ihr Dienst bietet Folgendes:

  • RPC-Methoden mit den Namen ListFeatures, RecordRoute und RouteChat, die der Server implementiert und der Client aufruft.
  • Die Nachrichtentypen Point, Feature, Rectangle, RouteNote und RouteSummary, die Datenstrukturen sind, die beim Aufrufen der oben genannten Methoden zwischen Client und Server ausgetauscht werden.

Diese RPC-Methoden und ihre Nachrichtentypen werden alle in der Datei proto/routeguide.proto des bereitgestellten Quellcodes definiert.

Protocol Buffers werden allgemein als Protobufs bezeichnet. Weitere Informationen zur gRPC-Terminologie finden Sie unter Core concepts, architecture, and lifecycle.

Nachrichtentypen definieren

Zuerst definieren wir die Nachrichten, die von unseren RPCs verwendet werden. Definieren Sie zuerst den Nachrichtentyp Point in der Datei routeguide/route_guide.proto des Quellcodes. Ein Point stellt ein Paar aus Breiten- und Längengrad auf einer Karte dar. Verwenden Sie für dieses Codelab Ganzzahlen für die Koordinaten:

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

Die Zahlen 1 und 2 sind eindeutige ID-Nummern für die einzelnen Felder in der message-Struktur.

Als Nächstes definieren Sie den Nachrichtentyp Feature. Bei einem Feature wird ein string-Feld für den Namen oder die Postanschrift von etwas an einem Standort verwendet, der durch ein Point angegeben wird:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Als Nächstes folgt eine Rectangle-Nachricht, die ein Rechteck aus Breiten- und Längengrad darstellt, das durch zwei diagonal gegenüberliegende Punkte („lo“ und „hi“) dargestellt wird.

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

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

Außerdem eine RouteNote-Nachricht, die eine Nachricht darstellt, die an einem bestimmten Punkt gesendet wurde.

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

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

Außerdem benötigen wir eine RouteSummary-Nachricht. Diese Nachricht wird als Antwort auf einen RecordRoute-RPC empfangen, der im nächsten Abschnitt erläutert wird. Sie enthält die Anzahl der einzelnen empfangenen Punkte, die Anzahl der erkannten Features und die zurückgelegte Gesamtstrecke als kumulative Summe der Entfernung zwischen den einzelnen Punkten.

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

Dienstmethoden definieren

Definieren wir zuerst unseren Dienst und dann unsere Nachrichten. Wenn Sie einen Dienst definieren möchten, geben Sie einen benannten Dienst in der Datei .proto an. Die Datei proto/routeguide.proto hat eine service-Struktur mit dem Namen RouteGuide, die eine oder mehrere Methoden definiert, die vom Dienst der Anwendung bereitgestellt werden.

Definieren Sie RPC-Methoden in Ihrer Dienstdefinition und geben Sie die zugehörigen Anfrage- und Antworttypen an. In diesem Abschnitt des Codelabs definieren wir Folgendes:

ListFeatures

Ruft die im angegebenen Rectangle verfügbaren Feature ab. Die Ergebnisse werden gestreamt und nicht auf einmal zurückgegeben (z.B. in einer Antwortnachricht mit einem wiederholten Feld), da das Rechteck einen großen Bereich abdecken und eine große Anzahl von Features enthalten kann.

Ein geeigneter Typ für diesen RPC ist ein serverseitiger Streaming-RPC: Der Client sendet eine Anfrage an den Server und erhält einen Stream, um eine Reihe von Nachrichten zurückzulesen. Der Client liest den zurückgegebenen Stream, bis keine Nachrichten mehr vorhanden sind. Wie Sie in unserem Beispiel sehen, geben Sie eine serverseitige Streamingmethode an, indem Sie das Keyword stream vor den Antworttyp setzen.

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

RecordRoute

Akzeptiert einen Stream von Points auf einer befahrenen Route und gibt ein RouteSummary zurück, wenn die Fahrt abgeschlossen ist.

Eine clientseitige Streaming-RPC scheint in diesem Fall angemessen zu sein: Der Client schreibt eine Reihe von Nachrichten und sendet sie an den Server, wobei er wieder einen bereitgestellten Stream verwendet. Nachdem der Client mit dem Schreiben der Nachrichten fertig ist, wartet er darauf, dass der Server sie alle liest und seine Antwort zurückgibt. Sie geben eine clientseitige Streamingmethode an, indem Sie das Keyword stream vor den Anfragetyp setzen.

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

RouteChat

Akzeptiert einen Stream von RouteNote, die während der Fahrt auf einer Route gesendet werden, und empfängt gleichzeitig andere RouteNote (z.B. von anderen Nutzern).

Genau für solche Anwendungsfälle ist bidirektionales Streaming gedacht. Bei einer bidirektionalen Streaming-RPC senden beide Seiten eine Reihe von Nachrichten über einen Lese-/Schreib-Stream. Die beiden Streams funktionieren unabhängig voneinander. Clients und Server können also in beliebiger Reihenfolge lesen und schreiben.

Der Server kann beispielsweise warten, bis er alle Clientnachrichten empfangen hat, bevor er seine Antworten schreibt. Alternativ kann er auch eine Nachricht lesen und dann eine Nachricht schreiben oder eine andere Kombination aus Lese- und Schreibvorgängen ausführen.

Die Reihenfolge der Nachrichten in jedem Stream wird beibehalten. Sie geben diesen Methodentyp an, indem Sie das Keyword stream sowohl vor die Anfrage als auch vor die Antwort setzen.

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

4. Client- und Servercode generieren

Wir haben Ihnen den generierten Code aus der Datei .proto im generierten Verzeichnis bereits zur Verfügung gestellt.

Wenn Sie erfahren möchten, wie Sie selbst Code aus der Datei .proto generieren oder Änderungen an der Datei .proto vornehmen und testen können, folgen Sie dieser Anleitung.

Der generierte Code enthält:

  • Struct-Definitionen für die Nachrichtentypen Point, Feature, Rectangle, RouteNote und RouteSummary.
  • Ein Dienst-Trait, das wir implementieren müssen: route_guide_server::RouteGuide.
  • Ein Clienttyp, den wir zum Aufrufen des Servers verwenden: route_guide_client::RouteGuideClient<T>.

Als Nächstes implementieren wir die Methoden auf der Serverseite, damit der Server auf eine Anfrage des Clients antworten kann.

5. Dienst implementieren

Sehen wir uns zuerst an, wie wir einen RouteGuide-Server erstellen. Damit unser RouteGuide-Dienst seine Aufgabe erfüllen kann, sind zwei Dinge erforderlich:

  • Implementieren der Dienstschnittstelle, die aus unserer Dienstdefinition generiert wurde: Hier wird die eigentliche „Arbeit“ unseres Dienstes erledigt.
  • Ausführen eines gRPC-Servers, der auf Anfragen von Clients wartet und sie an die richtige Methodenimplementierung weiterleitet.

In src/server/server.rs können wir den generierten Code über das gRPC-Makro include_generated_proto! in den Bereich aufnehmen und das Merkmal RouteGuide und Point importieren.

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

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

Wir können damit beginnen, eine Struktur zu definieren, die unseren Dienst repräsentiert. Das ist derzeit in src/server/server.rs möglich:

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

Als Nächstes müssen wir das route_guide_server::RouteGuide-Trait aus unserem generierten Code implementieren.

RouteGuide implementieren

Wir müssen die generierte RouteGuide-Schnittstelle implementieren. So würde die Implementierung aussehen. Das ist bereits in der Vorlage enthalten.

#[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> {
        ...
    }
}

Sehen wir uns die einzelnen RPC-Implementierungen genauer an.

Serverseitiger Streaming-RPC

Beginnen wir mit ListFeatures. Dies ist ein Server-Streaming-RPC, daher müssen wir mehrere Feature an unseren Client zurücksenden.

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

Wie Sie sehen, erhalten wir ein Anfrageobjekt (das Rectangle, in dem unser Kunde Features finden möchte). Dieses Mal müssen wir einen Stream von Werten zurückgeben. Wir erstellen einen Kanal und starten eine neue asynchrone Aufgabe, in der wir eine Suche durchführen und die Features, die unseren Einschränkungen entsprechen, in den Kanal senden. Die Stream-Hälfte des Kanals wird an den Aufrufer zurückgegeben, umschlossen von einem tonic::Response.

Clientseitiger Streaming-RPC

Sehen wir uns nun etwas Komplexeres an: die clientseitige Streamingmethode RecordRoute, bei der wir einen Stream von Points vom Client erhalten und ein einzelnes RouteSummary mit Informationen zur Fahrt zurückgeben. Es erhält einen Stream als Eingabe, den der Server sowohl zum Lesen als auch zum Schreiben von Nachrichten verwenden kann. Es kann Clientnachrichten mit der Methode next() durchlaufen und seine einzelne Antwort zurückgeben.

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

Im Methodentext verwenden wir die next()-Methode des Streams, um die Anfragen unseres Clients wiederholt in ein Anfrageobjekt (in diesem Fall ein Point) einzulesen, bis keine Nachrichten mehr vorhanden sind. Wenn dieser Wert „None“ ist, ist der Stream noch in Ordnung und es kann weiter gelesen werden.

Bidirektionale Streaming-RPC

Sehen wir uns zum Schluss noch unseren bidirektionalen Streaming-RPC RouteChat() an.

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

Dieses Mal erhalten wir einen Stream, der wie in unserem Beispiel für clientseitiges Streaming zum Lesen und Schreiben von Nachrichten verwendet werden kann. Dieses Mal geben wir jedoch Werte über den Stream unserer Methode zurück, während der Client weiterhin Nachrichten in seinen Nachrichtenstream schreibt. Die Syntax zum Lesen und Schreiben ist hier sehr ähnlich wie bei unserer Client-Streaming-Methode, nur dass der Server ein RouteChatStream zurückgibt. Obwohl jede Seite die Nachrichten der anderen Seite immer in der Reihenfolge erhält, in der sie geschrieben wurden, können sowohl der Client als auch der Server in beliebiger Reihenfolge lesen und schreiben. Die Streams funktionieren völlig unabhängig voneinander.

Wir erstellen den output-Stream mit try_stream!, was darauf hindeutet, dass der Stream Fehler zurückgeben könnte.

Server starten

Nachdem wir diese Methode implementiert haben, müssen wir auch einen gRPC-Server starten, damit Clients unseren Dienst nutzen können. Füllen Sie main() aus.

#[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(())
}

So läuft der Vorgang in main() ab:

  1. Geben Sie den Port an, der für das Abhören von Clientanfragen verwendet werden soll.
  2. RouteGuideService mit geladenen Funktionen erstellen
  3. Erstellen Sie eine Instanz des gRPC-Servers mit RouteGuideServer::new() und dem von uns erstellten Dienst.
  4. Registrieren Sie unsere Dienstimplementierung beim gRPC-Server.
  5. Rufen Sie serve() auf dem Server mit unseren Portdetails auf, um eine blockierende Wartezeit zu erzwingen, bis der Prozess beendet wird.

6. Client erstellen

In diesem Abschnitt sehen wir uns an, wie wir einen Rust-Client für unseren RouteGuide-Dienst in src/client/client.rs erstellen.

Bringen Sie zuerst den generierten Code in den Bereich.

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

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

Dienstmethoden aufrufen

Sehen wir uns nun an, wie wir unsere Dienstmethoden aufrufen. In gRPC-Rust werden RPCs im blockierenden/synchronen Modus ausgeführt. Das bedeutet, dass der RPC-Aufruf auf die Antwort des Servers wartet und entweder eine Antwort oder einen Fehler zurückgibt.

Serverseitiger Streaming-RPC

Hier rufen wir die serverseitige Streaming-Methode ListFeatures auf, die einen Stream geografischer Feature-Objekte zurückgibt.

async fn print_features(client: &mut 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(Request::new(rectangle))
        .await?
        .into_inner();

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

Wir übergeben der Methode eine Anfrage und erhalten eine Instanz von ListFeaturesStream zurück. Der Client kann den ListFeaturesStream-Stream verwenden, um die Antworten des Servers zu lesen. Wir verwenden die Methode message() von ListFeaturesStream, um die Antworten des Servers wiederholt in ein Antwortprotokollpufferobjekt (in diesem Fall ein Feature) einzulesen, bis keine Nachrichten mehr vorhanden sind.

Clientseitiger Streaming-RPC

Bei record_route wird ein Vektor von Punkten in einen Stream umgewandelt. Wir übergeben diesen Stream dann als Anfrage an record_route() und erhalten eine einzelne RouteSummary-Antwort, nachdem der Stream vollständig vom Server verarbeitet wurde.

async fn run_record_route(client: &mut 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 request = Request::new(tokio_stream::iter(points));

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

    Ok(())
}

Bidirektionale Streaming-RPC

Sehen wir uns zum Schluss noch unseren bidirektionalen Streaming-RPC RouteChat() an. Wir übergeben der Methode eine Streamanfrage, in die wir schreiben, und erhalten einen Stream zurück, aus dem wir Nachrichten lesen können. Dieses Mal geben wir Werte über den Stream unserer Methode zurück, während der Server weiterhin Nachrichten in seinen Nachrichtenstream schreibt.

async fn run_route_chat(client: &mut RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let start = time::Instant::now();
    let outbound = async_stream::stream! {
        let mut interval = time::interval(Duration::from_secs(1));
        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_secs() as i32,
                    longitude: -746188906,
                }),
                message: format!("at {elapsed:?}"),
            });
            yield note;
        }
    };
    let response = client.route_chat(Request::new(outbound)).await?;
    let mut inbound = response.into_inner();
    while let Some(note) = inbound.message().await? {
        println!("Note: Latitude = {}, Longitude = {}, Message = \"{}\"",
            note.location().latitude(),
            note.location().longitude(),
            note.message());
        }
    Ok(())
}

Obwohl jede Seite die Nachrichten der anderen Seite immer in der Reihenfolge erhält, in der sie geschrieben wurden, können sowohl der Client als auch der Server in beliebiger Reihenfolge lesen und schreiben. Die Streams funktionieren völlig unabhängig voneinander.

Hilfsmethoden aufrufen

Um Dienstmethoden aufzurufen, müssen wir zuerst einen Channel erstellen, um mit dem Server zu kommunizieren. Dazu erstellen wir zuerst einen Endpunkt, stellen eine Verbindung zu diesem Endpunkt her und übergeben den beim Herstellen der Verbindung erstellten Channel an RouteGuideClient::new():

#[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(())
}

Führen Sie in main() die gerade erstellten Methoden aus.

#[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!("\n*** SERVER STREAMING ***");
    print_features(&mut client).await?;

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

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

    Ok(())
}

7. Jetzt ausprobieren

Damit wir unseren Client und Server ausführen können, fügen wir sie zuerst als binäre Ziele zu unserem Crate hinzu. Wir müssen unsere Cargo.toml entsprechend bearbeiten:

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

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

Wie bei jedem Projekt müssen wir auch an die Abhängigkeiten denken, die für die Ausführung unseres Codes erforderlich sind. Bei Rust-Projekten befinden sich die Abhängigkeiten in Cargo.toml. Wir haben die erforderlichen Abhängigkeiten bereits in der Datei Cargo.toml aufgeführt.

Führen Sie dann die folgenden Befehle in unseren Arbeitsverzeichnissen aus:

  1. Führen Sie den Server in einem Terminal aus:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-server 
  1. Führen Sie den Client über ein anderes Terminal aus:
RUSTFLAGS="-Awarnings" cargo run --bin routeguide-client

Die Ausgabe sieht so aus:

*** 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. Nächste Schritte