Nodejs中TCP(MQTT等)在断网的超时处理

1. 问题

在做 NodeJS 开发的过程中,碰到一个棘手的问题:已建立好的 TCP 连接,在网络异常情况下(拔网线),客户端无法及时的响应(close 或 error),这会影响到业务应用的。

查了不少资料,从 Linux 网络模型到 Nodejs 手册再到内核,一步步走来,算是找到了解决问题的方法,这里总结一下。

2. 解决思路

2.1. Linux TCP 参数

2.1.1. TCP keepalive 机制

在网络上能找到大量的有关 TCP keepalive 机制,通过配置 SO_KEEPALIVE、TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT 四个内核变量的值,可以检测是否存在异常的网络情况。

使用 SO_KEEPALIVE 将会检测对方主机是否崩溃,避免服务器永远阻塞于 TCP 连接的输入。设置该选项后,如果 2 小时内在此套接口的任一方向都没有数据交换,TCP 就自动给对方发一个 keepalive probe。此时会出现以下三种情况:

a. 对方接收一切正常:以期望的 ACK 响应。2 小时后,TCP 将发出另一个 keepalive probe。

b. 对方已崩溃且已重新启动:以 RST 响应。套接口的待处理错误被置为 ECONNRESET,套接口本身则被关闭。

c. 对方无任何响应:源自 berkeley 的 TCP 发送另外 8 个探测分节,相隔 75 秒一个,试图得到一个响应。在发出第一个探测分节 11 分钟 15 秒后若仍无响应就放弃。套接口的待处理错误被置为 ETIMEOUT,套接口本身则被关闭。

在路径 /proc/sys/net/ipv4/下 可以看到 tcp_keepalive_intvl、tcp_keepalive_probes、tcp_keepalive_time 三个文件,分别对应:两次 KeepAlive 探测间的时间间隔(75)、判定断开前的 KeepAlive 探测次数(9)、发起 KeepAlive 探测的定时器时间(7200)。

2.1.2. TCP_USER_TIMEOUT

TCP_USER_TIMEOUT 选项是 TCP 层的 socket 选项,选项接受 unsigned int 类型的值。值为数据包被发送后未接收到 ACK 确认的最大时长,以毫秒为单位,例如设置为 10000 时,代表如果发送出去的数据包在十秒内未收到 ACK 确认,则下一次调用 send 或者 recv,则函数会返回-1,errno 设置为 ETIMEOUT,代表 connection timeout。

2.2. 使用 NodeJS 接口测试,只适用于没有数据发送的时候,但有数据发送则不行

在 NodeJS 中,有一个函数接口 socket.setKeepAlive,可以设置 SO_KEEPALIVE、TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT 四个内核变量

SO_KEEPALIVE=1
TCP_KEEPIDLE=initialDelay
TCP_KEEPCNT=10
TCP_KEEPINTVL=1

在示例代码中实验,未成功响应到网线拔出,示例代码如下:

//client.js
var net = require('net');

let handle;
const { setInterval } = require('timers');
var client = net.connect({ port: 8888, host: '192.168.1.1' }, function () {
  client.setKeepAlive(true, 15000);
  client.name = '客户机1';
  let i = 0;
  client.on('close', (error) => {
    console.log('close:', error);
    clearInterval(handle);
    client.end();
  });
  client.on('connect', (error) => {
  });
  client.on('data', function (data) {
    console.log('recv data:', data.toString());
  });
  client.on('drain', function (data) {
    console.log('drain:', data.toString());
  });
  client.on('end', (error) => {
    console.log('end:', error);
  });
  client.on('error', (error) => {
    console.log('error:', new Date(Date.now()).toString());
  });
  client.on('lookup', (error) => {
    console.log('lookup:', error);
  });
  client.on('ready', (error) => {
    console.log('ready:', new Date(Date.now()).toString());
    handle = setInterval(() => {
      console.log('write data:', 'asdf');
      client.write('asdf')
    }, 1000);
  });
  client.setTimeout(3000);
  client.on('timeout', () => {
    console.log('socket timeout');
    client.end();
  });
});

这个只适用于没有数据发送的时候,但有数据发送则不行,继续查资料。

2.3. 从网上找相关资料,出现关键信息

https://blog.csdn.net/See_mood/article/details/61921397?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0.pc_relevant_antiscanv2&spm=1001.2101.3001.4242.1&utm_relevant_index=3

在实验的过程中发现,如果发送方发送的数据包没有收到接收方回复的 ACK 数据包,则 TCP Keep-alive 机制就不会被启动,而 TCP 会启动超时重传机制,这样就使得 TCP Keep-alive 机制在未收到 ACK 包时失效。在查阅这个问题时找到了 stackoverflow 上面的资料:http://stackoverflow.com/questions/5907527/application-control-of-tcp-retransmission-on-linux

