Java gRPC desde cero

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 idiomas. gRPC usa el formato de mensajería Protobuf (Búferes de protocolo), un formato de mensajería altamente eficiente y muy empaquetado para serializar datos estructurados.

Para algunos casos de uso, la API gRPC puede ser más eficiente que la API 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 se ve así:

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 el 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 largo 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 será el código usando maven. Esto es lo suficientemente importante como para decirlo porque para Gradle, la generación futura del .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 motor de arranque 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 protoc 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: especificando el directorio donde se encuentran los archivos .proto.
  • outputDirectory: seleccione el directorio donde se generarán los archivos.
  • clearOutputDirectory: una bandera que indica que no se deben 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 transmisión 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": ""
}

La transmisión del cliente es muy similar a la transmisión del servidor. Solo que 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 el flujo del cliente, debe devolver el StreamObserver al cliente, al que el servidor recibirá los mensajes. Se llamará al método onError si se produce un error en la transmisión. 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 número de puntos 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.