NodeJS,Socket.io
想象一下,有2个用户 U1 & U2 ,通过Socket.io连接到应用。算法如下:
我想我明白为什么会这样:
如何防止此类数据丢失?我必须使用hearbeats,因为我没有人永远挂在应用程序中。此外,我仍然必须重新连接,因为当我部署新版本的应用程序时,我希望零停机时间。
P.S。我称之为“消息”的东西不仅仅是我可以存储在数据库中的文本消息,而是有价值的系统消息,必须保证交付,或者用户界面搞砸了。
谢谢!
我已经有了一个用户帐户系统。而且,我的申请已经很复杂了。添加离线/在线状态无济于事,因为我已经有了这种东西。问题不同了。
退房第二步。在这一步我们技术上不能说U1是否脱机,他只是输了连接让我说2秒,可能是因为网络不好。所以U2给他发了一条消息,但U1没有收到它,因为互联网仍在为他服务(步骤3)。需要步骤4来检测离线用户,假设超时为60秒。最后,在另一个10秒内,U1的互联网连接正常,他重新连接到socket.io。但来自U2的消息在空间中丢失,因为服务器U1在超时时断开连接。
这就是问题所在,我不是100%交付。
如果需要,当用户断开连接或/和连接时刷新{}
//服务器 const pendingEmits = {};
socket.on('reconnection',()=> resendAllPendingLimits); socket.on('confirm',(emitID)=> {delete(pendingEmits [emitID]);});
//客户端 socket.on('something',()=> { socket.emit('confirm',emitID); });
答案 0 :(得分:80)
其他人在其他答案和评论中暗示了这一点,但根本问题是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
});
这与上一个建议完全相似,只是没有持久数据存储。
您可能也对event sourcing的概念感兴趣。
答案 1 :(得分:1)
您似乎已拥有用户帐户系统。您知道哪个帐户在线/离线,您可以处理连接/断开事件:
因此,解决方案是在每个用户的数据库中添加在线/离线和离线消息:
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);
}
})
答案 2 :(得分:0)
请看这里:Handle browser reload socket.io。
我认为你可以使用我提出的解决方案。如果你正确地修改它,它应该按你的意愿工作。
答案 3 :(得分:0)
我认为你想要的是为每个用户提供一个可重复使用的套接字,例如:
客户端:
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
属性保存到数据库,并使这些方法成为用户原型的一部分。数据库无关紧要,只需保存即可保存用户。
这将通过在将消息标记为已发送之前要求客户端进行确认来避免Additon 1中提到的问题。如果你真的想,你可以给每条消息一个id并让客户端将消息ID发送给msg-conf
,然后检查它。
在此示例中,user
是从中复制所有用户的模板用户,或者与用户原型一样。
注意:尚未经过测试。
答案 4 :(得分:0)
后来一直在看这个东西,并认为不同的路径可能会更好。
尝试查看Azure Service总线,问题和主题处理离线状态。 消息等待用户返回然后他们收到消息。
运行队列的成本是基本队列的每百万次操作0.05美元,因此开发成本将更多来自编写排队系统所需的工时数。 https://azure.microsoft.com/en-us/pricing/details/service-bus/
azure总线包含PHP,C#,Xarmin,Anjular,Java Script等的库和示例。
因此服务器发送消息并且不必担心跟踪它们。 客户端也可以使用消息发回,因为可以根据需要处理负载平衡。
答案 5 :(得分:0)
尝试使用此发出聊天列表
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');
};
答案 6 :(得分:0)
Michelle的答案很正确,但是还有一些其他重要的事情需要考虑。要问自己的主要问题是:“用户与我的应用程序中的套接字之间是否有区别?”另一种询问方式是“每个登录用户可以一次拥有多个套接字连接吗?”
在网络世界中,单个用户可能总是有多个套接字连接的可能性,除非您专门放置了一些东西来防止这种情况。最简单的示例是,如果用户打开了同一页面的两个选项卡。在这些情况下,您不必担心仅一次向人类用户发送消息/事件……您需要将其发送到该用户的每个套接字实例,以便每个选项卡都可以运行其回调以更新ui状态。也许这对某些应用程序不是问题,但我的直言不讳地指出,这对于大多数应用程序来说都是如此。如果您对此感到担心,请继续阅读。...
要解决此问题(假设您使用数据库作为持久性存储),您将需要3个表。
如果您的应用不需要用户表,则该表是可选的,但是OP表示他们有一个。
需要正确定义的另一件事是“什么是套接字连接?”,“何时创建套接字连接?”,“何时重新使用套接字连接?”。 Michelle的伪代码使套接字连接看起来可以重用。使用Socket.IO,它们将无法重用。我已经看到了很多混乱的根源。在现实生活中,米歇尔的例子确实有意义。但是我必须想象那些情况很少见。真正发生的事情是当套接字连接丢失时,该连接,ID等将永远不会被重用。因此,专门为该套接字标记的任何消息都将永远不会传递给任何人,因为当最初连接的客户端重新连接时,它们将获得全新的连接和新的ID。这意味着您需要做一些事情来跨多个套接字连接来跟踪客户端(而不是套接字或用户)。
因此对于基于Web的示例,这是我建议的一组步骤:
因为最后一步很棘手(至少过去是这样,很长一段时间我都没有做过这样的事情),并且因为在某些情况下,例如断电时,客户端会断开连接而不会清理客户端行并且永远不要尝试与该相同的客户端行重新连接-您可能希望定期运行某些内容以清理所有过时的客户端和消息行。或者,您可以永久地永久存储所有客户端和消息,并适当地标记它们的状态。
因此要清楚一点,在一个用户打开两个选项卡的情况下,您将向消息表中添加两条相同的消息,每个消息都标记了一个不同的客户端,因为您的服务器需要知道每个客户端是否都收到了它们,而不仅仅是每个用户。
答案 7 :(得分:0)
正如已经在另一个答案中所写的那样,我还相信您应该将实时性视为一种奖励:系统应该也可以不实时地工作。
我正在为一家大型公司(ios,android,Web前端和.net core + postGres后端)开发企业聊天室,并为Websocket开发了一种重新建立连接(通过套接字uuid)的方法之后,收到未传递的消息(存储在队列中)我知道有一个更好的解决方案:通过rest API重新同步。
基本上,我最终只是通过实时使用websocket来结束的,每条实时消息(用户在线,打字员,聊天消息等)上都带有一个整数标签,用于监视丢失的消息。
当客户端获得的ID不是单片(+1)时,它就知道它不同步,因此它将丢弃所有套接字消息,并通过REST api请求重新同步其所有观察者。
通过这种方式,我们可以在离线期间处理应用程序状态的多种变化,而不必在重新连接时连续解析大量的websocket消息,并且我们肯定会被同步(因为上次同步日期仅由REST API,而不是来自套接字)。
唯一棘手的部分是监视从调用REST api到服务器回复的实时消息,因为从db读取的内容需要时间才能返回到客户端,同时可能发生变化,因此需要进行缓存并考虑在内。
我们将在几个月后投入生产, 我希望到那时再睡:)