La tecnología de socket web permite enviar mensajes desde un servidor a un cliente en una aplicación web o en una aplicación móvil, lo que no se puede hacer utilizando la REST-API. Para trabajar con sockets web, a menudo usan la biblioteca socket.io, o los desarrolladores trabajan con objetos de sockets web nativos del navegador. En esta publicación, intentaré mostrar que ambas formas no resuelven todos los problemas, y es mucho mejor usar servidores especializados, por ejemplo, el servidor mqtt (antes se llamaba el agente mqtt) para trabajar con sockets web.
Para ser justos, y para evitar disputas innecesarias, observo que, además del servidor mqtt, se pueden utilizar otros servidores, por ejemplo, rabbitmq.
Desarrollar aplicaciones usando sockets web parece muy simple hasta que nos encontramos con una realidad en la que a menudo ocurren interrupciones de conexión. La primera tarea que debe resolverse es rastrear los cortes de conexión y restaurarlos. La situación se complica por el hecho de que durante la desconexión y reconexión, el cliente continúa enviando nuevos mensajes, así como también se pueden enviar nuevos mensajes al cliente que tienen más probabilidades de perderse.
Es necesario en el nivel de la aplicación monitorear la recepción de mensajes e implementar su entrega repetida. Especialmente me gustaría llamar la atención sobre la frase "a nivel de aplicación" (pero me gustaría que fuera a nivel de protocolo).
Tan pronto como agregamos lógica para rastrear la entrega de mensajes, todos los mensajes comenzaron a llegar, pero de inmediato quedó claro que había mensajes duplicados, ya que el mensaje podría haberse recibido, y la confirmación de este hecho se perdió debido a una conexión desconectada. Y debe duplicar la cantidad de código de programa para excluir mensajes duplicados.
Junto con la complicación del código del programa, su eficiencia también disminuye, por lo que a menudo critican la biblioteca socket.io. Por supuesto, es menos efectivo que trabajar con sockets web nativos, en particular, debido a la presencia de lógica de reconexión y confirmación de entrega de mensajes (inmediatamente noto que la lógica de reenvío no está implementada en socket.io).
Una forma más confiable y eficiente sería llevar esta lógica al nivel de protocolo. Y ese protocolo existe: es mqtt. La primera versión del protocolo mqtt fue desarrollada por Andy Stanford-Clark (IBM) y Arlene Nipper (Arcom) en 1999. La especificación MQTT 3.1.1 fue estandarizada por el consorcio OASIS en 2014.
El protocolo mqtt tiene un parámetro de "calidad de servicio" (qos) que puede tomar valores:
0: el mensaje se entrega si es posible;
1 - el mensaje se entrega garantizado, pero puede haber duplicados;
2 - el mensaje se entrega garantizado y garantizado una vez.
Es decir, el protocolo mqtt resuelve el problema con la entrega garantizada de mensajes, y este problema se elimina de la agenda. Pero no solo esta pregunta.
Rendimiento y escalamiento.
Cuando se trabaja con sockets web, todos los clientes conectados dejan conexiones abiertas al servidor, incluso si no hay mensajes reales. Esta carga es inherentemente diferente de la carga en la REST-API, que está determinada por el flujo de solicitudes. La carga de las conexiones abiertas en los sockets web es difícil de emular durante la fase de prueba. Por lo tanto, a menudo se hace una suposición errónea sobre el rendimiento suficiente de la aplicación por la cantidad de mensajes enviados y recibidos, sin tener en cuenta la carga de mantener una gran cantidad de conexiones abiertas con los clientes.
Si transferimos todo el trabajo con sockets web a un servidor mqtt especializado, entonces nuestra aplicación nodejs abre solo una conexión en un socket web (o tcp, ya que mqtt admite ambos protocolos) con un servidor mqtt, y podemos escale nuestra aplicación conectando múltiples instancias de nodejs al servidor mqtt.
Si los recursos de un servidor mqtt están agotados, puede organizar un clúster de servidores mqtt sin afectar las aplicaciones en nodejs.
Ahora pasemos a un ejemplo.
El servidor o corredor mqtt, como se llamaba en las especificaciones anteriores, funciona de acuerdo con el modelo de envío de mensajes / suscripción a mensajes. Cada mensaje se envía al tema. El destinatario se suscribe al hilo del mensaje. Tanto el remitente como el receptor tienen dos identificadores: clientId (identificador de dispositivo) y userName (nombre de usuario).
El identificador del dispositivo es importante, ya que está asociado con la suscripción y se le enviarán mensajes. El nombre de usuario, a diferencia del identificador del dispositivo, no juega un papel decisivo en la entrega de mensajes, y se utiliza para diferenciar el acceso a los temas.
Para trabajar con el protocolo mqtt en el lado del cliente, se usa la biblioteca
github.com/eclipse/paho.mqtt.javascript . Hay varias implementaciones de servidor, incluidas las gratuitas. En este ejemplo, utilizaremos el servidor emqx, que se ejecuta a través de docker-compose (consulte
github.com/apapacy/tut-mqtt ).
Para las pruebas, cree un documento en el que configuraremos clientId, userName y mensaje de texto:
<script src="/paho-mqtt.js"></script> <script src="/messages.js"></script> <form name="sender" onsubmit="return false"> <input type="text" name="user"> <input type="text" name="client"> <input type="text" name="message"> <input type="button" onclick="connect()" value="connect"> <input type="button" onclick="send()" value="send"> </form>
El envío de mensajes se implementa en el archivo message.js:
var client; var connectOptions = { timeout: 30, reconnect: true, cleanSession: false, mqttVersion: 4, keepAliveInterval: 10, onSuccess: onConnect, onFailure: onFailure } function connect() { try { client = new Paho.Client('localhost', 8083, '/mqtt', document.forms.sender.client.value); connectOptions.userName = document.forms.sender.user.value; client.connect(connectOptions); } catch (ex) { console.log(ex); } } function onConnect() { console.log('on connect'); client.onMessageArrived = function(message) { console.log("onMessageArrived: " + message.payloadString); } client.subscribe("test", { qos: 2 }); } function onFailure(err) { console.log('on failure', JSON.stringify(err)); } function send() { var message = new Paho.Message(document.forms.sender.message.value); message.destinationName = "test"; message.qos = 2; client.send(message); }
Para verificar, abra el archivo index.html en el navegador, especifique clientId, userName, mensaje de texto y envíe varios mensajes (puede leerlos en la consola, ya que el cliente envía mensajes al tema de prueba y está suscrito a este tema).
Ahora abra otro navegador u otra pestaña del navegador, y únase a otro (esto es importante) clientId. Envíe algunos mensajes más desde el primer navegador y asegúrese de que lleguen a ambos clientes, ya que tienen un ID de cliente diferente y ambos están suscritos a la prueba de tema.
Ahora cierre el segundo navegador (o la segunda pestaña del navegador) y envíe algunos mensajes más. Después de eso, vuelva a abrir el segundo navegador y únase al mismo clientId. Asegúrese de que en los registros de la consola haya recibido todos los mensajes que se enviaron durante el período en que se cerró el segundo navegador (segunda pestaña). Esto sucedió porque:
- Al enviar un mensaje, se estableció el nivel de calidad qos = 2;
- Anteriormente se unió al mismo tema con el mismo clientId estableciendo qos = 2;
- Las opciones de conexión están establecidas en cleanSession: false.
El código de muestra se puede descargar desde el repositorio .
apapacy@gmail.com
29 de septiembre de 2019