Hace ya seis años que se publicó la versión 2.0 del Manifiesto Reactivo. Como bien se comenta en este documento, no hace tanto tiempo, una aplicación requería de decenas de servidores, varios segundos de respuesta, muchas horas de mantenimiento y bastantes gigabytes en datos. Hoy en día, la forma en que se construyen y despliegan los sistemas software está evolucionando a pasos agigantados, puesto que las necesidades han ido cambiando y ahora se requiere de aplicaciones que puedan ser desplegadas en cualquier lugar, desde dispositivos móviles hasta clusters en la nube, con tiempos de respuesta medidos en milisegundos, que operan 24x7 y manejan un millón de veces más en datos.

Llegados a este punto, dicho manifiesto introduce los sistemas reactivos, sistemas más flexibles, con bajo acoplamiento y altamente escalables y que se construyen adhiriéndose a 4 principios: capacidad de respuesta (responsive), resiliencia (resilient), elasticidad (elastic) y dirigidos por mensajes (message driven).

En este post vamos a hablar de RSocket, un nuevo protocolo de comunicaciones que se adapta como un guante a estos principios “reactivos”. Detallaremos sus características, las motivaciones por las que aparece y detalles de su implementación mediante un pequeño ejemplo práctico en el que nos mancharemos un poco las manos.

¿Qué es RSocket?

RSocket es un protocolo de comunicación reactivo, binario, asíncrono, uno a uno y sin estado. Está pensado, sobre todo, para las necesidades de la construcción de aplicaciones en la nube con microservicios. Además, es compatible con múltiples lenguajes de programación, como por ejemplo, Java, JavaScript o Python.

La comunicación en el protocolo Rsocket se descompone en frames. Cada frame consta de una cabecera (header), la cual contiene el identificador del stream, el tipo de frame y otra información específica del tipo de frame. Además del header, el frame contiene los metadatos y el payload. Existen varios tipos de frames que representan diferentes propósitos. Todos los tipos pueden consultarse en la documentación oficial.

Estos frames se envían como streams de bytes, permitiendo que este protocolo sea más eficiente que los típicos protocolos basados en texto. El protocolo no impone ninguna restricción en cuanto a la forma de serializar/deserializar la información, simplemente considera el frame como un contenedor de bits que puede ser convertido a cualquier cosa, haciendo posible el uso de JSON o cualquier otra solución como Protobuf.

En último lugar, el protocolo de Rsocket crea una serie de canales sobre la conexión física única (multiplexación). Esto permite enviar múltiples peticiones (requests) sobre la misma conexión, sin ser necesario abrir múltiples conexiones
con el consiguiente consumo de recursos.

Motivación

A finales del 2014 empezó a incrementarse el número de búsquedas sobre el término ‘microservicios’ y, en paralelo, se desarrollaron los primeros frameworks enfocados a la construcción de este tipo de arquitecturas. Hoy en día es un término del que no se para de hablar continuamente. Especialmente, debemos destacar el papel de la compañía estadounidense Netflix porque fue una de las empresas pioneras en migrar con éxito de una arquitectura monolítica a una de microservicios basada en la nube.

Actualmente, Netflix cuenta con más de 1000 microservicios y cada uno de ellos se encarga de una parte del modelo de negocio. RSocket tiene una estrecha relación con esta compañía: sus creadores tenían la intención de desarrollar una forma estándar para que las aplicaciones se comuniquen entre sí a través de la red y qué mejor lugar para hacerlo que ayudando a Netflix.

Os mostramos algunas de las motivaciones que sintieron los desarrolladores de este nuevo protocolo para su creación:

En la especificación original de RSocket, se puede encontrar una información mucho más detallada sobre todos los aspectos que hemos ido comentado anteriormente.

Modelos de interacción

RSocket está basado en cuatro modelos que permiten una interacción simétrica a través de una sola comunicación:

Hands-on

En este apartado vamos a ilustrar el modelo de interacción request - stream, por medio de un pequeño ejemplo práctico donde simularemos un famoso juego de lotería. Podéis encontrar el código de la demostración en el siguiente repositorio.

