Comienza a usar gRPC-Java: transmisión

1. Introducción

En este codelab, usarás gRPC-Java para crear un cliente y un servidor que formen la base de una aplicación de asignación de rutas escrita en Java.

Al final del instructivo, tendrás un cliente que se conecta a un servidor remoto con gRPC para obtener información sobre las funciones en la ruta de un cliente, crear un resumen de la ruta de un cliente y compartir información de la ruta, como actualizaciones de tráfico, con el servidor y otros clientes.

El servicio se define en un archivo de Protocol Buffers, que se usará para generar código estándar para el cliente y el servidor, de modo que puedan comunicarse entre sí, lo que te ahorrará tiempo y esfuerzo en la implementación de esa funcionalidad.

Este código generado se encarga no solo de las complejidades de la comunicación entre el servidor y el cliente, sino también de la serialización y deserialización de datos.

Qué aprenderás

  • Cómo usar los búferes de protocolo para definir una API de servicio
  • Cómo compilar un cliente y un servidor basados en gRPC a partir de una definición de Protocol Buffers con la generación de código automatizada
  • Conocimiento de la comunicación de transmisión cliente-servidor con gRPC

Este codelab está dirigido a desarrolladores de Java que no conocen gRPC o que desean repasar sus conceptos, o bien a cualquier persona interesada en crear sistemas distribuidos. No se requiere experiencia previa con gRPC.

2. Antes de comenzar

Requisitos previos

  • Versión 24 del JDK

Obtén el código

Para que no tengas que empezar desde cero, este codelab proporciona un andamio del código fuente de la aplicación para que lo completes. En los siguientes pasos, se muestra cómo finalizar la aplicación, incluido el uso de los complementos del compilador de búfer de protocolo para generar el código gRPC estándar.

Primero, crea el directorio de trabajo del codelab y cámbiate a él con el comando cd:

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

Descarga y extrae el codelab:

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

También puedes descargar el archivo .zip que contiene solo el directorio del codelab y descomprimirlo de forma manual.

El código fuente completo está disponible en GitHub si no quieres escribir una implementación.

3. Define mensajes y servicios

El primer paso es definir el servicio de gRPC de la aplicación, su método de RPC y sus tipos de mensajes de solicitud y respuesta con búferes de protocolo. Tu servicio proporcionará lo siguiente:

  • Métodos de RPC llamados ListFeatures, RecordRoute y RouteChat que el servidor implementa y el cliente llama.
  • Los tipos de mensajes Point, Feature, Rectangle, RouteNote y RouteSummary, que son estructuras de datos que se intercambian entre el cliente y el servidor cuando se llaman a los métodos anteriores.

Los búferes de protocolo se conocen comúnmente como protobufs. Para obtener más información sobre la terminología de gRPC, consulta los conceptos básicos, la arquitectura y el ciclo de vida de gRPC.

Este método de RPC y sus tipos de mensajes se definirán en el archivo proto/routeguide/route_guide.proto del código fuente proporcionado.

Creemos un archivo route_guide.proto.

Como en este ejemplo generamos código Java, especificamos una opción de archivo java_package en nuestro .proto:

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

Define los tipos de mensajes

En el archivo proto/routeguide/route_guide.proto del código fuente, primero define el tipo de mensaje Point. Un objeto Point representa un par de coordenadas de latitud y longitud en un mapa. En este codelab, usa números enteros para las coordenadas:

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

Los números 1 y 2 son números de ID únicos para cada uno de los campos de la estructura message.

A continuación, define el tipo de mensaje Feature. Un Feature usa un campo string para el nombre o la dirección postal de algo en una ubicación especificada por un Point:

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

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

Para que se puedan transmitir varios puntos dentro de un área a un cliente, necesitarás un mensaje Rectangle que represente un rectángulo de latitud y longitud, representado como dos puntos opuestos diagonalmente lo y hi:

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

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

Además, un mensaje de RouteNote que representa un mensaje enviado en un punto determinado:

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

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

Por último, necesitarás un mensaje de RouteSummary. Este mensaje se recibe en respuesta a una RPC de RecordRoute, que se explica en la siguiente sección. Contiene la cantidad de puntos individuales recibidos, la cantidad de atributos detectados y la distancia total recorrida como la suma acumulativa de la distancia entre cada punto.

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