根据排名第一的回答表示,Linux Kernel 2.6.37 中增加了一个叫做 TCP_USER_TIMEOUT 的 socket 选项。答案大意是,TCP_USER_TIMEOUT 选项是 TCP 层的 socket 选项,选项接受 unsigned int 类型的值。值为数据包被发送后未接收到 ACK 确认的最大时长,以毫秒为单位,例如设置为 10000 时,代表如果发送出去的数据包在十秒内未收到 ACK 确认,则下一次调用 send 或者 recv,则函数会返回-1,errno 设置为 ETIMEOUT,代表 connection timeout。

但在 Nodejs 中没有设置此值的方法,想到可以使用 NodeJS C++插件开发实现

2.4. NodeJS C++插件

插件是用 C++编写的动态链接的共享对象。require()函数可以作为普通的 Node.js 模块加载插件。插件提供了 JavaScript 和 C/C++库之间的接口。
实现插件有三个选项:Node-api、nan,或直接使用内部 V8、libuv 和 Node.js 库。除非需要直接访问 node-api 没有公开的功能,否则请使用 node-api。有关 node-api 的更多信息,请参阅带有 node-api 的 C/C++插件。
当不使用 node-api 时,实现插件是很复杂的,涉及到几个组件和 api 的知识:
V8:C++库 Node.js 用来提供 JavaScript 实现。V8 提供了创建对象、调用函数等的机制。V8 的 API 主要记录在 v8.h 头文件中(deps/v8/inv8/在 Node.js 源树中),该文件也可以在线获得。
Libuv:实现 Node.js 事件循环的 C 库,它的工作线程和平台的所有异步行为。它还作为一个跨平台抽象库,为所有主要操作系统提供许多常见系统任务的简单的、类似 posix 的访问,例如与文件系统、套接字、计时器和系统事件的交互。Libuv 还提供了一个类似于 POSIX 线程的线程抽象,用于需要超越标准事件循环的更复杂的异步插件。Addon 作者应该通过通过 libuv 将工作卸载到非阻塞系统操作、工作线程或定制使用 libuv 线程,从而避免使用 I/O 或其他耗费时间的任务来阻塞事件循环。
内部 Node.js 库。Node.js 本身导出插件可以使用的 C++api,其中最重要的是节点::ObbectWrap 类。
Node.js 包括其他静态链接的库,包括 OpenSSL。这些其他库位于 Node.js 源代码树中的 deps/目录中。只有 libuv、OpenSSL、V8 和 zlib 符号会被 Node.js 有意地重新导出,并且插件可以在不同程度上使用。有关其他信息,请参见链接到 Node.js 中包含的库。

在 NPM 中有sockopt库,此库可以插件形式实现 getsockopt & setsockopt 接口 Node.js sockets.

2.5. 测试两种场景

两种场景:

  1. TCP 一直没有发数据,此时拔网线,触发 nodejs setKeepAlive 接口设置
  2. TCP 一直发数据,此时拔网络,触发 TCP_USER_TIMEOUT 超时

经测试,两种场景都满足,问题解决。
代码如下:

//client.js
var net = require('net');
const { getsockopt, setsockopt } = require('sockopt');

let handle;
const { setInterval } = require('timers');
var client = net.connect({ port: 8888, host: '60.205.214.26' }, function () {
  client.setKeepAlive(true, 15000);
  client.name = '客户机1';
  let i = 0;
  client.on('close', (error) => {
    console.log('close:', error);
    clearInterval(handle);
    client.end();
  });
  client.on('connect', (error) => {});
  client.on('data', function (data) {
    console.log('recv data:', data.toString());
  });
  client.on('drain', function (data) {
    console.log('drain:', data.toString());
  });
  client.on('end', (error) => {
    console.log('end:', error);
  });
  client.on('error', (error) => {
    console.log('error:', new Date(Date.now()).toString());
  });
  client.on('lookup', (error) => {
    console.log('lookup:', error);
  });
  client.on('ready', (error) => {
    console.log('ready:', new Date(Date.now()).toString());
    const SOL_TCP = 6;
    const TCP_USER_TIMEOUT = 18;

    console.log('TCP_USER_TIMEOUT is now', getsockopt(client, SOL_TCP, TCP_USER_TIMEOUT));
    setsockopt(client, SOL_TCP, TCP_USER_TIMEOUT, 5 * 1000);
    console.log('TCP_USER_TIMEOUT is now', getsockopt(client, SOL_TCP, TCP_USER_TIMEOUT));
    handle = setInterval(() => {
      console.log('write data:', 'asdf');
      client.write('asdf');
    }, 1000);
  });
  client.setTimeout(3000);
  client.on('timeout', () => {
    console.log('socket timeout');
    client.end();
  });
});

3. 引申

尝试为 nodejs 贡献代码
Node.js 源码解析之 TCP
linux 定时器_通过 linux 源码分析 nodejs 的 keep-alive
unix: add uv_tcp_user_timeout fonction #900
epoll 使用详解(精髓)
硬核图解网络 IO 模型!
Linux IO 模式及 select、poll、epoll 详解
哪 5 种 IO 模型?什么是 select/poll/epoll?同步异步阻塞非阻塞有啥区别?全在这讲明白了!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值