Erste Schritte mit gRPC-Java

1. Einführung

In diesem Codelab verwenden Sie gRPC-Java, um einen Client und einen Server zu erstellen, die die Grundlage einer in Java 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.

Die API des Servers ist 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 Java-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

  • JDK-Version 8 oder höher

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-java-getting-started && cd grpc-java-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-java-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 src/main/proto/routeguide/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.

Da wir in diesem Beispiel Java-Code generieren, haben wir eine java_package-Dateioption und einen Namen für die Java-Klasse in unserer .proto angegeben:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

Mitteilungstypen

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

Servicemethode

Die Datei route_guide.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.

4. Client- und Servercode generieren

Als Nächstes müssen wir die gRPC-Client- und ‑Serverschnittstellen aus unserer .proto-Dienstdefinition generieren. Dazu verwenden wir den Protokollpuffer-Compiler protoc mit einem speziellen gRPC-Java-Plug-in. Sie müssen den proto3-Compiler (der sowohl die proto2- als auch die proto3-Syntax unterstützt) verwenden, um gRPC-Dienste zu generieren.

Wenn Sie Gradle oder Maven verwenden, kann das protoc-Build-Plug-in den erforderlichen Code im Rahmen des Builds generieren. In der README-Datei für grpc-java finden Sie Informationen dazu, wie Sie Code aus Ihren eigenen .proto-Dateien generieren.

Wir haben im Quellcode des Codelabs eine Gradle-Umgebung und -Konfiguration für die Erstellung dieses Projekts bereitgestellt.

Führen Sie im Verzeichnis grpc-java-getting-started den folgenden Befehl aus:

$ chmod +x gradlew
$ ./gradlew generateProto

Die folgenden Klassen werden aus unserer Dienstdefinition generiert:

  • Feature.java, Point.java und andere, die den gesamten Protocol Buffer-Code zum Erstellen, Serialisieren und Abrufen unserer Anfrage- und Antwortnachrichtentypen enthalten.
  • RouteGuideGrpc.java, das (neben anderem nützlichen Code) eine Basisklasse für die Implementierung von RouteGuide-Servern, RouteGuideGrpc.RouteGuideImplBase, mit allen im RouteGuide-Dienst definierten Methoden und Stub-Klassen für die Verwendung durch Clients enthält.

5. Server 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:

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

RouteGuide implementieren

Wie Sie sehen, hat unser Server eine RouteGuideService-Klasse, die die generierte abstrakte RouteGuideGrpc.RouteGuideImplBase-Klasse erweitert:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

Wir haben die folgenden zwei Dateien zum Initialisieren Ihres Servers mit Funktionen bereitgestellt:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

Sehen wir uns eine einfache RPC-Implementierung genauer an.

Unäre RPC

RouteGuideService implementiert alle unsere Dienstmethoden. In diesem Fall ist es nur GetFeature(). Es wird eine Point-Nachricht vom Client empfangen und in einer Feature-Nachricht werden die entsprechenden Standortinformationen aus einer Liste bekannter Orte zurückgegeben.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

Die Methode getFeature() hat zwei Parameter:

  • Point: die Anfrage.
  • StreamObserver<Feature>: Ein Antwort-Observer, eine spezielle Schnittstelle, die der Server mit seiner Antwort aufrufen kann.

So geben Sie unsere Antwort an den Kunden zurück und beenden den Anruf:

  1. Wir erstellen und füllen ein Feature-Antwortobjekt, das gemäß unserer Dienstdefinition an den Client zurückgegeben wird. In diesem Beispiel erfolgt dies in einer separaten privaten Methode checkFeature().
  2. Wir verwenden die Methode onNext() des Antwort-Observers, um die Feature zurückzugeben.
  3. Mit der Methode onCompleted() des Antwort-Observers geben wir an, dass wir den RPC abgeschlossen haben.

Server starten

Nachdem wir alle Dienstmethoden implementiert haben, müssen wir einen gRPC-Server starten, damit Clients unseren Dienst verwenden können. In unserem Boilerplate-Code ist die Erstellung des ServerBuilder-Objekts enthalten:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

