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 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 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.
Erstellen Sie zuerst das Arbeitsverzeichnis für das Codelab und wechseln Sie zu cd:
mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-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-go-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 müssen Sie den gRPC-Dienst der Anwendung, die RPC-Methoden sowie die Nachrichten-Typen für Anfragen und Antworten mit Protocol Buffers definieren. Ihr Dienst bietet Folgendes:
- RPC-Methoden mit den Namen ListFeatures,RecordRouteundRouteChat, die der Server implementiert und der Client aufruft.
- Die Nachrichtentypen Point,Feature,Rectangle,RouteNoteundRouteSummary, 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 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.
Nachrichtentypen definieren
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 Distanz 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
Um einen Dienst zu definieren, geben Sie einen benannten Dienst in Ihrer .proto-Datei an. 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.
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 Elementen 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 Streaming-Methode an, indem Sie das Stream-Keyword 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 Stream-Keyword 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, sodass Clients und Server in beliebiger Reihenfolge lesen und schreiben können.
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 diese Art von Methode an, indem Sie das Stream-Keyword sowohl vor die Anfrage als auch vor die Antwort setzen.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
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 sowie die Definition der Typen enthält, die die Nachrichten darstellen.
- 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 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.
- Einen gRPC-Server ausführen, um auf Anfragen von Clients zu warten und sie an die richtige Dienstimplementierung weiterzuleiten.
Wir implementieren RouteGuide in server/server.go.
RouteGuide implementieren
Wir müssen die generierte RouteGuideService-Schnittstelle implementieren. So würde die Implementierung aussehen.
type routeGuideServer struct {
        ...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
        ...
}
...
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
        ...
}
...
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
        ...
}
Sehen wir uns die einzelnen RPC-Implementierungen genauer an.
Serverseitiger Streaming-RPC
Beginnen Sie mit einem unserer Streaming-RPCs. ListFeatures ist ein serverseitiger Streaming-RPC, daher müssen wir mehrere Feature an unseren Client zurücksenden.
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
  for _, feature := range s.savedFeatures {
    if inRange(feature.Location, rect) {
      if err := stream.Send(feature); err != nil {
        return err
      }
    }
  }
  return nil
}
Wie Sie sehen, erhalten wir diesmal keine einfachen Anfrage- und Antwortobjekte in unseren Methodenparametern, sondern ein Anfrageobjekt (das Rectangle, in dem unser Kunde Features finden möchte) und ein spezielles RouteGuide_ListFeaturesServer-Objekt, um unsere Antworten zu schreiben. In der Methode füllen wir so viele Feature-Objekte wie nötig aus und schreiben sie mit der Send()-Methode in den RouteGuide_ListFeaturesServer. Wie bei unserem einfachen RPC geben wir schließlich einen nil-Fehler zurück, um gRPC mitzuteilen, dass wir mit dem Schreiben von Antworten fertig sind. Sollte bei diesem Aufruf ein Fehler auftreten, wird ein Fehler zurückgegeben, der nicht „nil“ ist. Die gRPC-Ebene übersetzt ihn in einen entsprechenden RPC-Status, der über das Netzwerk gesendet wird.
Clientseitiger Streaming-RPC
Sehen wir uns nun eine etwas kompliziertere Methode an: das clientseitige Streaming RecordRoute. Dabei erhalten wir einen Stream von Points vom Client und geben ein einzelnes RouteSummary mit Informationen zur Fahrt zurück. Wie Sie sehen, hat die Methode dieses Mal überhaupt keinen Anfrageparameter. Stattdessen wird ein RouteGuide_RecordRouteServer-Stream abgerufen, über den der Server sowohl Nachrichten lesen als auch schreiben kann. Er kann Clientnachrichten über die Recv()-Methode empfangen und seine einzelne Antwort über die SendAndClose()-Methode zurückgeben.
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
  var pointCount, featureCount, distance int32
  var lastPoint *pb.Point
  startTime := time.Now()
  for {
    point, err := stream.Recv()
    if err == io.EOF {
      endTime := time.Now()
      return stream.SendAndClose(&pb.RouteSummary{
        PointCount:   pointCount,
        FeatureCount: featureCount,
        Distance:     distance,
        ElapsedTime:  int32(endTime.Sub(startTime).Seconds()),
      })
    }
    if err != nil {
      return err
    }
    pointCount++
    for _, feature := range s.savedFeatures {
      if proto.Equal(feature.Location, point) {
        featureCount++
      }
    }
    if lastPoint != nil {
      distance += calcDistance(lastPoint, point)
    }
    lastPoint = point
  }
}
Im Methodenkörper verwenden wir die Recv()-Methode von RouteGuide_RecordRouteServer, um die Anfragen unseres Clients wiederholt in ein Anfrageobjekt (in diesem Fall ein Point) einzulesen, bis keine Nachrichten mehr vorhanden sind. Der Server muss den von Recv() zurückgegebenen Fehler nach jedem Aufruf prüfen. Wenn der Wert nil ist, ist der Stream noch in Ordnung und kann weiter gelesen werden. Wenn er io.EOF ist, ist der Nachrichtenstream beendet und der Server kann seinen RouteSummary zurückgeben. Wenn er einen anderen Wert hat, geben wir den Fehler „wie er ist“ zurück, damit er von der gRPC-Ebene in einen RPC-Status übersetzt wird.
Bidirektionale Streaming-RPC
Sehen wir uns zum Schluss noch unseren bidirektionalen Streaming-RPC RouteChat() an.
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
  for {
    in, err := stream.Recv()
    if err == io.EOF {
      return nil
    }
    if err != nil {
      return err
    }
    key := serialize(in.Location)
    s.mu.Lock()
    s.routeNotes[key] = append(s.routeNotes[key], in)
    // Note: this copy prevents blocking other clients while serving this one.
    // We don't need to do a deep copy, because elements in the slice are
    // insert-only and never modified.
    rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
    copy(rn, s.routeNotes[key])
    s.mu.Unlock()
    for _, note := range rn {
      if err := stream.Send(note); err != nil {
        return err
      }
    }
  }
}
Dieses Mal erhalten wir einen RouteGuide_RouteChatServer-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. Der Server verwendet jedoch die Methode send() des Streams anstelle von SendAndClose(), da er mehrere Antworten schreibt. 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.
Server starten
Nachdem wir alle unsere Methoden implementiert haben, müssen wir auch einen gRPC-Server starten, damit Clients unseren Dienst tatsächlich nutzen können. Das folgende Snippet zeigt, wie wir das für unseren RouteGuide-Dienst tun:
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
  log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
