习惯于

NodeJS,Socket.木卫一

问题

假设有两个用户U1&U2,通过插座连接到应用程序.伊奥.算法如下所示:

  1. U1完全失go 互联网连接(例如关闭互联网)
  2. U2U1发送消息.
  3. U1尚未收到该消息,因为互联网已关闭
  4. Server通过心跳超时检测U1断开
  5. U1重新连接到插座.木卫一
  6. U1永远不会收到U2的信息——我想它在第4步中丢失了.

可能的解释

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

  • 在步骤4,Server也将套接字实例和消息队列杀死到U1
  • 此外,在步骤5、U1Server中,创建新连接(它不会被重用),所以即使消息仍在排队,之前的连接也会丢失.

需要帮助吗

如何防止这种数据丢失?我必须使用hearbeats,因为我不会让人们永远挂在应用程序中.此外,我还必须提供重新连接的可能性,因为当我部署新版本的应用程序时,我希望零停机时间.

另外,我称之为"消息"的东西不仅仅是我可以存储在数据库中的文本消息,而是有价值的系统消息,必须保证传递,否则UI会出错.

谢谢


增补1

我已经有了一个用户帐户系统.此外,我的应用程序已经很复杂了.添加离线/在线状态不会有帮助,因为我已经有了这种东西.问题不一样.

看看第二步.在这一步中,我们技术上是cannot say if U1 goes offline,他只是失go 了连接,比如说2秒钟,可能是因为糟糕的互联网.所以U2向他发送了一条消息,但U1没有收到,因为他仍然无法上网(第3步).第4步需要检测离线用户,比如说,超时时间为60秒.最后,再过10秒钟,U1的互联网连接就接通了,他重新连接到了插座上.伊奥.但来自U2的消息在空间中丢失,因为服务器U1上的连接已被超时断开.

这就是问题所在,我不想100%交货.


解决方案

  1. 在{}用户中收集一个emit(emit name and data),由随机emitID标识.emits
  2. 确认客户端的emit(使用emitID将emit发送回服务器)
  3. 如果确认-从emitID标识的{}中删除对象
  4. 如果用户重新连接-判断该用户的{},并对{}中的每个对象执行步骤1
  5. 断开或/和连接时,如有必要,为用户刷新{}
// Server
const pendingEmits = {};

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

// Client
socket.on('something', () => {
    socket.emit('confirm', emitID);
});

解决方案 2 (kinda)

Added 1 Feb 2020.

虽然这并不是WebSocket的真正解决方案,但有些人可能仍然觉得它很方便.我们从WebSocket迁移到SSE+Ajax.SSE允许您从客户端连接,以保持持久的TCP连接,并实时接收来自服务器的消息.要将消息从客户端发送到服务器,只需使用Ajax.虽然存在延迟和开销等缺点,但SSE保证了可靠性,因为它是TCP连接.

由于我们使用Express,我们使用SSE https://github.com/dpskvn/express-sse的这个库,但您可以 Select 适合您的库.

IE和大多数Edge版本都不支持SSE,因此需要polyfill:https://github.com/Yaffle/EventSource.

推荐答案

其他人在其他回答和 comments 中也暗示了这一点,但根本问题在于插座.IO只是一种传递机制,你需要依靠它来可靠地传递信息.唯一确定消息已成功交付给客户端is the client itself的人.对于这种系统,我建议您做出以下断言:

  1. 消息不会直接发送到客户端;相反,它们被发送到服务器并存储在某种数据存储中.
  2. 客户端负责在重新连接时询问"我错过了什么",并将查询数据存储中存储的消息以更新其状态.
  3. 如果消息被发送到服务器while the recipient client is connected,,则该消息将被实时发送到客户端.

当然,根据应用程序的需要,您可以调整其中的一些部分——例如,您可以使用Redis列表或消息排序集,如果您知道客户机是最新的,则可以清除它们.


以下是几个例子:

Happy path:

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

Offline path:

  • U1失go 了互联网连接.
  • U2向服务器发送U1应该接收的消息.
  • 服务器将消息存储在某种持久性存储中,用某种时间戳或顺序ID将其标记为U1.
  • 服务器通过套接字将消息发送到U1.伊奥.
  • U1的客户端does not确认接收,因为它们处于脱机状态.
  • 也许U2会向U1发送更多信息;它们都以相同的方式存储在数据存储中.
  • 当U1重新连接时,它会询问服务器"我看到的最后一条消息是X/I have state X,我错过了什么."
  • 服务器根据U1的请求向U1发送它从数据存储中丢失的所有消息
  • U1的客户端确认接收,服务器从数据存储中删除这些消息.

如果你绝对想要有保证的交付,那么重要的是设计你的系统,让连接实际上并不重要,而实时交付只是一个bonus;这几乎总是涉及某种类型的数据存储.正如user568109在一篇 comments 中提到的,有一些消息传递系统可以抽象出所述消息的存储和传递,这可能值得研究这种预构建的解决方案.(您可能仍然需要自己编写Socket.IO集成.)

如果您对将消息存储在数据库中不感兴趣,那么可以将它们存储在本地数组中;服务器try 向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的概念感兴趣.

Node.js相关问答推荐

如何从puppeteer的page. evaluate()中获取流数据?(node.js)

node 模块错误:类型参数OT具有循环约束''

在 TypeScript 中正确键入 MongoDB find 方法

如果我在父文件夹中运行,子进程生成不起作用

仅在 vue 脚本未退出的情况下使用 docker 时出现错误

如何在带有 JS 的 Nodejs 中使用没有 Async 方法的 Await

如何删除mongodb中嵌套数组中所有出现的数组元素

MERN 堆栈项目中的 React [create-react-app] 正在提供依赖项

等到文件上传完成的有效方法(mongoose )

yarn.lock 和 npm 的 package-lock 有什么区别?

使用 WebSockets 有服务器成本吗?

如何将使用 Gulp 的 node 部署到 heroku

node/express:使用 Forever 连续运行脚本时设置 NODE_ENV

如何在客户端使用 node.js 模块系统

如何运行用 TypeScript 编写的 Mocha 测试?

Node.js 与 .net 中的异步/等待

处理快速异步中间件中的错误

使用 node.js 循环 JSON

从 node.js 连接到 mongodb 时出现 ECONNREFUSED 错误

Puppeteer 等待所有图像加载然后截图