Cómo definir métodos de servicio

Para definir un servicio, especifica un servicio con nombre en tu archivo .proto. El archivo route_guide.proto tiene una estructura service llamada RouteGuide que define uno o más métodos proporcionados por el servicio de la aplicación.

Cuando defines métodos RPC dentro de la definición de tu servicio, especificas sus tipos de solicitud y respuesta. En esta sección del codelab, definiremos lo siguiente:

ListFeatures

Obtiene los objetos Feature disponibles en el Rectangle determinado. Los resultados se transmiten en lugar de devolverse de una vez, ya que el rectángulo puede abarcar un área grande y contener una gran cantidad de entidades.

Para esta aplicación, usarás una RPC de transmisión del servidor: el cliente envía una solicitud al servidor y obtiene una transmisión para leer una secuencia de mensajes. El cliente lee la transmisión que se muestra hasta que no haya más mensajes. Como puedes ver en nuestro ejemplo, para especificar un método de transmisión del servidor, debes colocar la palabra clave stream antes del tipo de respuesta.

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

RecordRoute

Acepta un flujo de puntos en una ruta que se recorre y devuelve un RouteSummary cuando se completa el recorrido.

En este caso, es adecuada una RPC de transmisión del cliente: el cliente escribe una secuencia de mensajes y los envía al servidor, nuevamente a través de una transmisión proporcionada. Una vez que el cliente termina de escribir los mensajes, espera a que el servidor los lea todos y muestre la respuesta. Para especificar un método de transmisión por Internet del cliente, coloca la palabra clave stream antes del tipo de solicitud.

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

RouteChat

Acepta un flujo de RouteNotes que se envía mientras se recorre una ruta y recibe otros RouteNotes (p.ej., de otros usuarios).

Este es exactamente el caso de uso para la transmisión bidireccional. Es una RPC de transmisión bidireccional en la que ambos extremos envían una secuencia de mensajes a través de una transmisión de lectura y escritura. Las dos transmisiones operan de forma independiente, por lo que los clientes y los servidores pueden leer y escribir en el orden que deseen. Por ejemplo, el servidor podría esperar a recibir todos los mensajes del cliente antes de escribir sus respuestas, o podría leer un mensaje y, luego, escribir uno, o alguna otra combinación de lecturas y escrituras. Se conserva el orden de los mensajes en cada transmisión. Para especificar este tipo de método, coloca la palabra clave stream antes de la solicitud y la respuesta.

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

4. Genera código de cliente y servidor

A continuación, debemos generar las interfaces de cliente y servidor de gRPC a partir de nuestra definición de servicio .proto. Para ello, usamos el compilador de búfer de protocolo protoc con un complemento especial de gRPC Java. Debes usar el compilador proto3 (que admite la sintaxis de proto2 y proto3) para generar servicios de gRPC.

Cuando se usa Gradle o Maven, el complemento de compilación de protoc puede generar el código necesario como parte de la compilación. Puedes consultar el README de grpc-java para saber cómo generar código a partir de tus propios archivos .proto.

Proporcionamos la configuración de Gradle.

Desde el directorio streaming-grpc-java-getting-started, ingresa

$ chmod +x gradlew
$ ./gradlew generateProto

Las siguientes clases se generan a partir de nuestra definición de servicio (en build/generated/sources/proto/main/java):

  • Uno para cada tipo de mensaje: Feature.java y Rectangle.java, ..., que contienen todo el código del búfer de protocolo para completar, serializar y recuperar nuestros tipos de mensajes de solicitud y respuesta.
  • RouteGuideGrpc.java, que contiene (junto con otro código útil) una clase base para que implementen los servidores RouteGuide, RouteGuideGrpc.RouteGuideImplBase, con todos los métodos definidos en el servicio RouteGuide y las clases stub para que los clientes las usen

5. Implementa el servicio

Primero, veamos cómo crear un servidor RouteGuide. Para que nuestro servicio de RouteGuide funcione correctamente, se deben realizar dos pasos:

  • Implementar la interfaz de servicio generada a partir de nuestra definición de servicio: realizar el "trabajo" real de nuestro servicio
  • Ejecutar un servidor de gRPC para escuchar las solicitudes de los clientes y enviarlas a la implementación del servicio correcta