s := &routeGuideServer{routeNotes: make(map[string][]*pb.RouteNote)}
s.loadFeatures()
pb.RegisterRouteGuideServer(grpcServer, s)
grpcServer.Serve(lis)
So läuft der Vorgang in main() ab:
- 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-Port50051, der durch die Variableportangegeben wird, oder durch Übergabe des Schalters--portin der Befehlszeile beim Ausführen des Servers. Wenn der TCP-Port nicht geöffnet werden kann, wird die Anwendung mit einem schwerwiegenden Fehler beendet.
- Erstellen Sie eine Instanz des gRPC-Servers mit grpc.NewServer(...)und geben Sie ihr den NamengrpcServer.
- Erstellen Sie einen Zeiger auf routeGuideServer, eine Struktur, die den API-Dienst der Anwendung darstellt, und nennen Sie den Zeigers..
- Verwenden Sie s.loadFeatures(), um das Arrays.savedFeaturesauszufüllen.
- Registrieren Sie unsere Dienstimplementierung beim gRPC-Server.
- Rufen Sie Serve()auf dem Server mit unseren Portdetails auf, um auf Clientanfragen zu warten. Dies wird fortgesetzt, bis der Prozess beendet oderStop()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:
// 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()
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_streaming/routeguide")
client := pb.NewRouteGuideClient(conn)
Dienstmethoden aufrufen
Sehen wir uns nun an, wie wir unsere 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.
Serverseitiger Streaming-RPC
Hier rufen wir die serverseitige Streamingmethode ListFeatures auf, die einen Stream geografischer Feature-Objekte zurückgibt.
rect := &pb.Rectangle{ ... }  // initialize a pb.Rectangle
log.Printf("Looking for features within %v", rect)
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
  log.Fatalf("client.ListFeatures failed: %v", err)
}
for {
  // For server-to-client streaming RPCs, you call stream.Recv() until it
  // returns io.EOF.
  feature, err := stream.Recv()
  if err == io.EOF {
    break
  }
  if err != nil {
    log.Fatalf("client.ListFeatures failed: %v", err)
  }
  log.Printf("Feature: name: %q, point:(%v, %v)", feature.GetName(),
    feature.GetLocation().GetLatitude(), feature.GetLocation().GetLongitude())
}
Wie beim einfachen RPC übergeben wir der Methode einen Kontext und eine Anfrage. Anstelle eines Antwortobjekts erhalten wir jedoch eine Instanz von RouteGuide_ListFeaturesClient. Der Client kann den RouteGuide_ListFeaturesClient-Stream verwenden, um die Antworten des Servers zu lesen. Wir verwenden die Recv()-Methode von RouteGuide_ListFeaturesClient, um die Antworten des Servers wiederholt in ein Antwort-Protobuf-Objekt (in diesem Fall ein Feature) einzulesen, bis keine Nachrichten mehr vorhanden sind. Der Client muss den von Recv() zurückgegebenen Fehler „err“ nach jedem Aufruf prüfen. Wenn nil, ist der Stream weiterhin gültig und kann weiter gelesen werden. Wenn io.EOF, ist der Nachrichtenstream beendet. Andernfalls muss ein RPC-Fehler vorliegen, der über err übergeben wird.
Clientseitiger Streaming-RPC
Die clientseitige Streaming-Methode RecordRoute ähnelt der serverseitigen Methode. Der Unterschied besteht darin, dass wir der Methode nur einen Kontext übergeben und einen RouteGuide_RecordRouteClient-Stream zurückerhalten, mit dem wir sowohl Nachrichten schreiben als auch lesen können.
// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
  points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