Para ejecutar la aplicación de manera local, no es necesario disponer de más software que, Docker y Docker Compose. Dentro del repositorio, existe un fichero docker-compose.yml, con el que levantar el sistema mediante el comando:docker-compose up -d (directamente desde el directorio donde has clonado el repositorio).

La aplicación se compone de dos servicios. Por un lado, el componente cliente (una aplicación desarrollada en Angular), que es el encargado de solicitar los números del sorteo al servidor y mostrarlos en la interfaz web. Por otro lado, tenemos el componente servidor (una aplicación desarrollada con Spring Boot) que se encarga de comunicarse con el cliente para la generación y transmisión de los números ganadores de la lotería.

En primer lugar, comenzaremos mostrando los aspectos más relevantes de la parte servidor. Se trata de una aplicación de Spring Boot, y para empezar a trabajar requerimos de las siguientes dependencias en el proyecto:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-rsocket</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

El starter ‘spring-boot-starter-rsocket’ integra RSocket en Spring Boot y configura automáticamente cierta infraestructura de RSocket en tiempo de ejecución (autoconfiguración de servidor RSocket, autoconfiguración de cliente RSocket, autoconfiguración de estrategias RSocket, etc.). El starter ‘spring-boot-starter-webflux’ es la elección que nos permitirá construir la aplicación web de forma reactiva.

En conjunción con estas dependencias debemos configurar una serie de propiedades en el fichero application.yml (o application.properties) para que Spring Boot pueda realizar toda su magia.

Con las propiedades y valores definidos, Spring Boot registra de manera automática una serie de beans, como se muestra en las siguientes capturas de pantalla. Se definen las propiedades:

Sin entrar mucho en detalle, en esta imagen se puede ver cómo se registran una serie de beans pertenecientes a una clase de configuración (EmbeddedServerAutoConfiguration) en el momento que se encuentra definida la propiedad ‘spring.rsocket.server.port’. Uno de esos beans, rSocketServerFactory, se encarga de la creación de un objeto de tipo RSocketServerFactory que puede ser utilizado para crear un servidor de RSocket, respaldado por Netty.

Otro de los beans que se registran es rSocketServerBootstrap, creando un objeto de tipo RSocketServerBootstrap que permite el arranque de un servidor de RSocket y su iniciación con el contexto de aplicación de Spring.

Ya tenemos realizada la configuración del servidor. Como hemos visto previamente, Spring Boot se encargará de la mayor parte. Ahora, el siguiente punto será crear una clase Controlador (LotteryController) que esencialmente declarará endpoints del servicio, en este caso, endpoints RSocket. Dentro del controlador, declararemos el método que se encarga de realizar la operación de generar los números ganadores del sorteo.

El método generateLotteryNumbers recibirá por parámetro un objeto de tipo RequestData que representa el Payload de la petición y que indicará la cantidad de números a generar y si se tratan de números especiales o no. Por otro lado, el método será decorado con @MessageMapping(“generate.numbers”). Esta anotación indica que cualquier mensaje entrante cuyos metadatos contengan la routing-key ‘generate.numbers’ será procesado por este método. Este ejemplo es bastante sencillo, ya que solo disponemos de esta operación, pero si tuviéramos diferentes operaciones en el sistema esta anotación sirve para discernir entre diferentes métodos handler.

Con este último punto ya se han comentado los aspectos más relevantes de la parte servidor. En el repositorio. se puede consultar la pieza restante, el LotteryService, cuya responsabilidad es sortear los números en base a un rango. Este rango se define mediante properties y pueden ser sobreescritas en el arranque de la aplicación mediante variables de entorno.

En cuanto a lo que se refiere a la parte cliente de la aplicación se trata de un proyecto de Angular 11 creado con el propio angular-cli. Resaltar que este mismo ejemplo se podría haber mostrado en cualquier otro framework de JavaScript (React, Vue.js…) o incluso en JavaScript Vanilla.

De igual manera que se ha hecho en la parte servidor, se van a detallar los aspectos más significativos de la parte cliente referente a RSocket.

Para comenzar deberemos añadir las dependencias que se requieren en el proyecto. Después habrá que ejecutar el correspondiente comando npm i para que se instalen en el proyecto.

