Надежность транспорта Websocket (потеря данных Socket.io при повторном подключении)
Б
NodeJS, Socket.io
Проблема
Представьте, что есть 2 пользователя U1 и U2, подключенные к приложению через Socket.io. Алгоритм следующий:
- U1 полностью теряет подключение к Интернету (например, отключает Интернет)
- U2 отправляет сообщение U1.
- U1 еще не получил сообщение, потому что Интернет отключен.
- Сервер обнаруживает разъединение U1 с помощью тайм-аута heartbeat
- U1 подключается к socket.io
- U1 никогда не получает сообщение от U2 - он потерян на шаге 4, я думаю.
Возможное объяснение
Думаю, я понимаю, почему это происходит:
- на шаге 4 Сервер убивает экземпляр сокета и очередь сообщений U1, а также
- Кроме того, на шаге 5 U1 и Сервер создать новое соединение (оно не используется повторно), поэтому даже если сообщение все еще поставлено в очередь, предыдущее соединение все равно потеряно.
Нужна помощь
Как я могу предотвратить такую потерю данных? Я должен использовать listenbeats, потому что я не нахожусь в приложении навсегда. Кроме того, я должен предоставить возможность повторно подключиться, потому что, когда я развертываю новую версию приложения, я хочу нулевое время простоя.
P.S. То, что я называю "сообщение", - это не просто текстовое сообщение, которое я могу хранить в базе данных, но ценное системное сообщение, доставка которого должна быть гарантирована, или пользовательский интерфейс затягивается.
Спасибо!
Дополнение 1
У меня уже есть система учетных записей пользователей. Более того, моя заявка уже сложна. Добавление автономных/онлайн-статусов не поможет, потому что у меня уже есть такие вещи. Проблема другая.
Выполните шаг 2. На этом шаге мы технически не можем сказать, что если U1 отправляется в автономном режиме, он просто теряет соединение, скажем, в течение 2 секунд, возможно, из-за плохого интернета. Таким образом, U2 отправляет ему сообщение, но U1 не получает его, потому что интернет все еще для него (шаг 3). Шаг 4 необходим для обнаружения автономных пользователей, скажем, тайм-аут составляет 60 секунд. В конце концов через 10 секунд подключится интернет-соединение для U1, и он снова подключится к socket.io. Но сообщение из U2 теряется в пространстве, потому что на сервере U1 был отключен таймаутом.
В этом проблема, я не получаю 100% доставки.
Решение
- Соберите излучение (введите имя и данные) в {} пользователь, идентифицированный случайным emitID. Отправить emit
- Подтвердить испускание на стороне клиента (отправить emit обратно на сервер с emitID)
- Если подтверждено - удалить объект из {}, идентифицированный emitID
- Если пользователь повторно подключен - проверьте {} для этого пользователя и пропустите его, выполнив Шаг 1 для каждого объекта в {}
- При отключенном или/или подключенном флажке {} для пользователя при необходимости
Ответы
Ответ 1
Другие намекают на это в других ответах и комментариях, но основная проблема заключается в том, что Socket.IO - это всего лишь механизм доставки, и вы не можете зависеть только от него для надежной доставки. Единственный человек, который точно знает, что сообщение успешно доставлено клиенту , - это сам клиент. Для такого рода систем я бы рекомендовал сделать следующие утверждения:
- Сообщения не отправляются напрямую клиентам; вместо этого они отправляются на сервер и сохраняются в каком-то хранилище данных.
- Клиенты несут ответственность за запрос "что я пропустил" при повторном подключении и запрошу сохраненные сообщения в хранилище данных, чтобы обновить их состояние.
- Если сообщение отправляется на сервер, когда клиент-получатель подключен, это сообщение будет отправлено клиенту в режиме реального времени.
Конечно, в зависимости от потребностей вашего приложения вы можете настроить части этого - например, вы можете использовать, скажем, список Redis или отсортированный набор для сообщений, и очистить их, если вы знаете, что клиент обновлен.
Вот несколько примеров:
Счастливый путь:
- U1 и U2 подключены к системе.
- U2 отправляет на сервер сообщение, которое должен получить U1.
- Сервер хранит сообщение в каком-то постоянном хранилище, маркируя его для U1 с некоторой меткой времени или последовательным идентификатором.
- Сервер отправляет сообщение в U1 через Socket.IO.
- Клиент U1 подтверждает (возможно, через обратный вызов Socket.IO), что он получил сообщение.
- Сервер удаляет сохраненное сообщение из хранилища данных.
Автономный путь:
- U1 теряет интернет-соединение.
- U2 отправляет на сервер сообщение, которое должен получить U1.
- Сервер хранит сообщение в каком-то постоянном хранилище, маркируя его для U1 с некоторой меткой времени или последовательным идентификатором.
- Сервер отправляет сообщение в U1 через Socket.IO.
- Клиент U1 не выполняет подтверждение получения, потому что он не работает.
- Возможно, U2 отправляет U1 еще несколько сообщений; все они сохраняются в хранилище данных таким же образом.
- Когда U1 снова подключается, он запрашивает сервер "Последнее сообщение, которое я видел, было X/у меня состояние X, что я пропустил".
- Сервер отправляет U1 все сообщения, которые он пропустил из хранилища данных, на основе запроса U1
- Клиент U1 подтверждает получение, и сервер удаляет эти сообщения из хранилища данных.
Если вам абсолютно нужна гарантированная доставка, важно создать свою систему таким образом, чтобы связать ее на самом деле не так, и что доставка в реальном времени - это просто бонус; это почти всегда связано с каким-то хранилищем данных. Как указано в комментарии пользователя568109, существуют системы обмена сообщениями, которые абстрагируют хранение и доставку указанных сообщений, и, возможно, стоит заглянуть в такое заранее построенное решение. (Вероятно, вам все равно придется записывать интеграцию 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
});
Это очень похоже на последнее предложение, просто без постоянного хранилища данных.
Вас также может заинтересовать концепция event sourcing.
Ответ 2
Кажется, что у вас уже есть система учетных записей пользователей. Вы знаете, какая учетная запись находится в режиме онлайн/офлайн, вы можете обрабатывать событие connect/disconnect:
Таким образом, решение есть, добавляет онлайн/оффлайн и автономные сообщения в базу данных для каждого пользователя:
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);
}
})
Ответ 3
Посмотрите здесь: Обработать перезагрузку браузера socket.io.
Я думаю, вы могли бы использовать решение, с которым я столкнулся. Если вы измените его правильно, он будет работать так, как вы хотите.
Ответ 4
Я думаю, что вам нужно иметь многоразовый сокет для каждого пользователя, например:
Клиент:
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 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 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
в базе данных и делает методы частью вашего прототипа пользователя. База данных не имеет значения, просто сохраните ее, однако вы сохраняете своих пользователей.
Это позволит избежать проблемы, упомянутой в Additon 1, требуя подтверждения от клиента перед тем, как пометить сообщение как отправленное. Если вы действительно этого захотите, вы можете дать каждому сообщению идентификатор и отправить клиенту идентификатор сообщения msg-conf
, а затем проверить его.
В этом примере user
является пользователем шаблона, с которого все пользователи скопированы или похожи на прототип пользователя.
Примечание: Это не было протестировано.
Ответ 5
Посмотрел на этот материал последним и подумал, что другой путь может быть лучше.
Попробуйте найти шину Azure Service, вопросы и темы позаботиться о состояниях автономной линии.
Сообщение ждет, пока пользователь вернется, а затем они получат сообщение.
Является ли стоимость запуска очереди, но она равна $0,05 за миллион операций для базовой очереди, поэтому стоимость dev будет больше от часов работы, необходимо написать систему очередей.
https://azure.microsoft.com/en-us/pricing/details/service-bus/
А шина azure имеет библиотеки и примеры для PHP, С#, Xarmin, Anjular, Java Script и т.д.
Итак, отправьте сообщение на сервер и не нужно беспокоиться о его отслеживании.
Клиент может использовать сообщение для отправки назад, так как средства могут выполнять балансировку нагрузки, если это необходимо.