Implementa RouteGuide

Implementaremos una clase RouteGuideService que extenderá la clase RouteGuideGrpc.RouteGuideImplBase generada. Así se vería la implementación.

public void listFeatures(Rectangle request, StreamObserver<Feature> responseObserver) {
        ...
}

public StreamObserver<Point> recordRoute(final StreamObserver<RouteSummary> responseObserver) {

        ...
}

public StreamObserver<RouteNote> routeChat(final StreamObserver<RouteNote> responseObserver) {

        ...
}

Analicemos en detalle cada implementación de RPC

RPC de transmisión del servidor

A continuación, veamos uno de nuestros RPC de transmisión. ListFeatures es una RPC de transmisión del servidor, por lo que debemos enviar varios Features a nuestro cliente.

private final Collection<Feature> features;

@Override
public void listFeatures(Rectangle request, StreamObserver<Feature> responseObserver) {
  int left = min(request.getLo().getLongitude(), request.getHi().getLongitude());
  int right = max(request.getLo().getLongitude(), request.getHi().getLongitude());
  int top = max(request.getLo().getLatitude(), request.getHi().getLatitude());
  int bottom = min(request.getLo().getLatitude(), request.getHi().getLatitude());

  for (Feature feature : features) {
    if (!RouteGuideUtil.exists(feature)) {
      continue;
    }

    int lat = feature.getLocation().getLatitude();
    int lon = feature.getLocation().getLongitude();
    if (lon >= left && lon <= right && lat >= bottom && lat <= top) {
      responseObserver.onNext(feature);
    }
  }
  responseObserver.onCompleted();
}

Al igual que el RPC simple, este método obtiene un objeto de solicitud (el Rectangle en el que nuestro cliente desea encontrar Features) y un observador de respuesta StreamObserver.

Esta vez, obtenemos tantos objetos Feature como necesitamos para devolver al cliente (en este caso, los seleccionamos de la colección de entidades del servicio según si están dentro de nuestra solicitud Rectangle) y los escribimos uno por uno en el observador de respuestas con su método onNext(). Por último, al igual que en nuestro RPC simple, usamos el método onCompleted() del observador de respuestas para indicarle a gRPC que terminamos de escribir respuestas.

RPC de transmisión del cliente

Ahora veamos algo un poco más complicado: el método de transmisión del cliente RecordRoute(), en el que obtenemos un flujo de Points del cliente y devolvemos un solo RouteSummary con información sobre su viaje.

@Override
public StreamObserver<Point> recordRoute(final StreamObserver<RouteSummary> responseObserver) {
  return new StreamObserver<Point>() {
    int pointCount;
    int featureCount;
    int distance;
    Point previous;
    long startTime = System.nanoTime();

    @Override
    public void onNext(Point point) {
      pointCount++;
      if (RouteGuideUtil.exists(checkFeature(point))) {
        featureCount++;
      }
      // For each point after the first, add the incremental distance from the previous point
      // to the total distance value.
      if (previous != null) {
        distance += calcDistance(previous, point);
      }
      previous = point;
    }

    @Override
    public void onError(Throwable t) {
      logger.log(Level.WARNING, "Encountered error in recordRoute", t);
    }

    @Override
    public void onCompleted() {
      long seconds = NANOSECONDS.toSeconds(System.nanoTime() - startTime);
      responseObserver.onNext(RouteSummary.newBuilder().setPointCount(pointCount)
          .setFeatureCount(featureCount).setDistance(distance)
          .setElapsedTime((int) seconds).build());
      responseObserver.onCompleted();
    }
  };
}

Como puedes ver, al igual que los tipos de métodos anteriores, nuestro método obtiene un parámetro StreamObserver responseObserver, pero, esta vez, devuelve un StreamObserver para que el cliente escriba su Points.