"dependencies": {
     …
    "rsocket-core": "0.0.19",
    "rsocket-flowable": "0.0.14",
    "rsocket-websocket-client": "0.0.19",
     …
  },
  "devDependencies": {
    …
    "@types/rsocket-core": "0.0.5",
    "@types/rsocket-websocket-client": "0.0.3",
   …
  }

Para gestionar la conexión con el servidor se ha creado una clase Client.ts que contiene los atributos y métodos necesarios para el ejemplo.

A continuación vamos a detallar los métodos más significativos de esta clase:

  constructor(address: string) {
    this.client = new RSocketClient({
      serializers: {
      data: JsonSerializer,
        metadata: IdentitySerializer,
      },
      setup: {
        keepAlive: 10000,
        lifetime: 20000,
        dataMimeType: 'application/json',
        metadataMimeType: 'message/x.rsocket.routing.v0',
      },
      transport: new RSocketWebSocketClient({ url: address }),
    });
  }
 connect(): Promise<any> {
    return new Promise((resolve, reject) => {
      this.client.connect().subscribe({
        onComplete: (rs: object) => {
          this.rsocket = rs;
          this.rsocket.connectionStatus().subscribe((status: object) => {
            console.info(status);
          });

          resolve(this.rsocket);
        },
        onError: (error: Error) => {
          reject(error);
        },
        onSubscribe: (cancel: CancelCallback) => {
          this.cancel = cancel;
        },
      });
    });
  }
 requestStream(clientData: ClientData): Flowable<any> {
    return this.socket.requestStream({
      data: clientData,
      metadata:
        String.fromCharCode('generate.numbers'.length) + 'generate.numbers',
    });
  }
  disconnect(): void {
    this.client.close();
  }

En cuanto al componente principal, el cual hemos llamado LotteryComponent, va a ser el encargado de gestionar el flujo y hacer uso del objeto Client.

Las funciones más representativas en cuanto al flujo e interacción con RSocket de esta componente son:

  connectClient(): void {
    if (!this.connected) {
      this.client = new Client(this.address);
      this.client.connect().then(() => {
        this.connected = true;
        this.getNumbers(5, false, '1');
      });
    } else {
      this.client.disconnect();
      this.connected = false;
    }
  }
 getNumbers(totalNumbers: number, isSpecial: boolean, idContent: string): void {
    if (!this.streamInProgress) {
      const requestedMsg = totalNumbers;
      let processedMsg = 0;
      const clientData = new ClientData(totalNumbers, isSpecial);

      this.client.requestStream(clientData).subscribe({
        onSubscribe: (sub: object) => {
          this.requestStreamSubscription = sub;
          this.requestStreamSubscription.request(requestedMsg);
          this.streamInProgress = true;
        },
        onError: (error: Error) => {
          console.error(error);
        },
        onNext: (clientDto: ClientDto) => {
          this.printResults(clientDto.data, idContent, isSpecial);
          processedMsg++;

          if (processedMsg >= requestedMsg) {
            this.requestStreamSubscription.request(requestedMsg);
            processedMsg = 0;
          }
        },
        onComplete: () => {
          this.streamInProgress = false;
          if (!isSpecial) {
            this.getNumbers(2, true, '2');
          } else {
            this.client.disconnect();
            this.connected = false;
            this.goToHome();
          }
        },
      });
    } else {
      this.requestStreamSubscription.cancel();
    }
  }

Por último, comentar que, igual que en la parte servidor, el resto del código se puede visualizar en el repositorio donde se podrá observar diferentes funciones, sobre todo en la parte referente a la visualización de los números y otras propias del ciclo de vida de una aplicación Angular.

Conclusión

En este post hemos querido mostraros este nuevo protocolo, una alternativa atractiva para trabajar junto con las arquitecturas de software emergentes y en diferentes tipos de entorno como puede ser una arquitectura de microservicios basada en la nube. Viene a resolver muchos de los puntos débiles que existen en el protocolo que se usa de manera tradicional para las comunicaciones (flexibilidad, rendimiento, etc.), HTTP 1/x.

En definitiva, RSocket permite una comunicación efectiva y escalable entre los componentes de la aplicación que es uno de los aspectos cruciales en sistemas distribuidos, afectando directamente a la latencia que experimentan los usuarios y a la cantidad de recursos necesarios para construir y hacer funcionar el sistema.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.