Erste Schritte mit gRPC-Go

1. Einführung

In diesem Codelab verwenden Sie gRPC-Go, um einen Client und einen Server zu erstellen, die die Grundlage einer in Go geschriebenen Anwendung für die Routenplanung 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 Go-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:

  • Die Go-Toolchain-Version 1.24.5 oder höher. Eine Installationsanleitung finden Sie unter Erste Schritte mit Go.
  • Der Protokollpuffercompiler protoc, Version 3.27.1 oder höher. Eine Installationsanleitung finden Sie in der Installationsanleitung des Compilers.
  • Die Compiler-Plug-ins für Protokollpuffer für Go und gRPC. Führen Sie die folgenden Befehle aus, um diese Plug-ins zu installieren:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Aktualisieren Sie die Variable PATH, damit der Protokollpuffer-Compiler die Plug-ins finden kann:

export PATH="$PATH:$(go env GOPATH)/bin"

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.

Laden Sie diesen Quellcode als ZIP-Archiv von GitHub herunter und entpacken Sie ihn.

Alternativ ist der vollständige Quellcode 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 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.

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 generieren Sie den Boilerplate-gRPC-Code für Client und Server aus der Datei .proto mit dem Protokollpuffer-Compiler. Führen Sie im Verzeichnis routeguide Folgendes aus:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

Mit diesem Befehl werden die folgenden Dateien generiert:

  • route_guide.pb.go, die Funktionen zum Erstellen der Nachrichtentypen der Anwendung und zum Zugriff auf ihre Daten enthält.
  • route_guide_grpc.pb.go, das Funktionen enthält, mit denen der Client die Remote-gRPC-Methode des Dienstes aufruft, und Funktionen, die der Server zum Bereitstellen dieses Remotedienstes verwendet.

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

5. Dienst implementieren

Die Funktion GetFeature 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 server/server.go implementiert:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

Wenn diese Methode nach einer Anfrage von einem Remote-Client aufgerufen wird, wird der Funktion ein Context-Objekt übergeben, das den RPC-Aufruf beschreibt, sowie ein Point-Protokollpufferobjekt aus dieser Clientanfrage. Die Funktion gibt ein Feature-Protokollzwischenspeicherobjekt für den gesuchten Standort und bei Bedarf ein error zurück.

Füllen Sie in der Methode ein Feature-Objekt mit den entsprechenden Informationen für den angegebenen Point aus und return Sie es dann zusammen mit einem nil-Fehler, um gRPC mitzuteilen, dass Sie den RPC abgeschlossen haben und das Feature-Objekt an den Client zurückgegeben werden kann.

Für die Methode GetFeature muss ein routeGuideServer-Objekt erstellt und registriert werden, damit Anfragen von Clients für die Standortsuche an diese Funktion weitergeleitet werden können. Das funktioniert so:main()

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

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

  1. Geben Sie den TCP-Port an, der für das Abhören von Remote-Clientanfragen verwendet werden soll, indem Sie lis, err := net.Listen(...) verwenden. Standardmäßig verwendet die Anwendung den TCP-Port 50051, der durch die Variable port angegeben wird, oder durch Übergabe des Schalters --port in der Befehlszeile beim Ausführen des Servers. Wenn der TCP-Port nicht geöffnet werden kann, wird die Anwendung mit einem schwerwiegenden Fehler beendet.
  2. Erstellen Sie eine Instanz des gRPC-Servers mit grpc.NewServer(...) und geben Sie ihr den Namen grpcServer.
  3. Erstellen Sie einen Zeiger auf routeGuideServer, eine Struktur, die den API-Dienst der Anwendung darstellt, und nennen Sie den Zeiger s..
  4. Verwenden Sie s.loadFeatures(), um das Array s.savedFeatures mit Standorten zu füllen, die über GetFeature abgerufen werden können.
  5. Registrieren Sie den API-Dienst beim gRPC-Server, damit RPC-Aufrufe an GetFeature an die entsprechende Funktion weitergeleitet werden.
  6. Rufen Sie Serve() auf dem Server mit unseren Portdetails auf, um auf Clientanfragen zu warten. Dies wird fortgesetzt, bis der Prozess beendet oder Stop() aufgerufen wird.

Die Funktion loadFeatures() ruft ihre Zuordnungen von Koordinaten zu Orten aus server/testdata.go ab.

6. Client erstellen

Bearbeiten Sie jetzt client/client.go. Dort implementieren Sie den Clientcode.

Um die Methoden des Remotedienstes aufzurufen, müssen wir zuerst einen gRPC-Channel erstellen, um mit dem Server zu kommunizieren. Dazu übergeben wir den Ziel-URI-String des Servers (in diesem Fall einfach die Adresse und Portnummer) an grpc.NewClient() in der main()-Funktion des Clients:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

Die Adresse des Servers, die durch die Variable serverAddr definiert wird, ist standardmäßig localhost:50051 und kann durch den Schalter --addr in der Befehlszeile beim Ausführen des Clients überschrieben werden.

Wenn der Client eine Verbindung zu einem Dienst herstellen muss, für den Authentifizierungsanmeldedaten wie TLS- oder JWT-Anmeldedaten erforderlich sind, kann er ein DialOptions-Objekt als Parameter an grpc.NewClient übergeben, das die erforderlichen Anmeldedaten enthält. Für den Dienst RouteGuide sind keine Anmeldedaten erforderlich.

Nachdem der gRPC-Channel eingerichtet wurde, benötigen wir einen Client-Stub, um RPCs über Go-Funktionsaufrufe auszuführen. Wir rufen diesen Stub mit der Methode NewRouteGuideClient ab, die von der Datei route_guide_grpc.pb.go bereitgestellt wird, die aus der Datei .proto der Anwendung generiert wurde.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

Dienstmethoden aufrufen

In gRPC-Go 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.

Einfache RPC

Der Aufruf des einfachen RPC GetFeature ist fast so einfach wie der Aufruf einer lokalen Methode, in diesem Fall client.GetFeature:

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

Der Client ruft die Methode für den zuvor erstellten Stub auf. Für die Parameter der Methode erstellt und füllt der Client ein Point-Anfrageprotokollzwischenspeicherobjekt. Außerdem übergeben Sie ein context.Context-Objekt, mit dem wir das Verhalten unseres RPC bei Bedarf ändern können, z. B. ein Zeitlimit für den Aufruf festlegen oder einen laufenden RPC abbrechen. Wenn beim Aufruf kein Fehler zurückgegeben wird, kann der Client die Antwortinformationen vom Server aus dem ersten Rückgabewert lesen:

log.Println(feature)

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

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. Jetzt ausprobieren

Prüfen Sie, ob Server und Client ordnungsgemäß zusammenarbeiten, indem Sie die folgenden Befehle im Arbeitsverzeichnis der Anwendung ausführen:

  1. Führen Sie den Server in einem Terminal aus:
cd server
go run .
  1. Führen Sie den Client über ein anderes Terminal aus:
cd client
go run .

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

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. Nächste Schritte