Используемый
NodeJS, Socket.io
Проблема
Представьте, что есть 2 пользователя U1 и U2 , подключенных к приложению через Socket.io. Алгоритм следующий:
- U1 полностью теряет подключение к Интернету (например, отключает Интернет)
- U2 отправляет сообщение U1 .
- U1 еще не получает сообщение, потому что Интернет не работает
- Сервер обнаруживает отключение U1 по таймауту пульса
- U1 повторно подключается к socket.io
- U1 никогда не получает сообщение от U2 - я думаю, оно потеряно на шаге 4.
Возможное объяснение
Думаю, я понимаю, почему это происходит:
- на шаге 4 сервер убивает экземпляр сокета и очередь сообщений для U1 , а
- Более того, на шаге 5 U1 и сервер создают новое соединение (оно не используется повторно), поэтому, даже если сообщение все еще стоит в очереди, предыдущее соединение все равно теряется.
Нужна помощь
Как я могу предотвратить такую потерю данных? Я должен использовать ритм, потому что я не заставляю людей зависать в приложении навсегда. Кроме того, я должен предоставить возможность повторного подключения, потому что, когда я развертываю новую версию приложения, я хочу, чтобы время простоя было нулевым.
PS То, что я называю «сообщением», - это не просто текстовое сообщение, которое я могу сохранить в базе данных, но ценное системное сообщение, доставка которого должна быть гарантирована, иначе пользовательский интерфейс испортится.
Благодаря!
Дополнение 1
У меня уже есть система учетных записей пользователей. Более того, мое приложение уже сложное. Добавление статусов офлайн / онлайн не поможет, потому что у меня уже есть такие вещи. Проблема в другом.
Проверьте шаг 2. На этом шаге мы технически не можем сказать, перейдет ли U1 в автономный режим. , он просто теряет соединение, скажем, на 2 секунды, вероятно, из-за плохого интернета. Итак, U2 отправляет ему сообщение, но U1 не получает его, потому что для него все еще не работает Интернет (шаг 3). Шаг 4 необходим для обнаружения офлайн-пользователей, допустим, таймаут составляет 60 секунд. В конце концов, еще через 10 секунд интернет-соединение для U1 установлено, и он снова подключается к socket.io. Но сообщение от U2 потеряно в пространстве, потому что сервер U1 был отключен по таймауту.
Вот в чем проблема, мне не нужна 100% доставка.
Решение
- Соберите эмитент (имя и данные эмитента) у пользователя {}, идентифицированного случайным emitID. Отправить эмитент
- Подтвердите эмитент на стороне клиента (отправьте эмитент обратно на сервер с emitID)
- Если подтверждено - удалить объект из {}, идентифицированный emitID
- Если пользователь подключился повторно - проверьте {} для этого пользователя и прокрутите его, выполнив шаг 1 для каждого объекта в {}
- При отключении и / или подключении промывка {} для пользователя при необходимости
// Server
const pendingEmits = {};
socket.on('reconnection', () => resendAllPendingLimits);
socket.on('confirm', (emitID) => { delete(pendingEmits[emitID]); });
// Client
socket.on('something', () => {
socket.emit('confirm', emitID);
});
Решение 2 (вроде)
Добавлено 1 фев 2020.
Хотя на самом деле это не решение для Websockets, кому-то оно может пригодиться. Мы перешли с Websockets на SSE + Ajax. SSE позволяет подключаться от клиента, чтобы поддерживать постоянное TCP-соединение и получать сообщения от сервера в реальном времени. Чтобы отправлять сообщения от клиента на сервер - просто используйте Ajax. Есть недостатки, такие как задержка и накладные расходы, но SSE гарантирует надежность, потому что это TCP-соединение.
Поскольку мы используем Express, мы используем эту библиотеку для SSE https://github.com/dpskvn/express-sse , но вы можете выбрать ту, которая вам подходит.
SSE не поддерживается в IE и большинстве версий Edge, поэтому вам понадобится полифил: https://github.com/Yaffle/EventSource .
Ответы:
Другие намекали на это в других ответах и комментариях, но основная проблема заключается в том, что Socket.IO - это всего лишь механизм доставки, и вы не можете полагаться только на него для надежной доставки. Единственный человек, который точно знает, что сообщение было успешно доставлено клиенту, - это сам клиент . Для такой системы я бы рекомендовал сделать следующие утверждения:
Конечно, в зависимости от потребностей вашего приложения вы можете настроить отдельные части этого - например, вы можете использовать, скажем, список Redis или отсортированный набор для сообщений и очистить их, если вы точно знаете, что клиент работает. на свидание.
Вот пара примеров:
Счастливый путь :
Автономный путь :
Если вы абсолютно хотите гарантированную доставку, то важно спроектировать вашу систему таким образом, чтобы подключение к сети не имело значения, а доставка в реальном времени была просто бонусом ; это почти всегда связано с каким-либо хранилищем данных. Как упоминал user568109 в комментарии, существуют системы обмена сообщениями, которые абстрагируются от хранения и доставки указанных сообщений, и, возможно, стоит изучить такое готовое решение. (Скорее всего, вам все равно придется самостоятельно написать интеграцию Socket.IO.)
Если вы не заинтересованы в хранении сообщений в базе данных, возможно, вы сможете сохранить их в локальном массиве; сервер пытается отправить сообщение U1 и сохраняет его в списке «ожидающих сообщений», пока клиент U1 не подтвердит, что он его получил. Если клиент отключен, то, когда он возвращается, он может сказать серверу: «Привет, я был отключен, пришлите мне все, что я пропустил», и сервер может перебирать эти сообщения.
К счастью, Socket.IO предоставляет механизм, который позволяет клиенту «отвечать» на сообщение, которое выглядит как нативные обратные вызовы JS. Вот какой-то псевдокод:
// 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 });
Это очень похоже на последнее предложение, просто без постоянного хранилища данных.
Вас также может заинтересовать концепция поиска событий .
источник
Ответ Мишель довольно точен, но есть еще несколько важных моментов, которые следует учитывать. Главный вопрос, который стоит задать себе: «Есть ли разница между пользователем и сокетом в моем приложении?» Другой способ спросить: «Может ли каждый вошедший в систему пользователь иметь более одного сокета одновременно?»
В веб-мире, вероятно, всегда существует вероятность того, что один пользователь имеет несколько подключений к сокетам, если вы специально не добавили что-то, что предотвращает это. Самый простой пример - если у пользователя открыты две вкладки одной и той же страницы. В этих случаях вам не нужно отправлять сообщение / событие пользователю-человеку только один раз ... вам нужно отправить его в каждый экземпляр сокета для этого пользователя, чтобы каждая вкладка могла запускать свои обратные вызовы для обновления состояния пользовательского интерфейса. Может быть, это не проблема для некоторых приложений, но моя интуиция подсказывает, что это будет для большинства. Если вас это беспокоит, читайте дальше ....
Чтобы решить эту проблему (при условии, что вы используете базу данных в качестве постоянного хранилища), вам понадобятся 3 таблицы.
Таблица пользователей является необязательной, если вашему приложению она не требуется, но OP сказал, что она у них есть.
Другой вопрос, который необходимо правильно определить, - это «что такое сокет-соединение?», «Когда создается сокет-соединение?», «Когда оно повторно используется?». Псудокод Мишель создает впечатление, что соединение через сокет можно использовать повторно. С Socket.IO их НЕЛЬЗЯ использовать повторно. Я видел источник большого замешательства. Есть сценарии из реальной жизни, в которых пример Мишель имеет смысл. Но я должен представить, что такие сценарии редки. Что действительно происходит, так это когда соединение с сокетом потеряно, это соединение, идентификатор и т. Д. Никогда не будут повторно использованы. Таким образом, любые сообщения, специально отмеченные для этого сокета, никогда не будут доставлены кому-либо, потому что, когда клиент, который изначально подключился, повторно подключается, он получает совершенно новое соединение и новый идентификатор. Это значит это '
Итак, для веб-примера я бы рекомендовал следующие шаги:
Потому что последний шаг сложен (по крайней мере, раньше я не делал ничего подобного в течение долгого времени), и потому что есть случаи, такие как потеря мощности, когда клиент отключается, не очищая клиентскую строку, и никогда не пытается для повторного подключения к той же клиентской строке - вы, вероятно, захотите иметь что-то, что периодически запускается для очистки устаревших клиентских строк и строк сообщений. Или вы можете просто навсегда сохранить всех клиентов и сообщения навсегда и просто пометить их состояние соответствующим образом.
Итак, для ясности: в случаях, когда у одного пользователя открыты две вкладки, вы будете добавлять два идентичных сообщения в таблицу сообщений, каждое из которых помечено для другого клиента, потому что вашему серверу необходимо знать, получил ли их каждый клиент, а не только каждый пользователь.
источник
Похоже, у вас уже есть система учетных записей пользователей. Вы знаете, какая учетная запись находится в сети / в автономном режиме, вы можете обрабатывать событие подключения / отключения:
Итак, решение состоит в том, чтобы добавить онлайн / офлайн и офлайн сообщения в базу данных для каждого пользователя:
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); } })
источник
Посмотрите здесь: Обработка перезагрузки браузера socket.io .
Я думаю, вы могли бы использовать решение, которое я придумал. Если вы измените его правильно, он должен работать так, как вы хотите.
источник
Я думаю, вам нужно иметь многоразовый сокет для каждого пользователя, например:
Клиент:
socket.on("msg", function(){ socket.send("msg-conf"); });
Сервер:
// 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. }
Тогда очередь сокетов сохраняется между отключениями.
Убедитесь, что он сохраняет каждое
socket
свойство пользователя в базе данных и делает методы частью вашего пользовательского прототипа. База данных не имеет значения, просто сохраните ее, как бы вы ни сохраняли своих пользователей.Это позволит избежать проблемы, упомянутой в Дополнении 1, за счет запроса подтверждения от клиента перед пометкой сообщения как отправленного. Если вы действительно хотите, вы можете присвоить каждому сообщению идентификатор и попросить клиента отправить идентификатор сообщения
msg-conf
, а затем проверить его.В этом примере
user
это пользователь шаблона, из которого скопированы все пользователи, или аналогичный пользовательскому прототипу.Примечание: это не было проверено.
источник
users.getUserByName("U2").send("Hi")
. Затем, если U2 находится в сети, socket.io U2 не будет нулевым, поэтому сообщение будет отправлено. Если сокет U2 нулевой, он будет поставлен в очередь до тех пор, пока U2 не перейдет в режим онлайн.this.socket.io
будет не бытьnull
, и сервер будет пытаться доставить сообщения.Как уже было написано в другом ответе, я также считаю, что вы должны смотреть на реальное время как на бонус: система должна иметь возможность работать без реального времени.
Я разрабатываю корпоративный чат для крупной компании (ios, android, веб-интерфейс и .net core + postGres backend), и после разработки способа для веб-сокета восстановить соединение (через сокет uuid) и получить недоставленные сообщения (хранится в очереди) Я понял, что есть лучшее решение: повторная синхронизация через rest API.
В основном я закончил тем, что использовал websocket только для реального времени, с целочисленным тегом в каждом сообщении в реальном времени (пользователь онлайн, печатники, сообщение чата и т. Д.) Для мониторинга потерянных сообщений.
Когда клиент получает идентификатор, который не является монолитным (+1), он понимает, что он не синхронизирован, поэтому он отбрасывает все сообщения сокета и запрашивает повторную синхронизацию всех своих наблюдателей через REST api.
Таким образом, мы можем обрабатывать множество вариантов состояния приложения во время автономного периода без необходимости анализировать тонны сообщений веб-сокета подряд при повторном подключении, и мы обязательно будем синхронизированы (поскольку последняя дата синхронизации устанавливается только REST api , а не из розетки).
Единственная сложная часть - это мониторинг сообщений в реальном времени с момента вызова REST api до момента ответа сервера, потому что то, что читается из базы данных, требует времени, чтобы вернуться к клиенту, а в то же время могут происходить изменения, поэтому их нужно кэшировать и учел.
Мы идем в производство через пару месяцев, надеюсь к тому времени снова поспим :)
источник
Недавно смотрел на этот материал и думал, что другой путь может быть лучше.
Попробуйте посмотреть на служебную шину Azure, вопросы и тему, чтобы позаботиться о состояниях офлайн. Сообщение ожидает, пока пользователь вернется, а затем они получают сообщение.
Это стоимость запуска очереди, но это примерно 0,05 доллара за миллион операций для базовой очереди, поэтому затраты на разработку будут больше из-за часов работы, необходимых для написания системы очередей. https://azure.microsoft.com/en-us/pricing/details/service-bus/
А в azure bus есть библиотеки и примеры для PHP, C #, Xarmin, Anjular, Java Script и т. Д.
Таким образом, сервер отправляет сообщение, и ему не нужно беспокоиться об их отслеживании. Клиент может использовать сообщение для обратной отправки, а также средство для балансировки нагрузки, если это необходимо.
источник
Попробуйте этот список чата
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'); };
источник