c2sStream, err := client.RecordRoute(context.TODO())
if err != nil {
  log.Fatalf("client.RecordRoute failed: %v", err)
}
// Stream each point to the server.
for _, point := range points {
  if err := c2sStream.Send(point); err != nil {
    log.Fatalf("client.RecordRoute: stream.Send(%v) failed: %v", point, err)
  }
}
// Close the stream and receive the RouteSummary from the server.
reply, err := c2sStream.CloseAndRecv()
if err != nil {
  log.Fatalf("client.RecordRoute failed: %v", err)
}
log.Printf("Route summary: %v", reply)
Das RouteGuide_RecordRouteClient hat eine Send()-Methode, mit der wir Anfragen an den Server senden können. Nachdem wir die Anfragen unseres Clients mit Send() in den Stream geschrieben haben, müssen wir CloseAndRecv() für den Stream aufrufen, um gRPC mitzuteilen, dass wir mit dem Schreiben fertig sind und eine Antwort erwarten. Wir erhalten unseren RPC-Status vom Fehler, der von CloseAndRecv() zurückgegeben wird. Wenn der Status nil lautet, ist der erste Rückgabewert von CloseAndRecv() eine gültige Serverantwort.
Bidirektionale Streaming-RPC
Sehen wir uns zum Schluss noch unseren bidirektionalen Streaming-RPC RouteChat() an. Wie bei RecordRoute übergeben wir der Methode nur ein Kontextobjekt und erhalten einen Stream zurück, mit dem wir sowohl Nachrichten schreiben als auch lesen können. Dieses Mal geben wir jedoch Werte über den Stream unserer Methode zurück, während der Server weiterhin Nachrichten in seinen Nachrichtenstream schreibt.
biDiStream, err := client.RouteChat(context.Background())
if err != nil {
  log.Fatalf("client.RouteChat failed: %v", err)
}
// this channel is used to wait for the receive goroutine to finish.
recvDoneCh := make(chan struct{})
// receive goroutine.
go func() {
  for {
    in, err := biDiStream.Recv()
    if err == io.EOF {
      // read done.
      close(recvDoneCh)
      return
    }
    if err != nil {
      log.Fatalf("client.RouteChat failed: %v", err)
    }
    log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
  }
}()
// send messages simultaneously.
for _, note := range notes {
  if err := biDiStream.Send(note); err != nil {
    log.Fatalf("client.RouteChat: stream.Send(%v) failed: %v", note, err)
  }
}
biDiStream.CloseSend()
// wait for the receive goroutine to finish.
<-recvDoneCh
Die Syntax zum Lesen und Schreiben ist hier sehr ähnlich wie bei unserer clientseitigen Streamingmethode. Der einzige Unterschied besteht darin, dass wir die CloseSend()-Methode des Streams verwenden, sobald wir den Aufruf abgeschlossen haben. 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.
7. Jetzt ausprobieren
Prüfen Sie, ob Server und Client ordnungsgemäß zusammenarbeiten, indem Sie die folgenden Befehle im Arbeitsverzeichnis der Anwendung ausführen:
- Führen Sie den Server in einem Terminal aus:
cd server go run .
- 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):
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 > name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 > ... name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 > Traversing 56 points. Route summary: point_count:56 distance:497013163 Got message First message at point(0, 1) Got message Second message at point(0, 2) Got message Third message at point(0, 3) Got message First message at point(0, 1) Got message Fourth message at point(0, 1) Got message Second message at point(0, 2) Got message Fifth message at point(0, 2) Got message Third message at point(0, 3) Got message Sixth message at point(0, 3)
8. Nächste Schritte
- Informationen zur Funktionsweise von gRPC finden Sie unter Einführung in gRPC und Grundlegende Konzepte.
- Grundlagen-Anleitung durcharbeiten
- API-Referenz