Wir erstellen den Dienst im Konstruktor:

  1. Geben Sie den Port an, den wir verwenden möchten, um auf Clientanfragen zu warten. Verwenden Sie dazu die forPort()-Methode des Builders (es wird die Platzhalteradresse verwendet).
  2. Erstellen Sie eine Instanz unserer Dienstimplementierungsklasse RouteGuideService und übergeben Sie sie an die addService()-Methode des Builders.
  3. Rufen Sie build() für den Builder auf, um einen RPC-Server für unseren Dienst zu erstellen.

Im folgenden Snippet sehen Sie, wie ein ServerBuilder-Objekt erstellt wird.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

Das folgende Snippet zeigt, wie wir ein Serverobjekt für unseren RouteGuide-Dienst erstellen.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

Implementieren Sie eine Startmethode, die start auf dem oben erstellten Server aufruft.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

Implementieren Sie eine Methode, mit der Sie warten, bis der Server abgeschlossen ist, damit er nicht sofort beendet wird.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

Wie Sie sehen, erstellen und starten wir unseren Server mit einem ServerBuilder.

In der Hauptmethode führen wir folgende Schritte aus:

  1. Erstellen Sie eine RouteGuideServer-Instanz.
  2. Rufen Sie start() auf, um einen RPC-Server für unseren Dienst zu aktivieren.
  3. Warten Sie, bis der Dienst durch Aufrufen von blockUntilShutdown() beendet wird.
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. Client erstellen

In diesem Abschnitt sehen wir uns an, wie wir einen Client für unseren RouteGuide-Dienst erstellen.

Stub instanziieren

Um Dienstmethoden aufzurufen, müssen wir zuerst einen Stub erstellen. Es gibt zwei Arten von Stubs, aber für dieses Codelab benötigen wir nur den blockierenden. Es gibt zwei Arten:

  • Ein blockierender/synchroner Stub, der einen RPC-Aufruf ausführt und auf die Antwort des Servers wartet. Er gibt entweder eine Antwort zurück oder löst eine Ausnahme aus.
  • ein nicht blockierender/asynchroner Stub, der nicht blockierende Aufrufe an den Server ausführt, wobei die Antwort asynchron zurückgegeben wird. Bestimmte Arten von Streaminganrufen können nur über den asynchronen Stub erfolgen.

Zuerst müssen wir einen gRPC-Channel erstellen und dann den Channel verwenden, um unseren Stub zu erstellen.

Wir hätten auch direkt eine ManagedChannelBuilder verwenden können, um den Channel zu erstellen.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

Wir verwenden jedoch eine Hilfsmethode, die einen String mit hostname:port akzeptiert.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

Jetzt können wir den Kanal verwenden, um den Blockierungs-Stub zu erstellen. In diesem Codelab haben wir nur blockierende RPCs. Daher verwenden wir die newBlockingStub-Methode, die in der RouteGuideGrpc-Klasse bereitgestellt wird, die wir aus unserem .proto generiert haben.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

Dienstmethoden aufrufen

Sehen wir uns nun an, wie wir unsere Dienstmethoden aufrufen.

Einfache RPC

Der Aufruf des einfachen RPC GetFeature ist fast so einfach wie der Aufruf einer lokalen Methode.

Wir erstellen und füllen ein Protokollzwischenspeicherobjekt für die Anfrage (in unserem Fall Point), übergeben es an die Methode getFeature() in unserem blockierenden Stub und erhalten eine Feature zurück.

Wenn ein Fehler auftritt, wird er als Status codiert, die wir aus dem StatusRuntimeException abrufen können.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

Im Boilerplate-Code wird eine Nachricht mit dem Inhalt protokolliert, je nachdem, ob an der angegebenen Stelle ein Feature vorhanden war oder nicht.

7. Probier es gleich aus!

  1. Führen Sie im Verzeichnis start_here den folgenden Befehl aus:
$ ./gradlew installDist

Dadurch wird Ihr Code kompiliert, in einem JAR verpackt und die Skripts zum Ausführen des Beispiels werden erstellt. Sie werden im Verzeichnis build/install/start_here/bin/ erstellt. Die Skripts sind: route-guide-server und route-guide-client.

Der Server muss ausgeführt werden, bevor der Client gestartet wird.

  1. Führen Sie den Server aus:
$ ./build/install/start_here/bin/route-guide-server
  1. Führen Sie den Client aus:
$ ./build/install/start_here/bin/route-guide-client

Die Ausgabe sieht so aus (Zeitstempel wurden der Übersichtlichkeit halber weggelassen):

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. Nächste Schritte