Websocket传输可靠性(重新连接时Socket.io数据丢失)

时间:2013-12-19 15:14:23

标签: node.js socket.io

二手

NodeJS,Socket.io

问题

想象一下,有2个用户 U1 & U2 ,通过Socket.io连接到应用。算法如下:

  1. U1 完全失去互联网连接(例如,关闭互联网)
  2. U2 U1 发送消息。
  3. U1 尚未收到该邮件,因为互联网已关闭
  4. 服务器检测到 U1 因心跳超时而断开连接
  5. U1 重新连接到socket.io
  6. U1 从未收到来自 U2 的讯息 - 我猜测它会在第4步丢失。
  7. 可能的解释

    我想我明白为什么会这样:

    • on step 4 服务器将套接字实例和消息队列杀死为 U1 以及
    • 此外,在第5步 U1 服务器创建新连接(不会重复使用),因此即使邮件仍然排队,以前的连接仍会丢失。< / LI>

    需要帮助

    如何防止此类数据丢失?我必须使用hearbeats,因为我没有人永远挂在应用程序中。此外,我仍然必须重新连接,因为当我部署新版本的应用程序时,我希望零停机时间。

    P.S。我称之为“消息”的东西不仅仅是我可以存储在数据库中的文本消息,而是有价值的系统消息,必须保证交付,或者用户界面搞砸了。

    谢谢!


    加1

    我已经有了一个用户帐户系统。而且,我的申请已经很复杂了。添加离线/在线状态无济于事,因为我已经有了这种东西。问题不同了。

    退房第二步。在这一步我们技术上不能说U1是否脱机,他只是输了连接让我说2秒,可能是因为网络不好。所以U2给他发了一条消息,但U1没有收到它,因为互联网仍在为他服务(步骤3)。需要步骤4来检测离线用户,假设超时为60秒。最后,在另一个10秒内,U1的互联网连接正常,他重新连接到socket.io。但来自U2的消息在空间中丢失,因为服务器U1在超时时断开连接。

    这就是问题所在,我不是100%交付。


    解决方案

    1. 在{}用户中收集emit(发出名称和数据),由随机的emitID标识。发送发射
    2. 确认客户端的emit(发送回发送回具有emitID的服务器)
    3. 如果确认 - 从emitID
    4. 标识的{}中删除对象
    5. 如果用户重新连接 - 请检查此用户的{}并循环执行{}
    6. 中每个对象的步骤1
    7. 如果需要,当用户断开连接或/和连接时刷新{}

      //服务器 const pendingEmits = {};

      socket.on('reconnection',()=&gt; resendAllPendingLimits); socket.on('confirm',(emitID)=&gt; {delete(pendingEmits [emitID]);});

      //客户端 socket.on('something',()=&gt; {   socket.emit('confirm',emitID); });

8 个答案:

答案 0 :(得分:80)

其他人在其他答案和评论中暗示了这一点,但根本问题是Socket.IO只是一种传递机制,而不能单独依赖它来实现可靠传递。唯一确定消息已成功传递给客户的人是客户本身。对于这种系统,我建议做出以下断言:

  1. 讯息不会直接发送给客户;相反,它们被发送到服务器并存储在某种数据存储中。
  2. 客户端负责在重新连接时询问“我错过了什么”,并将查询数据存储中存储的消息以更新其状态。
  3. 如果在连接收件人客户端时将消息发送到服务器该消息将实时发送给客户端。
  4. 当然,根据您的应用程序的需要,您可以调整其中的一部分 - 例如,您可以使用Redis列表或消息的排序集,如果您知道事实,则清除它们客户是最新的。


    以下是几个例子:

    快乐路径

    • U1和U2都连接到系统。
    • U2向服务器发送U1应该收到的消息。
    • 服务器将消息存储在某种持久性存储中,使用某种时间戳或顺序ID将其标记为U1。
    • 服务器通过Socket.IO将消息发送给U1。
    • U1的客户端确认(可能是通过Socket.IO回调)它收到了消息。
    • 服务器从数据存储中删除持久消息。

    离线路径

    • U1失去了互联网连接。
    • U2向服务器发送U1应该收到的消息。
    • 服务器将消息存储在某种持久性存储中,使用某种时间戳或顺序ID将其标记为U1。
    • 服务器通过Socket.IO将消息发送给U1。
    • U1的客户 确认收货,因为他们已离线。
    • 也许U2向U1发送更多消息;它们都以相同的方式存储在数据存储中。
    • 当U1重新连接时,它会询问服务器“我看到的最后一条消息是X /我有X状态,我错过了什么。”
    • 服务器根据U1的请求向U1发送它从数据存储中遗漏的所有消息
    • U1的客户确认收到,服务器从数据存储中删除这些消息。

    如果你绝对想要保证交付,那么设计你的系统是非常重要的,因为连接实际上并不重要,实时交付只是奖励;这几乎总是涉及某种数据存储。正如评论中提到的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个表。

  1. 用户-与真实的人一对一地
  2. clients-代表一个“制表符”,表示可以与套接字服务器建立单一连接。 (任何“用户”可能有多个)
  3. 消息-需要发送给客户端的消息(而不是需要发送给用户或套接字的消息)