En el cuerpo del método, creamos una instancia de StreamObserver anónima para devolverla, en la que hacemos lo siguiente:

  • Anula el método onNext() para obtener funciones y otra información cada vez que el cliente escriba un Point en el flujo de mensajes.
  • Anula el método onCompleted() (se llama cuando el cliente terminó de escribir mensajes) para completar y compilar nuestro RouteSummary. Luego, llamamos al onNext() del observador de respuesta de nuestro método con nuestro RouteSummary y, luego, llamamos a su método onCompleted() para finalizar la llamada desde el servidor.

RPC de transmisión bidireccional

Por último, veamos nuestro RPC de transmisión bidireccional RouteChat().

@Override
public StreamObserver<RouteNote> routeChat(final StreamObserver<RouteNote> responseObserver) {
  return new StreamObserver<RouteNote>() {
    @Override
    public void onNext(RouteNote note) {
      List<RouteNote> notes = getOrCreateNotes(note.getLocation());

      // Respond with all previous notes at this location.
      for (RouteNote prevNote : notes.toArray(new RouteNote[0])) {
        responseObserver.onNext(prevNote);
      }

      // Now add the new note to the list
      notes.add(note);
    }

    @Override
    public void onError(Throwable t) {
      logger.log(Level.WARNING, "Encountered error in routeChat", t);
    }

    @Override
    public void onCompleted() {
      responseObserver.onCompleted();
    }
  };
}

Al igual que con nuestro ejemplo de transmisión del cliente, obtenemos y devolvemos un StreamObserver, excepto que esta vez devolvemos valores a través del observador de respuesta de nuestro método mientras el cliente sigue escribiendo mensajes en su transmisión de mensajes. La sintaxis para leer y escribir aquí es exactamente la misma que para nuestros métodos de transmisión del cliente y transmisión del servidor. Aunque cada lado siempre recibirá los mensajes del otro en el orden en que se escribieron, tanto el cliente como el servidor pueden leer y escribir en cualquier orden. Las transmisiones operan de forma completamente independiente.

Inicia el servidor.

Una vez que implementamos todos nuestros métodos, también debemos iniciar un servidor de gRPC para que los clientes puedan usar nuestro servicio. En el siguiente fragmento, se muestra cómo lo hacemos para nuestro servicio RouteGuide:

public RouteGuideServer(int port, URL featureFile) throws IOException {
  this(ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile));
}

