Como comentaba cuando presenté PlanPok mi intención era aprender pero todavía más interesante es enfrentarse a problemas, resolverlos y poder enseñar. No hay mejor forma de entender las cosas que intentar explicarlas. Para este post me centro cómo tuve que configurar RSocket en la parte javascript para hacerlo funcionar.

¿Qué es RSocket?

RSocket es un protocolo binario para comunicación entre pares, en este caso entre un frontend web y un backend. En estos casos el canal de comunicación se establece y permanece abierto hasta que una de las partes lo cierra, permitiendo que cualquiera de los dos pueda enviar información en cualquier momento.

Hay 4 formas de interactuar:

  • Fire and forget: se envía un mensaje desde el cliente y no se espera respuesta
  • Request response: se envía un mensaje desde el cliente y el servidor envía respuesta
  • Request stream: el cliente envía un mensaje y se espera que lleguen mensajes asíncronos desde el servidor en algún momento
  • Request channel: se establece un intercambio continuo y asíncrono entre cliente y servidor

En mi caso lo utilicé en el proyecto porque me daba dos cosas que necesitaba, por un lado enrutar los mensajes al estilo API REST, es decir, definir endpoints en los que cada uno espera un mensaje concreto y por otro me permitía se completamente reactivo en la parte de backend con Spring Boot.

¿Es lo que debes usar? Depende. Hay más opciones, yo empecé probando WebSockets con STOMP pero no me permitía la parte reactiva y tuve que cambiar.

Inicializar la conexión

Voy a centrarme en esta parte porque fue la que me dio más dolor de cabeza por no entenderlo bien.

En primer lugar aviso que este código es para una versión 0.0.27 de RSocket. A día de hoy existe una versión 1.0.0 en alpha y este código no funciona, habrá que esperar a la versión estable para probarlo bien.

import {RSocketClient} from "rsocket-core";
import {IdentitySerializer, JsonSerializer} from "rsocket-core/build/RSocketSerialization";
import RSocketWebSocketClient from "rsocket-websocket-client";

const metadataRoute = (route) => {
  return String.fromCharCode(route.length) + route;
};

const createClient = url => {
  return new RSocketClient({
    transport: new RSocketWebSocketClient({url})
    serializers: {
      metadata: IdentitySerializer,
      data: JsonSerializer
    },
    setup: {
      keepAlive: 60000,
      lifetime: 180000,
      dataMimeType: 'application/json',
      metadataMimeType: 'message/x.rsocket.routing.v0',
      payload: {
        metadata: metadataRoute('setup'),
        data: {/* your data */}
      }
    }
  });
}

const connect = (url, callback) => {
  createClient(url)
    .connect()
    .subscribe(callback);
  };

export {connect}

Con este módulo podremos establecer una conexión contra el servidor y entonces realizar cualquiera de las cuatro formas de comunicación que he descrito antes. Pero veamos para qué sirve cada cosa.

connect recibe dos parámetros:

  • url que apunta al servidor
  • callback un objeto para implementar dos posibles acciones: onSuccess y onError que contienen la respuesta cuando la conexión se ha podido realizar y cuando ha fallado respectivamente

Así por ejemplo:

import {create} from "socket";

create("wss://ws.planpok.com", {
  onSuccess: successMessage => /* connection established */,
  onError: errorMessage => /* cannot connect */
});

Ya está lo fácil, ahora veamos cómo se ha creado la conexión y qué pasos hemos dado.

La llamada a createClient devuelve una instancia de RSocketClient cuyo constructor tiene tres opciones:

  • transport: cliente usado para transportar la información, usaremos RSocketTcpClient o RSocketWebSocketClient. La diferencia es que en el primero se usa una conexión TCP en plano mientras que en la segunda no. En caso de querer usar un navegador web, no queda otra que usar la opción de websocket porque TCP no la soportan
  • serializers: qué conversión hay que realizar a los datos (data) y a los metadatos (metadata). En caso de no querer enviarla tal y como se genera usaremos IdentitySerializer mientras que si queremos convertirla en JSON usaremos JsonSerializer
  • setup: más parámetros para configurar:
    • keepAlive: establece el tiempo que debe pasar entre cada llamada que envía un paquete destinado a mantener la conexión abierta, en milisegundos
    • lifetime: tiempo máximo de vida de la conexión, en milisegundos
    • dataMimeType: mime-type del contenido de los mensajes que se envían. Tener siempre en cuenta lo que se ha indicado en serializers
    • metadataMimeType: mime-type de la cabecera de los mensajes. En el caso de usar enrutado debemos indicar message/x.rsocket.routing.v0
    • payload: mensaje enviado en la conexión, si se necesita

Ahora que espero que esté un poco más claro, contaré donde me enfrasqué yo de forma bastante tonta.

Lo primero es la forma en la que se envíalos metadatos. Es sencillo, por cada elemento se enviará primero un char con el tamaño de bytes y seguidamente el contenido. En caso de tener más de un elemento es cuestión de repetir la misma estructura: tamaño+contenido, tamaño+contenido, etc

Por lo tanto, cada elemento de metadata tiene un tamaño máximo de 255 bytes. En metadataRoute se puede ver este código.

Como he indicado que el mime-type de metadata es message/x.rsocket.routing.v0 eso quiere decir que voy a enrutar los mensajes, es decir, voy a indicar para cada mensaje una ruta que podríamos hacer la equivalencia al path en una petición REST y esta información tiene que enviarse de forma plana como he indicado antes. Bien, cometí un error en la definición del serializador de metadata y en vez de usar IdentitySerializer usé JsonSerializer, lo que hacía que en destino no se entendiese esta información.

No comentáis el mismo error que yo. En mi favor diré que aprendí bastante leyendo el protocolo para intentar entender cómo funcionaba y dónde estaba cometiendo el error, pero en retrospectiva diré que no hace falta llegar tan lejos para poder usarlo.