Java gRPC desde cero

Publicado: 2021-07-16

Exploremos cómo implementar gRPC en Java.

gRPC (Llamada a procedimiento remoto de Google): gRPC es una arquitectura RPC de código abierto desarrollada por Google para permitir la comunicación de alta velocidad entre microservicios. gRPC permite a los desarrolladores integrar servicios escritos en diferentes lenguajes. gRPC usa el formato de mensajería Protobuf (Protocol Buffers), un formato de mensajería altamente eficiente y empaquetado para serializar datos estructurados.

Para algunos casos de uso, la API de gRPC puede ser más eficiente que la API de REST.

Intentemos escribir un servidor en gRPC. Primero, necesitamos escribir varios archivos .proto que describan servicios y modelos (DTO). Para un servidor simple, usaremos ProfileService y ProfileDescriptor.

ProfileService tiene este aspecto:

 syntax = "proto3"; package com.deft.grpc; import "google/protobuf/empty.proto"; import "profile_descriptor.proto"; service ProfileService { rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {} rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {} rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {} rpc biDirectionalStream (stream ProfileDescriptor) returns (stream ProfileDescriptor) {} }

gRPC admite una variedad de opciones de comunicación cliente-servidor. Los desglosaremos todos:

  • Llamada normal al servidor: solicitud / respuesta.
  • Transmisión de cliente a servidor.
  • Transmisión de servidor a cliente.
  • Y, por supuesto, el flujo bidireccional.

El servicio ProfileService utiliza ProfileDescriptor, que se especifica en la sección de importación:

 syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
  • int64 es Long para Java. Deje que la identificación del perfil pertenezca.
  • Cadena : al igual que en Java, esta es una variable de cadena.

Puede usar Gradle o maven para construir el proyecto. Es más conveniente para mí usar maven. Y además estará el código usando maven. Esto es lo suficientemente importante como para decirlo porque para Gradle, la futura generación de .proto será ligeramente diferente y el archivo de compilación deberá configurarse de manera diferente. Para escribir un servidor gRPC simple, solo necesitamos una dependencia:

 <dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>

Es simplemente increíble. Este entrante hace una gran cantidad de trabajo para nosotros.

El proyecto que crearemos se verá así:

Necesitamos GrpcServerApplication para iniciar la aplicación Spring Boot. Y GrpcProfileService, que implementará métodos del servicio .proto . Para usar protocolos y generar clases a partir de archivos .proto escritos, agregue protobuf-maven-plugin a pom.xml. La sección de construcción se verá así:

 <build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.6.2</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.6.1</version> <configuration> <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot> <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory> <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact> <clearOutputDirectory>false</clearOutputDirectory> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
  • protoSourceRoot : especifica el directorio donde se encuentran los archivos .proto.
  • outputDirectory : seleccione el directorio donde se generarán los archivos.
  • clearOutputDirectory : una bandera que indica no borrar los archivos generados.

En esta etapa, puede construir un proyecto. A continuación, debe ir a la carpeta que especificamos en el directorio de salida. Los archivos generados estarán allí. Ahora puede implementar gradualmente GrpcProfileService .

La declaración de clase se verá así:

 @GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Anotación GRpcService : marca la clase como un bean grpc-service.

Dado que heredamos nuestro servicio de ProfileServiceGrpc , ProfileServiceImplBase , podemos anular los métodos de la clase principal. El primer método que anularemos es getCurrentProfile :

 @Override public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { System.out.println("getCurrentProfile"); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(1) .setName("test") .build()); responseObserver.onCompleted(); }

Para responder al cliente, debe llamar al método onNext en el StreamObserver pasado. Después de enviar la respuesta, envíe una señal al cliente de que el servidor ha terminado de trabajar en Completado . Al enviar una solicitud al servidor getCurrentProfile, la respuesta será:

 { "profile_id": "1", "name": "test" }

A continuación, echemos un vistazo a la secuencia del servidor. Con este enfoque de mensajería, el cliente envía una solicitud al servidor, el servidor responde al cliente con un flujo de mensajes. Por ejemplo, envía cinco solicitudes en un bucle. Cuando se completa el envío, el servidor envía un mensaje al cliente sobre la finalización exitosa de la transmisión.

El método de transmisión del servidor anulado se verá así:

 @Override public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { for (int i = 0; i < 5; i++) { responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(i) .build()); } responseObserver.onCompleted(); }

Así, el cliente recibirá cinco mensajes con un ProfileId, igual al número de respuesta.

 { "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }

El flujo del cliente es muy similar al flujo del servidor. Solo ahora el cliente transmite un flujo de mensajes y el servidor los procesa. El servidor puede procesar mensajes inmediatamente o esperar todas las solicitudes del cliente y luego procesarlas.

 @Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) { return new StreamObserver<>() { @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }

En la secuencia del cliente, debe devolver el StreamObserver al cliente, al que el servidor recibirá los mensajes. Se llamará al método onError si ocurre un error en la secuencia. Por ejemplo, terminó incorrectamente.

Para implementar un flujo bidireccional, es necesario combinar la creación de un flujo desde el servidor y el cliente.

 @Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream( StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { return new StreamObserver<>() { int pointCount = 0; @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("biDirectionalStream, pointCount {}", pointCount); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(pointCount++) .build()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }

En este ejemplo, en respuesta al mensaje del cliente, el servidor devolverá un perfil con un pointCount aumentado.

Conclusión

Hemos cubierto las opciones básicas para la mensajería entre un cliente y un servidor usando gRPC : flujo de servidor implementado, flujo de cliente, flujo bidireccional.

El artículo fue escrito por Sergey Golitsyn.