如果您的应用不需要用户表,则该表是可选的,但是OP表示他们有一个。

需要正确定义的另一件事是“什么是套接字连接?”,“何时创建套接字连接?”,“何时重新使用套接字连接?”。 Michelle的伪代码使套接字连接看起来可以重用。使用Socket.IO,它们将无法重用。我已经看到了很多混乱的根源。在现实生活中,米歇尔的例子确实有意义。但是我必须想象那些情况很少见。真正发生的事情是当套接字连接丢失时,该连接,ID等将永远不会被重用。因此,专门为该套接字标记的任何消息都将永远不会传递给任何人,因为当最初连接的客户端重新连接时,它们将获得全新的连接和新的ID。这意味着您需要做一些事情来跨多个套接字连接来跟踪客户端(而不是套接字或用户)。

因此对于基于Web的示例,这是我建议的一组步骤:

  • 当用户加载有可能创建套接字连接的客户端(通常是单个网页)时,请在客户端数据库中添加一行链接到其用户ID。
  • 当用户确实确实连接到套接字服务器时,将客户端ID与连接请求一起传递给服务器。
  • 服务器应验证是否允许用户连接,并且clients表中的client行可用于连接,并相应地允许/拒绝。
  • 使用Socket.IO生成的套接字ID更新客户端行。
  • 发送消息表中与客户端ID连接的所有项目。初始连接上没有任何连接,但是如果这是来自尝试重新连接的客户端,则可能有一些连接。
  • 每当需要将消息发送到该套接字时,请在消息表中添加一行,该行链接到您生成的客户端ID(而不是套接字ID)。
  • 尝试发出消息并通过确认监听客户端。
  • 获得确认后,从邮件表中删除该项目。
  • 您可能希望在客户端创建一些逻辑,以丢弃从服务器发送的重复消息,因为正如某些人指出的那样,这在技术上是可能的。
  • 然后,当客户端从套接字服务器断开连接(有意或通过错误)时,请勿删除客户端行,最多只需清除套接字ID。这是因为同一客户端可以尝试重新连接。
  • 当客户端尝试重新连接时,发送与原始连接尝试发送的客户端ID相同的客户端ID。服务器将像初始连接一样查看此内容。
  • 当客户端被销毁(用户关闭选项卡或导航离开)时,这是您删除客户端行和该客户端的所有消息的时候。此步骤可能会有些棘手。

因为最后一步很棘手(至少过去是这样,很长一段时间我都没有做过这样的事情),并且因为在某些情况下,例如断电时,客户端会断开连接而不会清理客户端行并且永远不要尝试与该相同的客户端行重新连接-您可能希望定期运行某些内容以清理所有过时的客户端和消息行。或者,您可以永久地永久存储所有客户端和消息,并适当地标记它们的状态。

因此要清楚一点,在一个用户打开两个选项卡的情况下,您将向消息表中添加两条相同的消息,每个消息都标记了一个不同的客户端,因为您的服务器需要知道每个客户端是否都收到了它们,而不仅仅是每个用户。

答案 7 :(得分:0)

正如已经在另一个答案中所写的那样,我还相信您应该将实时性视为一种奖励:系统应该也可以不实时地工作。

我正在为一家大型公司(ios,android,Web前端和.net core + postGres后端)开发企业聊天室,并为Websocket开发了一种重新建立连接(通过套接字uuid)的方法之后,收到未传递的消息(存储在队列中)我知道有一个更好的解决方案:通过rest API重新同步。

基本上,我最终只是通过实时使用websocket来结束的,每条实时消息(用户在线,打字员,聊天消息等)上都带有一个整数标签,用于监视丢失的消息。

当客户端获得的ID不是单片(+1)时,它就知道它不同步,因此它将丢弃所有套接字消息,并通过REST api请求重新同步其所有观察者。

通过这种方式,我们可以在离线期间处理应用程序状态的多种变化,而不必在重新连接时连续解析大量的websocket消息,并且我们肯定会被同步(因为上次同步日期仅由REST API,而不是来自套接字)。

唯一棘手的部分是监视从调用REST api到服务器回复的实时消息,因为从db读取的内容需要时间才能返回到客户端,同时可能发生变化,因此需要进行缓存并考虑在内。

我们将在几个月后投入生产, 我希望到那时再睡:)