/** 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();
}
public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

Como puedes ver, compilamos e iniciamos nuestro servidor con un ServerBuilder.

Para ello, hacemos lo siguiente:

  1. Especifica la dirección y el puerto que queremos usar para escuchar las solicitudes del cliente con el método forPort() del compilador.
  2. Crea una instancia de nuestra clase de implementación de servicio RouteGuideService y pásala al método addService() del compilador.
  3. Llama a build() y start() en el compilador para crear y, luego, iniciar un servidor RPC para nuestro servicio.

Como ServerBuilder ya incorpora el puerto, el único motivo por el que pasamos un puerto es para usarlo en el registro.

6. Crea el cliente

En esta sección, veremos cómo crear un cliente para nuestro servicio RouteGuide. Puedes ver nuestro código de cliente de ejemplo completo en ../complete/src/main/java/io/grpc/complete/routeguide/ RouteGuideClient.java.

Crea una instancia de un stub

Para llamar a los métodos de servicio, primero debemos crear un stub, o más bien, dos stubs:

  • Un stub síncrono o de bloqueo: Esto significa que la llamada a RPC espera a que el servidor responda y devolverá una respuesta o generará una excepción.
  • Un stub asíncrono/sin bloqueo que realiza llamadas sin bloqueo al servidor, en el que la respuesta se devuelve de forma asíncrona. Solo puedes realizar ciertos tipos de llamadas de transmisión con un stub asíncrono.

Primero, debemos crear un canal de gRPC para nuestro stub, especificando la dirección y el puerto del servidor al que queremos conectarnos:

  public static void main(String[] args) throws InterruptedException {
    String target = "localhost:8980";
    if (args.length > 0) {
      if ("--help".equals(args[0])) {
        System.err.println("Usage: [target]");
        System.err.println("");
        System.err.println("  target  The server to connect to. Defaults to " + target);
        System.exit(1);
      }
      target = args[0];
    }

    List<Feature> features;
    try {
      features = RouteGuideUtil.parseFeatures(RouteGuideUtil.getDefaultFeaturesFile());
    } catch (IOException ex) {
      ex.printStackTrace();
      return;
    }

    ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
        .build();
    try {
      RouteGuideClient client = new RouteGuideClient(channel);

      // Looking for features between 40, -75 and 42, -73.
      client.listFeatures(400000000, -750000000, 420000000, -730000000);

      // Record a few randomly selected points from the features file.
      client.recordRoute(features, 10);

      // Send and receive some notes.
      CountDownLatch finishLatch = client.routeChat();

      if (!finishLatch.await(1, TimeUnit.MINUTES)) {
        client.warning("routeChat did not finish within 1 minutes");
      }
    } finally {
      channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
    }
  }

Usamos un ManagedChannelBuilder para crear el canal.

Ahora podemos usar el canal para crear nuestros stubs con los métodos newStub y newBlockingStub proporcionados en la clase RouteGuideGrpc que generamos a partir de nuestro .proto.

public RouteGuideClient(Channel channel) {
    blockingStub = RouteGuideGrpc.newBlockingStub(channel);
    asyncStub = RouteGuideGrpc.newStub(channel);
  }

Recuerda que, si no es de bloqueo, es asíncrono

Llama a los métodos de servicio

Ahora veamos cómo llamamos a nuestros métodos de servicio. Ten en cuenta que cualquier RPC creada a partir del stub de bloqueo funcionará en modo de bloqueo o síncrono, lo que significa que la llamada a RPC espera a que responda el servidor y mostrará una respuesta o un error.

RPC de transmisión del servidor

A continuación, veamos una llamada de transmisión del servidor a ListFeatures, que devuelve una transmisión de Feature geográficos:

Rectangle request = Rectangle.newBuilder()
             .setLo(Point.newBuilder().setLatitude(lowLat).setLongitude(lowLon).build())
        .setHi(Point.newBuilder().setLatitude(hiLat).setLongitude(hiLon).build()).build();

Iterator<Feature> features;
try {
  features = blockingStub.listFeatures(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

Como puedes ver, es muy similar al RPC unario simple que vimos en el codelab de Getting_Started_With_gRPC_Java, excepto que, en lugar de devolver un solo Feature, el método devuelve un Iterator que el cliente puede usar para leer todos los Features devueltos.

RPC de transmisión del cliente

Ahora, veamos algo un poco más complicado: el método de transmisión del cliente RecordRoute, en el que enviamos una transmisión de Points al servidor y recibimos un solo RouteSummary. Para este método, debemos usar el stub asíncrono. Si ya leíste Cómo crear el servidor, es posible que parte de esto te resulte muy familiar, ya que las RPC de transmisión asíncronas se implementan de manera similar en ambos lados.

public void recordRoute(List<Feature> features, int numPoints) throws InterruptedException {
  info("*** RecordRoute");
  final CountDownLatch finishLatch = new CountDownLatch(1);
  StreamObserver<RouteSummary> responseObserver = new StreamObserver<RouteSummary>() {

    @Override
    public void onNext(RouteSummary summary) {
      info("Finished trip with {0} points. Passed {1} features. "
          + "Travelled {2} meters. It took {3} seconds.", summary.getPointCount(),
          summary.getFeatureCount(), summary.getDistance(), summary.getElapsedTime());
    }

    @Override
    public void onError(Throwable t) {
      Status status = Status.fromThrowable(t);
      logger.log(Level.WARNING, "RecordRoute Failed: {0}", status);
      finishLatch.countDown();
    }

    @Override
    public void onCompleted() {
      info("Finished RecordRoute");
      finishLatch.countDown();
    }
  };

  StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);
  try {
    // Send numPoints points randomly selected from the features list.
    Random rand = new Random();
    for (int i = 0; i < numPoints; ++i) {
      int index = rand.nextInt(features.size());
      Point point = features.get(index).getLocation();
      info("Visiting point {0}, {1}", RouteGuideUtil.getLatitude(point),
          RouteGuideUtil.getLongitude(point));
      requestObserver.onNext(point);
      // Sleep for a bit before sending the next one.
      Thread.sleep(rand.nextInt(1000) + 500);
      if (finishLatch.getCount() == 0) {
        // RPC completed or errored before we finished sending.
        // Sending further requests won't error, but they will just be thrown away.
        return;
      }
    }
  } catch (RuntimeException e) {
    // Cancel RPC
    requestObserver.onError(e);
    throw e;
  }
  // Mark the end of requests
  requestObserver.onCompleted();

  // Receiving happens asynchronously
  finishLatch.await(1, TimeUnit.MINUTES);
}

Como puedes ver, para llamar a este método, necesitamos crear un StreamObserver, que implementa una interfaz especial para que el servidor llame con su respuesta RouteSummary. En nuestro StreamObserver, hacemos lo siguiente:

  • Anula el método onNext() para imprimir la información que se devuelve cuando el servidor escribe un RouteSummary en el flujo de mensajes.
  • Anula el método onCompleted() (se llama cuando el servidor completó la llamada de su lado) para reducir un CountDownLatch de modo que podamos verificar si el servidor terminó de escribir.

Luego, pasamos el StreamObserver al método recordRoute() del stub asíncrono y recuperamos nuestro propio observador de solicitudes StreamObserver para escribir nuestro Points y enviarlo al servidor. Una vez que terminamos de escribir puntos, usamos el método onCompleted() del observador de solicitudes para indicarle a gRPC que terminamos de escribir en el cliente. Cuando terminamos, revisamos nuestro CountDownLatch para ver si el servidor completó su parte.

RPC de transmisión bidireccional

Por último, veamos nuestro RPC de transmisión bidireccional RouteChat().

public CountDownLatch routeChat() {
    info("*** RouteChat");
    final CountDownLatch finishLatch = new CountDownLatch(1);
    StreamObserver<RouteNote> requestObserver =
        asyncStub.routeChat(new StreamObserver<RouteNote>() {
          @Override
          public void onNext(RouteNote note) {
            info("Got message \"{0}\" at {1}, {2}", note.getMessage(), note.getLocation()
                .getLatitude(), note.getLocation().getLongitude());
          }

          @Override
          public void onError(Throwable t) {
            warning("RouteChat Failed: {0}", Status.fromThrowable(t));
            finishLatch.countDown();
          }

          @Override
          public void onCompleted() {
            info("Finished RouteChat");
            finishLatch.countDown();
          }
        });

    try {
      RouteNote[] requests =
          {newNote("First message", 0, 0), newNote("Second message", 0, 10_000_000),
              newNote("Third message", 10_000_000, 0), newNote("Fourth message", 10_000_000, 10_000_000)};

      for (RouteNote request : requests) {
        info("Sending message \"{0}\" at {1}, {2}", request.getMessage(), request.getLocation()
            .getLatitude(), request.getLocation().getLongitude());
        requestObserver.onNext(request);
      }
    } catch (RuntimeException e) {
      // Cancel RPC
      requestObserver.onError(e);
      throw e;
    }
    // Mark the end of requests
    requestObserver.onCompleted();

    // return the latch while receiving happens asynchronously
    return finishLatch;
  }

Al igual que con nuestro ejemplo de transmisión del cliente, obtenemos y devolvemos un observador de respuesta StreamObserver, excepto que, esta vez, enviamos valores a través del observador de respuesta de nuestro método mientras el servidor sigue escribiendo mensajes en el flujo de mensajes de ellos. La sintaxis para leer y escribir aquí es exactamente la misma que para nuestro método de transmisión del cliente. Aunque cada lado siempre recibirá los mensajes del otro en el orden en que se escribieron, tanto el cliente como el servidor pueden leer y escribir en cualquier orden. Las transmisiones operan de forma completamente independiente.

7. ¡Pruébalo!

  1. Desde el directorio start_here, haz lo siguiente:
$ ./gradlew installDist

Esto compilará tu código, lo empaquetará en un archivo JAR y creará las secuencias de comandos que ejecutan el ejemplo. Se crearán en el directorio build/install/start_here/bin/. Los guiones son route-guide-server y route-guide-client.

El servidor debe estar en ejecución antes de iniciar el cliente.

  1. Ejecuta el servidor:
$ ./build/install/start_here/bin/route-guide-server
  1. Ejecuta el cliente:
$ ./build/install/start_here/bin/route-guide-client

8. ¿Qué sigue?