¿Cómo diseñar una aplicación web basada en sockets web en tiempo real?

17

En el proceso de desarrollo de una aplicación de página única en tiempo real, he adoptado progresivamente websockets para darles a mis usuarios datos actualizados. Durante esta fase, me entristeció darme cuenta de que estaba destruyendo demasiado la estructura de mi aplicación, y no pude encontrar una solución a este fenómeno.

Antes de entrar en detalles, solo un poco de contexto:

  • La aplicación web es un SPA en tiempo real;
  • El backend está en Ruby on Rails. Ruby empuja los eventos en tiempo real a una tecla Redis, luego un servidor de micronodo lo retira y lo empuja a Socket.Io;
  • El Frontend está en AngularJS y se conecta directamente al servidor socket.io en Node.

En el lado del servidor, antes del tiempo real tenía una separación clara de los recursos basada en el controlador / modelo, con el procesamiento adjunto a cada uno. Este diseño clásico de MVC fue completamente triturado, o al menos omitido, justo cuando comencé a enviar cosas a mis usuarios a través de websockets. Ahora tengo una única tubería en la que toda mi aplicación fluye por datos más o menos estructurados . Y lo encuentro estresante.

En la parte frontal, la principal preocupación es la duplicación de la lógica empresarial. Cuando el usuario carga la página, tengo que cargar mis modelos a través de llamadas clásicas de AJAX. Pero también tengo que manejar la inundación de datos en tiempo real, y me encuentro duplicando gran parte de mi lógica comercial del lado del cliente para mantener la coherencia de mis modelos del lado del cliente.

Después de algunas investigaciones, no puedo encontrar buenas publicaciones, artículos, libros o lo que sea que brinde consejos sobre cómo se puede y se debe diseñar la arquitectura de una aplicación web moderna con algunos temas específicos en mente:

  • ¿Cómo estructurar los datos que se envían desde el servidor al usuario?
    • ¿Debería enviar solo eventos como "este recurso ha sido actualizado y usted debería volver a cargarlo a través de una llamada AJAX" o enviar los datos actualizados y reemplazar los datos anteriores cargados a través de llamadas AJAX iniciales?
    • ¿Cómo definir un esqueleto coherente y escalable para los datos enviados? ¿Es este un mensaje de actualización del modelo o el mensaje "hubo un error con blahblahblah"?
  • ¿Cómo no enviar datos sobre todo desde cualquier lugar del backend?
  • ¿Cómo reducir la duplicación de lógica de negocios tanto en el servidor como en el lado del cliente?
Philippe Durix
fuente
¿Estás seguro de que Rails es la mejor opción para tu SPA? Rails es excelente, pero está dirigido a la aplicación de monolitos ... es posible que desee un back-end modular con separación de preocupaciones ... Dependiendo de sus necesidades, consideraría marcos de trabajo Ruby alternativos para un SPA en tiempo real.
Myst
1
No estoy seguro de que Rails sea la mejor opción, pero estoy muy satisfecho con la pila en su lugar, al menos en el back-end (probablemente porque soy bueno con este marco en particular). Mi preocupación aquí es más acerca de cómo diseñar el SPA en un punto de vista agnóstico tecnológico. Y tampoco quiero multiplicar la cantidad de idiomas, marcos y bibliotecas en un solo proyecto.
Philippe Durix
El punto de referencia vinculado puede tener problemas, pero expone una debilidad en la implementación actual de ActiveRecord. Podría estar sesgado sobre plezi.io , pero como se señaló en los resultados posteriores del punto de referencia , proporciona mejoras de rendimiento significativas, incluso antes de la agrupación y Redis (que no se probaron). Creo que funcionó mejor que node.js ... Hasta que las cosas cambien, usaría plezi.io.
Myst

Respuestas:

10

¿Cómo estructurar los datos que se envían desde el servidor al usuario?

Usa el patrón de mensajes . Bueno, ya estás usando un protocolo de mensajería, pero quiero decir estructurar los cambios como mensajes ... específicamente eventos. Cuando el lado del servidor cambia, eso resulta en eventos de negocios. En su escenario, las opiniones de sus clientes están interesadas en estos eventos. Los eventos deben contener todos los datos relevantes para ese cambio (no necesariamente todos los datos de vista). La página del cliente debería actualizar las partes de la vista que mantiene con los datos del evento.

Por ejemplo, si estaba actualizando un ticker de acciones y AAPL cambió, no querría bajar todos los precios de las acciones o incluso todos los datos sobre AAPL (nombre, descripción, etc.). Solo presionaría AAPL, el delta y el nuevo precio. En el cliente, solo actualizaría ese precio de la acción en la vista.

¿Debería enviar solo eventos como "este recurso ha sido actualizado y usted debería volver a cargarlo a través de una llamada AJAX" o enviar los datos actualizados y reemplazar los datos anteriores cargados a través de llamadas AJAX iniciales?

Yo diría que ninguno. Si está enviando el evento, continúe y envíe datos relevantes (no todos los datos del objeto). Déle un nombre al tipo de evento que es. (Los nombres y los datos relevantes para ese evento están más allá del alcance del funcionamiento mecánico del sistema. Esto tiene más que ver con la forma en que se modela la lógica de negocios). Los actualizadores de su vista necesitan saber cómo traducir cada evento específico en un cambio de vista preciso (es decir, solo actualizar lo que ha cambiado).

