Usado
NodeJS, Socket.io
Problema
Imagina que hay 2 usuarios U1 y U2 , conectados a una aplicación a través de Socket.io. El algoritmo es el siguiente:
- U1 pierde completamente la conexión a Internet (por ejemplo, apaga Internet)
- U2 envía un mensaje a U1 .
- U1 aún no recibe el mensaje, porque Internet no funciona
- El servidor detecta la desconexión de U1 por tiempo de espera de latido
- U1 se vuelve a conectar a socket.io
- U1 nunca recibe el mensaje de U2 ; se pierde en el paso 4, supongo.
Explicación posible
Creo que entiendo por qué sucede:
- en el paso 4, el servidor mata la instancia de socket y la cola de mensajes a U1 también
- Además, en el paso 5, U1 y el servidor crean una nueva conexión (no se reutiliza), por lo que incluso si el mensaje todavía está en cola, la conexión anterior se pierde de todos modos.
Necesitas ayuda
¿Cómo puedo evitar este tipo de pérdida de datos? Tengo que usar Hearbeats, porque la gente no cuelga la aplicación para siempre. También debo dar la posibilidad de volver a conectarme, porque cuando implemento una nueva versión de la aplicación, no quiero tiempo de inactividad.
PD: Lo que llamo "mensaje" no es sólo un mensaje de texto que puedo almacenar en la base de datos, sino un valioso mensaje del sistema, cuya entrega debe garantizarse, o la interfaz de usuario se estropea.
¡Gracias!
Adición 1
Ya tengo un sistema de cuentas de usuario. Además, mi aplicación ya es compleja. Agregar estados fuera de línea / en línea no ayudará, porque ya tengo este tipo de cosas. El problema es diferente.
Consulte el paso 2. En este paso, técnicamente no podemos decir si U1 se desconecta , simplemente pierde la conexión, digamos durante 2 segundos, probablemente debido a una mala conexión a Internet. Entonces U2 le envía un mensaje, pero U1 no lo recibe porque Internet todavía está inactivo para él (paso 3). El paso 4 es necesario para detectar usuarios sin conexión, digamos, el tiempo de espera es de 60 segundos. Finalmente, en otros 10 segundos, la conexión a Internet para U1 se activa y se vuelve a conectar a socket.io. Pero el mensaje de U2 se pierde en el espacio porque el servidor U1 se desconectó por tiempo de espera.
Ese es el problema, no quiero entregar al 100%.
Solución
- Recopile un emisor (nombre de emisión y datos) en {} usuario, identificado por un ID de emisor aleatorio. Enviar emitir
- Confirme la emisión en el lado del cliente (envíe la emisión de vuelta al servidor con emitID)
- Si se confirma, elimina el objeto de {} identificado por emitID
- Si el usuario se volvió a conectar, marque {} para este usuario y recorra el paso ejecutando el Paso 1 para cada objeto en {}
- Cuando se desconecta o se conecta al ras {} para el usuario si es necesario
// Server
const pendingEmits = {};
socket.on('reconnection', () => resendAllPendingLimits);
socket.on('confirm', (emitID) => { delete(pendingEmits[emitID]); });
// Client
socket.on('something', () => {
socket.emit('confirm', emitID);
});
Solución 2 (un poco)
Añadido el 1 de febrero de 2020.
Si bien esta no es realmente una solución para Websockets, es posible que alguien la encuentre útil. Migramos de Websockets a SSE + Ajax. SSE le permite conectarse desde un cliente para mantener una conexión TCP persistente y recibir mensajes de un servidor en tiempo real. Para enviar mensajes de un cliente a un servidor, simplemente use Ajax. Hay desventajas como la latencia y la sobrecarga, pero SSE garantiza la confiabilidad porque es una conexión TCP.
Como usamos Express, usamos esta biblioteca para SSE https://github.com/dpskvn/express-sse , pero puede elegir la que más le convenga.
SSE no es compatible con IE y la mayoría de las versiones de Edge, por lo que necesitaría un polyfill: https://github.com/Yaffle/EventSource .
Respuestas:
Otros han insinuado esto en otras respuestas y comentarios, pero el problema principal es que Socket.IO es solo un mecanismo de entrega, y no puede depender solo de él para una entrega confiable. La única persona que sabe con certeza que un mensaje se ha entregado correctamente al cliente es el propio cliente . Para este tipo de sistema, recomendaría hacer las siguientes afirmaciones:
Por supuesto, dependiendo de las necesidades de su aplicación, puede ajustar partes de esto; por ejemplo, puede usar, digamos, una lista de Redis o un conjunto ordenado para los mensajes, y borrarlos si sabe con certeza que un cliente está activo. hasta la fecha.
Aquí hay un par de ejemplos:
Camino feliz :
Ruta sin conexión :
Si desea absolutamente una entrega garantizada, entonces es importante diseñar su sistema de tal manera que estar conectado realmente no importe, y que la entrega en tiempo real sea simplemente una ventaja ; esto casi siempre implica un almacén de datos de algún tipo. Como mencionó el usuario568109 en un comentario, existen sistemas de mensajería que abstraen el almacenamiento y la entrega de dichos mensajes, y puede valer la pena buscar una solución prediseñada de este tipo. (Es probable que aún tenga que escribir la integración de Socket.IO usted mismo).
Si no está interesado en almacenar los mensajes en la base de datos, es posible que pueda guardarlos en una matriz local; el servidor intenta enviar el mensaje a U1 y lo almacena en una lista de "mensajes pendientes" hasta que el cliente de U1 confirma que lo recibió. Si el cliente está desconectado, cuando regrese, puede decirle al servidor "Oye, me desconectaron, por favor envíame todo lo que me perdí" y el servidor puede iterar a través de esos mensajes.
Afortunadamente, Socket.IO proporciona un mecanismo que permite a un cliente "responder" a un mensaje que se parece a las devoluciones de llamada nativas de JS. Aquí hay un pseudocódigo:
// server pendingMessagesForSocket = []; function sendMessage(message) { pendingMessagesForSocket.push(message); socket.emit('message', message, function() { pendingMessagesForSocket.remove(message); } }; socket.on('reconnection', function(lastKnownMessage) { // you may want to make sure you resend them in order, or one at a time, etc. for (message in pendingMessagesForSocket since lastKnownMessage) { socket.emit('message', message, function() { pendingMessagesForSocket.remove(message); } } }); // client socket.on('connection', function() { if (previouslyConnected) { socket.emit('reconnection', lastKnownMessage); } else { // first connection; any further connections means we disconnected previouslyConnected = true; } }); socket.on('message', function(data, callback) { // Do something with `data` lastKnownMessage = data; callback(); // confirm we received the message });
Esto es bastante similar a la última sugerencia, simplemente sin un almacén de datos persistente.
También puede interesarle el concepto de abastecimiento de eventos .
fuente
La respuesta de Michelle es bastante acertada, pero hay algunas otras cosas importantes a considerar. La pregunta principal que debe hacerse es: "¿Hay alguna diferencia entre un usuario y un socket en mi aplicación?" Otra forma de preguntar es "¿Puede cada usuario que haya iniciado sesión tener más de 1 conexión de socket a la vez?"
En el mundo web, es probable que siempre exista la posibilidad de que un solo usuario tenga múltiples conexiones de socket, a menos que haya establecido específicamente algo que lo evite. El ejemplo más simple de esto es si un usuario tiene abiertas dos pestañas de la misma página. En estos casos, no le importa enviar un mensaje / evento al usuario humano solo una vez ... debe enviarlo a cada instancia de socket para ese usuario para que cada pestaña pueda ejecutar sus devoluciones de llamada para actualizar el estado de la interfaz de usuario. Tal vez esto no sea una preocupación para ciertas aplicaciones, pero mi instinto dice que lo sería para la mayoría. Si esto le preocupa, siga leyendo ...
Para resolver esto (asumiendo que está utilizando una base de datos como su almacenamiento persistente) necesitaría 3 tablas.
La tabla de usuarios es opcional si su aplicación no la requiere, pero el OP dijo que tienen una.
La otra cosa que debe definirse correctamente es "¿qué es una conexión de socket?", "¿Cuándo se crea una conexión de socket?", "¿Cuándo se reutiliza una conexión de socket?". El psudocódigo de Michelle hace que parezca que una conexión de socket se puede reutilizar. Con Socket.IO, NO PUEDEN reutilizarse. He visto ser la fuente de mucha confusión. Hay escenarios de la vida real en los que el ejemplo de Michelle tiene sentido. Pero tengo que imaginarme que esos escenarios son raros. Lo que realmente sucede es que cuando se pierde una conexión de socket, esa conexión, ID, etc. nunca se reutilizarán. Por lo tanto, cualquier mensaje marcado específicamente para ese socket nunca se entregará a nadie porque cuando el cliente que se había conectado originalmente, se vuelve a conectar, obtiene una conexión completamente nueva y una nueva ID. Esto significa que '
Entonces, para un ejemplo basado en la web, aquí estaría el conjunto de pasos que recomendaría:
Porque el último paso es complicado (al menos solía serlo, no he hecho nada de eso en mucho tiempo), y porque hay casos como pérdida de energía en los que el cliente se desconecta sin limpiar la fila del cliente y nunca lo intenta para volver a conectarse con esa misma fila de cliente, probablemente desee tener algo que se ejecute periódicamente para limpiar cualquier cliente obsoleto y filas de mensajes. O puede almacenar permanentemente todos los clientes y mensajes para siempre y marcar su estado de manera adecuada.
Entonces, para que quede claro, en los casos en que un usuario tiene dos pestañas abiertas, agregará dos mensajes idénticos a la tabla de mensajes, cada uno marcado para un cliente diferente porque su servidor necesita saber si cada cliente los recibió, no solo cada usuario.
fuente
Parece que ya tienes un sistema de cuentas de usuario. Usted sabe qué cuenta está en línea / fuera de línea, puede manejar el evento de conexión / desconexión:
Entonces, la solución es agregar mensajes en línea / fuera de línea y fuera de línea en la base de datos para cada usuario:
chatApp.onLogin(function (user) { user.readOfflineMessage(function (msgs) { user.sendOfflineMessage(msgs, function (err) { if (!err) user.clearOfflineMessage(); }); }) }); chatApp.onMessage(function (fromUser, toUser, msg) { if (user.isOnline()) { toUser.sendMessage(msg, function (err) { // alert CAN NOT SEND, RETRY? }); } else { toUser.addToOfflineQueue(msg); } })
fuente
Mire aquí: Manejar la recarga del navegador socket.io .
Creo que podrías usar la solución que se me ocurrió. Si lo modifica correctamente, debería funcionar como desee.
fuente
Lo que creo que quieres es tener un socket reutilizable para cada usuario, algo como:
Cliente:
socket.on("msg", function(){ socket.send("msg-conf"); });
Servidor:
// Add this socket property to all users, with your existing user system user.socket = { messages:[], io:null } user.send = function(msg){ // Call this method to send a message if(this.socket.io){ // this.io will be set to null when dissconnected // Wait For Confirmation that message was sent. var hasconf = false; this.socket.io.on("msg-conf", function(data){ // Expect the client to emit "msg-conf" hasconf = true; }); // send the message this.socket.io.send("msg", msg); // if connected, call socket.io's send method setTimeout(function(){ if(!hasconf){ this.socket = null; // If the client did not respond, mark them as offline. this.socket.messages.push(msg); // Add it to the queue } }, 60 * 1000); // Make sure this is the same as your timeout. } else { this.socket.messages.push(msg); // Otherwise, it's offline. Add it to the message queue } } user.flush = function(){ // Call this when user comes back online for(var msg in this.socket.messages){ // For every message in the queue, send it. this.send(msg); } } // Make Sure this runs whenever the user gets logged in/comes online user.onconnect = function(socket){ this.socket.io = socket; // Set the socket.io socket this.flush(); // Send all messages that are waiting } // Make sure this is called when the user disconnects/logs out user.disconnect = function(){ self.socket.io = null; // Set the socket to null, so any messages are queued not send. }
Luego, la cola de sockets se conserva entre desconexiones.
Asegúrese de que guarde la
socket
propiedad de cada usuario en la base de datos y haga que los métodos formen parte de su prototipo de usuario. La base de datos no importa, simplemente guárdela sin embargo ha estado guardando a sus usuarios.Esto evitará el problema mencionado en el complemento 1 al requerir una confirmación del cliente antes de marcar el mensaje como enviado. Si realmente lo desea, puede darle a cada mensaje una identificación y hacer que el cliente envíe la identificación del mensaje a
msg-conf
, luego verifíquelo.En este ejemplo,
user
es el usuario de la plantilla del que se copian todos los usuarios, o como el prototipo de usuario.Nota: esto no ha sido probado.
fuente
users.getUserByName("U2").send("Hi")
. Entonces, si U2 está en línea, socket.io de U2 no será nulo, por lo que se enviará el mensaje. Si el socket de U2 es nulo, se pondrá en cola hasta que U2 se conecte.this.socket.io
será no sernull
, y el servidor intentará entregar los mensajes.Como ya se escribió en otra respuesta, también creo que debería considerar el tiempo real como una ventaja: el sistema debería poder funcionar también sin tiempo real.
Estoy desarrollando un chat empresarial para una gran empresa (ios, android, web frontend y .net core + postGres backend) y después de haber desarrollado una forma para que websocket restablezca la conexión (a través de un uuid de socket) y reciba mensajes no entregados (almacenado en una cola) Entendí que había una solución mejor: resincronizar a través de la API de descanso.
Básicamente terminé usando websocket solo en tiempo real, con una etiqueta entera en cada mensaje en tiempo real (usuario en línea, mecanografiado, mensaje de chat, etc.) para monitorear los mensajes perdidos.
Cuando el cliente obtiene una identificación que no es monolítica (+1), entonces entiende que no está sincronizado, por lo que elimina todos los mensajes de socket y solicita una resincronización de todos sus observadores a través de la API REST.
De esta manera, podemos manejar muchas variaciones en el estado de la aplicación durante el período fuera de línea sin tener que analizar toneladas de mensajes websocket seguidos en la reconexión y estamos seguros de que estaremos sincronizados (porque la última fecha de sincronización se establece solo con la API REST , no del enchufe).
La única parte complicada es monitorear los mensajes en tiempo real desde el momento en que llama a la API de REST hasta el momento en que el servidor responde porque lo que se lee en la base de datos tarda en volver al cliente y, mientras tanto, pueden ocurrir variaciones, por lo que deben almacenarse en caché. y tuvo en cuenta.
Entraremos en producción en un par de meses, espero volver a dormir para entonces :)
fuente
He estado mirando estas cosas últimamente y creo que un camino diferente podría ser mejor.
Intente buscar en Azure Service bus, las preguntas y el tema para cuidar los estados fuera de línea. El mensaje espera a que el usuario regrese y luego recibe el mensaje.
Es un costo ejecutar una cola, pero es como $ 0.05 por millón de operaciones para una cola básica, por lo que el costo de desarrollo sería mayor por las horas de trabajo que necesita para escribir un sistema de cola. https://azure.microsoft.com/en-us/pricing/details/service-bus/
Y azure bus tiene bibliotecas y ejemplos para PHP, C #, Xarmin, Anjular, Java Script, etc.
Entonces, el servidor envía un mensaje y no necesita preocuparse por rastrearlos. El cliente puede usar el mensaje para enviar de vuelta también, ya que los medios pueden manejar el equilibrio de carga si es necesario.
fuente
Prueba esta lista de chat de emisión
io.on('connect', onConnect); function onConnect(socket){ // sending to the client socket.emit('hello', 'can you hear me?', 1, 2, 'abc'); // sending to all clients except sender socket.broadcast.emit('broadcast', 'hello friends!'); // sending to all clients in 'game' room except sender socket.to('game').emit('nice game', "let's play a game"); // sending to all clients in 'game1' and/or in 'game2' room, except sender socket.to('game1').to('game2').emit('nice game', "let's play a game (too)"); // sending to all clients in 'game' room, including sender io.in('game').emit('big-announcement', 'the game will start soon'); // sending to all clients in namespace 'myNamespace', including sender io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon'); // sending to individual socketid (private message) socket.to(<socketid>).emit('hey', 'I just met you'); // sending with acknowledgement socket.emit('question', 'do you think so?', function (answer) {}); // sending without compression socket.compress(false).emit('uncompressed', "that's rough"); // sending a message that might be dropped if the client is not ready to receive messages socket.volatile.emit('maybe', 'do you really need it?'); // sending to all clients on this node (when using multiple nodes) io.local.emit('hi', 'my lovely babies'); };
fuente