Erste Schritte mit gRPC-Rust

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 den Namen oder die Postanschrift des Objekts an bestimmten Koordinaten auf einer Karte abzurufen. Eine vollwertige Anwendung könnte dieses Client-Server-Design verwenden, um POIs entlang einer Route aufzulisten oder zusammenzufassen.

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.
  • Kenntnisse der Client-Server-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 in dieses Verzeichnis:

mkdir grpc-rust-getting-started && cd 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-getting-started/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. Dienst definieren

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

  • Eine RPC-Methode namens GetFeature, die vom Server implementiert und vom Client aufgerufen wird.
  • Die Nachrichtentypen Point und Feature sind Datenstrukturen, die beim Verwenden der Methode GetFeature zwischen dem Client und dem Server ausgetauscht werden. Der Client stellt Kartenkoordinaten als Point in seiner GetFeature-Anfrage an den Server bereit und der Server antwortet mit einem entsprechenden Feature, das beschreibt, was sich an diesen Koordinaten befindet.

Diese RPC-Methode und ihre Nachrichtentypen werden alle in der Datei proto/route_guide.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.

Servicemethode

Definieren wir zuerst unsere Dienstmethoden und dann unsere Nachrichtentypen Point und Feature. 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.

Fügen Sie die Methode rpc GetFeature in die Definition von RouteGuide ein. Wie bereits erwähnt, wird mit dieser Methode der Name oder die Adresse eines Orts anhand einer bestimmten Menge von Koordinaten ermittelt. Lassen Sie also von GetFeature ein Feature für ein bestimmtes Point zurückgeben:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Dies ist eine unäre RPC-Methode: ein einfacher RPC, bei dem der Client eine Anfrage an den Server sendet und auf eine Antwort wartet, genau wie bei einem lokalen Funktionsaufruf.

Mitteilungstypen

Definieren Sie zuerst den Nachrichtentyp Point in der Datei proto/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;
}

4. Client- und Servercode generieren

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

Wie bei jedem Projekt müssen wir an die Abhängigkeiten denken, die für unseren Code 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.

Wenn Sie wissen möchten, wie Sie selbst Code aus der Datei .proto generieren, folgen Sie dieser Anleitung.

Der generierte Code enthält:

  • Strukturdefinitionen für die Nachrichtentypen Point und Feature.
  • 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 Methode GetFeature auf der Serverseite, damit der Server auf eine Anfrage des Clients antworten kann.

5. Dienst implementieren

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

Wir beginnen mit der Definition einer Struktur zur Darstellung unseres Dienstes. Das können wir vorerst in src/server/server.rs tun:

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

Unäre RPC

Die RouteGuideService implementiert alle unsere Dienstmethoden. Die Funktion get_feature auf der Serverseite ist der Ort, an dem die Hauptarbeit erledigt wird: Sie empfängt eine Point-Nachricht vom Client und gibt in einer Feature-Nachricht die entsprechenden Standortinformationen aus einer Liste bekannter Orte zurück. So wird die Funktion in src/server/server.rs implementiert:

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

Füllen Sie in der Methode ein Feature-Objekt mit den entsprechenden Informationen für das angegebene Point aus und geben Sie es dann zurück.

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

#[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. Erstellen Sie ein RouteGuideService mit Funktionen, die durch Aufrufen der Hilfsfunktion load() geladen werden.
  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.

Wie in src/server/server.rs können wir den generierten Code mit dem include_generated_code!-Makro von gRPC in den Bereich einbeziehen und den Typ RouteGuideClient importieren.

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

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

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.

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

Wenn wir in dieser Funktion den Client erstellen, umschließen wir den oben erstellten generischen Channel mit dem generierten Code-Stub, der die spezifischen Methoden implementiert, die im .proto-Dienst definiert sind.

Einfache RPC

Der Aufruf des einfachen RPC GetFeature ist fast so einfach wie der Aufruf einer lokalen Methode. Fügen Sie dies in main() hinzu.

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

Wie Sie sehen, rufen wir die Methode für den Stub auf, den wir zuvor erhalten haben. In unseren Methodenparametern erstellen und füllen wir ein Protokollpufferobjekt für die Anfrage (in unserem Fall Point). Wenn der Aufruf keinen Fehler zurückgibt, können wir die Antwortinformationen vom Server aus dem ersten Rückgabewert lesen.

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

Insgesamt sollte die main()-Funktion des Clients so aussehen:

#[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. 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 und Folgendes hinzufügen:

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

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

Führen Sie dann die folgenden Befehle in unserem Arbeitsverzeichnis 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 (Zeitstempel wurden der Übersichtlichkeit halber weggelassen):

*** SIMPLE RPC ***

FEATURE: Name = "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", Lat = 409146138, Lon = -746188906

8. Nächste Schritte