¿Cómo definir un esqueleto coherente y escalable para los datos enviados? ¿Es este un mensaje de actualización del modelo o el mensaje "hubo un error con blahblahblah"?

Yo diría que esta es una pregunta grande y abierta que debería dividirse en varias otras preguntas y publicarse por separado.

Sin embargo, en general, su sistema de back-end debería crear y enviar eventos para eventos importantes para su negocio. Esos pueden provenir de fuentes externas o de actividad en el back-end.

¿Cómo no enviar datos sobre todo desde cualquier lugar del backend?

Use el patrón de publicación / suscripción . Cuando su SPA carga una nueva página que está interesada en recibir actualizaciones en tiempo real, la página debe suscribirse solo a los eventos que puede usar, y llamar a la lógica de actualización de vista a medida que ingresan esos eventos. Probablemente necesitará lógica de pub / sub en el servidor para reducir la carga de la red. Existen bibliotecas para Websocket pub / sub, pero no estoy seguro de cuáles están en el ecosistema de Rails.

¿Cómo reducir la duplicación de lógica de negocios tanto en el servidor como en el lado del cliente?

Parece que tiene que actualizar los datos de vista tanto en el cliente como en el servidor. Supongo que necesita los datos de vista del lado del servidor para tener una instantánea para iniciar el cliente en tiempo real. Dado que hay dos idiomas / plataformas involucrados (Ruby y Javascript), la lógica de actualización de la vista deberá escribirse en ambos. Además de transpirar (que tiene sus propios problemas), no veo una forma de evitarlo.

Punto técnico: la manipulación de datos (ver actualización) no es lógica empresarial. Si se refiere a la validación de casos de uso, eso parece inevitable ya que las validaciones del cliente son necesarias para una buena experiencia del usuario, pero en última instancia el servidor no puede confiar en él.


Así es como veo que tal cosa está bien estructurada.

Vistas del cliente:

  • Solicita una instantánea de vista y el último número de evento visto de la vista
    • Esto prepoblará la vista para que el cliente no tenga que construir desde cero.
    • Podría estar sobre HTTP GET para simplificar
  • Establece una conexión websocket y se suscribe a eventos específicos, a partir del último número de evento de la vista.
  • Recibe eventos a través de websocket y actualiza su vista según el tipo de evento / datos.

Comandos del cliente:

  • Solicitar cambio de datos (HTTP PUT / POST / DELETE)
    • La respuesta es solo éxito o falla + error
    • (Los eventos generados por el cambio vendrán a través de websocket y desencadenarán una actualización de vista).

El lado del servidor podría dividirse en varios componentes con responsabilidades limitadas. Uno que solo procesa las solicitudes entrantes y crea eventos. Otro podría administrar suscripciones de clientes, escuchar eventos (por ejemplo, en proceso) y enviar eventos apropiados a los suscriptores. Podría tener un tercero que escuche eventos y actualice las vistas del lado del servidor; tal vez esto incluso ocurra antes de que los suscriptores reciban los eventos.

Lo que he descrito es una forma de mensajería CQRS + , y una estrategia típica para abordar el tipo de problemas que enfrenta.

No incluí Event Sourcing en esta descripción, ya que no estoy seguro de si es algo que desea asumir o si lo necesita necesariamente. Pero es un patrón relacionado.

Kasey Speakman
fuente
He progresado mucho en el tema, y ​​los consejos que diste fueron muy útiles. Acepté la respuesta porque usé muchos de los consejos, incluso si no los usé todos. Voy a describir el camino que seguí en otra respuesta.
Philippe Durix
4

Después de algunos meses de trabajo en el back-end principalmente, he podido usar algunos de los consejos aquí para abordar los problemas que enfrentaba la plataforma.

El objetivo principal al repensar el backend era apegarse lo más posible a CRUD. Todas las acciones, mensajes y solicitudes dispersos por muchas rutas se reagruparon en recursos que se crean, actualizan, leen o eliminan . Parece obvio ahora, pero esta ha sido una forma muy difícil de pensar para aplicar con cuidado.

Después de que todo se ha organizado en recursos, he podido adjuntar mensajes en tiempo real a los modelos.

  • La creación desencadena un mensaje con un nuevo recurso;
  • Actualización activa un mensaje con solo los atributos actualizados (más el UUID);
  • La eliminación activa un mensaje de eliminación.

En la API Rest, todos los métodos de creación, actualización y eliminación generan una respuesta de solo cabeza, el código HTTP informa del éxito o el fracaso y los datos reales se envían a través de los sockets web.

En la parte frontal, cada recurso es manejado por un componente específico que los carga a través de HTTP en la inicialización, luego se suscribe a las actualizaciones y mantiene su estado a lo largo del tiempo. Las vistas luego se unen a estos componentes para mostrar recursos y realizar acciones en esos recursos a través de los mismos componentes.


Encontré que las lecturas de CQRS + Messaging and Event Sourcing son muy interesantes, pero sentí que era un poco complicado para mi problema y tal vez esté más adaptado a las aplicaciones intensivas donde ingresar datos en una base de datos centralizada es un lujo costoso. Pero definitivamente tendré en cuenta este enfoque.

En este caso, la aplicación tendrá pocos clientes concurrentes y tomé la decisión de confiar mucho en la base de datos. Los modelos más cambiantes se almacenan en Redis, en el que confío para manejar unos cientos de actualizaciones por segundo.

Philippe Durix
fuente