MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js 网络通信中的心跳包设计与实现

2021-03-213.2k 阅读

什么是心跳包

在网络通信领域,心跳包是一种在客户端和服务器之间周期性发送的小数据包。它的主要作用类似于人类的心跳,通过定期的信号传输来确认连接的健康状态,确保通信链路持续有效。

从本质上来说,网络连接可能由于各种原因中断,比如网络故障、服务器过载或者客户端异常关闭等。而心跳包就像是一种“探路者”,在规定的时间间隔内发送,告知对方“我还在,连接正常”。如果一方长时间没有收到对方的心跳包,就可以判断连接出现了问题,进而采取相应的措施,比如重新建立连接。

以即时通讯应用为例,客户端与服务器之间需要保持长连接以实现实时消息推送。但网络环境复杂多变,可能会出现短暂的网络波动或者连接假死状态。这时,心跳包就起到了关键作用,客户端定时向服务器发送心跳包,服务器根据心跳包的接收情况判断客户端是否在线,若长时间未收到心跳包,服务器可主动断开连接以释放资源,避免无效连接占用过多资源。

心跳包在 Node.js 网络通信中的重要性

  1. 检测网络连接状态 在 Node.js 构建的网络应用中,无论是 Web 服务器与客户端的交互,还是 Node.js 服务之间的通信,都可能面临网络不稳定的情况。通过心跳包机制,服务器可以实时了解客户端的连接状态,反之亦然。当网络出现短暂中断时,心跳包能够及时发现问题,而不是等到真正的数据传输时才察觉到连接已失效。

  2. 防止连接被中间设备关闭 在网络通信过程中,中间设备(如防火墙、路由器等)可能会因为长时间未检测到数据传输而自动关闭连接。心跳包以固定的时间间隔发送,能够维持连接的活跃状态,防止被中间设备误判为闲置连接而关闭。例如,在企业内部网络中,一些安全策略可能会定期清理长时间无数据交互的连接,心跳包可以有效规避这种情况。

  3. 优化资源管理 对于服务器端来说,准确掌握客户端的连接状态有助于合理分配资源。当服务器检测到某个客户端长时间未发送心跳包,即认为该客户端可能已异常离线,此时服务器可以及时释放为该客户端分配的资源,如内存、文件描述符等,提高服务器整体的资源利用率。

心跳包设计原理

  1. 心跳包发送频率 心跳包的发送频率是一个关键参数,需要根据具体应用场景进行合理设置。如果发送频率过高,会增加网络带宽和系统资源的消耗;如果频率过低,则可能无法及时检测到连接故障。一般来说,对于网络环境较为稳定的应用,心跳包频率可以相对低一些,比如 30 秒到 1 分钟发送一次;而对于网络环境复杂、对连接状态敏感的应用,可能需要 5 到 10 秒发送一次心跳包。

  2. 心跳包内容 心跳包的内容通常非常简单,主要包含一些标识信息,用于让接收方识别这是一个心跳包。常见的做法是在包中添加特定的头部字段,如“Heartbeat: true”,或者使用固定的消息格式,比如简单的文本字符串“HEARTBEAT”。这样接收方在接收到数据包时,通过识别这些特定标识就能判断是否为心跳包。

  3. 超时机制 与心跳包发送频率相对应的是超时机制。接收方在设定的时间内(通常为心跳包发送间隔的一定倍数,如 2 到 3 倍)未收到心跳包,就判定连接超时。例如,心跳包发送间隔为 10 秒,超时时间可设置为 30 秒。一旦判定连接超时,接收方就可以执行相应的处理逻辑,如断开连接、尝试重连等。

Node.js 实现心跳包的技术选型

  1. 使用 Net 模块 Node.js 的 Net 模块提供了创建 TCP 服务器和客户端的功能,它是 Node.js 内置的网络模块,性能高效且稳定。通过 Net 模块,可以方便地实现心跳包的发送和接收逻辑。在服务器端,可以监听连接事件,为每个连接创建一个心跳检测定时器;在客户端,定时发送心跳包给服务器。

  2. WebSocket WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它在 Node.js 中也有很好的支持。WebSocket 协议本身就支持心跳机制,通过设置 ping 和 pong 消息来实现类似心跳包的功能。使用 WebSocket 实现心跳包,代码相对简洁,而且能更好地处理复杂的网络场景,适合用于 Web 应用的实时通信场景。

  3. Socket.IO Socket.IO 是一个跨平台的实时双向通信库,它在 WebSocket 的基础上进行了封装,提供了更简单易用的 API,并且能够自动适应不同的网络环境,包括在不支持 WebSocket 的环境下自动降级使用其他传输方式(如轮询)。在 Node.js 应用中使用 Socket.IO 实现心跳包,可以充分利用其丰富的功能和良好的兼容性。

使用 Net 模块实现心跳包

  1. 服务器端代码实现 首先,创建一个简单的 TCP 服务器,并为每个连接设置心跳检测机制。
const net = require('net');

const server = net.createServer((socket) => {
  let heartbeatTimer;
  // 重置心跳定时器
  const resetHeartbeat = () => {
    clearTimeout(heartbeatTimer);
    heartbeatTimer = setTimeout(() => {
      console.log('Client connection timed out');
      socket.destroy();
    }, 30000); // 30秒超时
  };

  socket.on('data', (data) => {
    const message = data.toString().trim();
    if (message === 'HEARTBEAT') {
      resetHeartbeat();
      socket.write('HEARTBEAT_ACK');
    }
  });

  socket.on('connect', () => {
    resetHeartbeat();
  });

  socket.on('end', () => {
    clearTimeout(heartbeatTimer);
    console.log('Client disconnected');
  });
});

server.listen(8080, () => {
  console.log('Server listening on port 8080');
});

在上述代码中,服务器为每个连接创建了一个心跳定时器。当接收到客户端发送的“HEARTBEAT”消息时,重置定时器。如果 30 秒内未收到心跳包,则判定连接超时,关闭连接。

  1. 客户端代码实现 客户端定时向服务器发送心跳包,并处理服务器的响应。
const net = require('net');

const client = net.connect({ port: 8080 }, () => {
  console.log('Connected to server');
  const sendHeartbeat = () => {
    client.write('HEARTBEAT');
    setTimeout(sendHeartbeat, 10000); // 每10秒发送一次心跳包
  };
  sendHeartbeat();
});

client.on('data', (data) => {
  const message = data.toString().trim();
  if (message === 'HEARTBEAT_ACK') {
    console.log('Heartbeat acknowledged by server');
  }
});

client.on('end', () => {
  console.log('Connection to server ended');
});

客户端每 10 秒向服务器发送一次“HEARTBEAT”消息,并在接收到服务器的“HEARTBEAT_ACK”响应时,打印确认信息。

使用 WebSocket 实现心跳包

  1. 安装依赖 首先需要安装 ws 库,它是 Node.js 中常用的 WebSocket 实现库。
npm install ws
  1. 服务器端代码实现
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8081 });

wss.on('connection', (ws) => {
  let heartbeatTimer;
  const resetHeartbeat = () => {
    clearTimeout(heartbeatTimer);
    heartbeatTimer = setTimeout(() => {
      console.log('Client connection timed out');
      ws.close();
    }, 30000); // 30秒超时
  };

  ws.on('message', (message) => {
    if (message === 'ping') {
      resetHeartbeat();
      ws.send('pong');
    }
  });

  ws.on('open', () => {
    resetHeartbeat();
  });

  ws.on('close', () => {
    clearTimeout(heartbeatTimer);
    console.log('Client disconnected');
  });
});

在 WebSocket 服务器端,当接收到客户端发送的“ping”消息(类似心跳包)时,重置心跳定时器并回复“pong”消息。如果 30 秒内未收到心跳包,则关闭连接。

  1. 客户端代码实现
const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:8081');

ws.on('open', () => {
  const sendHeartbeat = () => {
    ws.send('ping');
    setTimeout(sendHeartbeat, 10000); // 每10秒发送一次心跳包
  };
  sendHeartbeat();
});

ws.on('message', (message) => {
  if (message === 'pong') {
    console.log('Heartbeat acknowledged by server');
  }
});

ws.on('close', () => {
  console.log('Connection to server ended');
});

客户端每 10 秒向服务器发送一次“ping”消息作为心跳包,并在接收到“pong”响应时,打印确认信息。

使用 Socket.IO 实现心跳包

  1. 安装依赖 安装 socket.io 库。
npm install socket.io
  1. 服务器端代码实现
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);

io.on('connection', (socket) => {
  let heartbeatTimer;
  const resetHeartbeat = () => {
    clearTimeout(heartbeatTimer);
    heartbeatTimer = setTimeout(() => {
      console.log('Client connection timed out');
      socket.disconnect();
    }, 30000); // 30秒超时
  };

  socket.on('heartbeat', () => {
    resetHeartbeat();
    socket.emit('heartbeat_ack');
  });

  socket.on('connect', () => {
    resetHeartbeat();
  });

  socket.on('disconnect', () => {
    clearTimeout(heartbeatTimer);
    console.log('Client disconnected');
  });
});

http.listen(8082, () => {
  console.log('Server listening on port 8082');
});

在 Socket.IO 服务器端,当接收到客户端发送的“heartbeat”事件(即心跳包)时,重置心跳定时器并发送“heartbeat_ack”事件作为响应。如果 30 秒内未收到心跳包,则断开连接。

  1. 客户端代码实现
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Socket.IO Heartbeat Client</title>
</head>

<body>
  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io('http://localhost:8082');
    const sendHeartbeat = () => {
      socket.emit('heartbeat');
      setTimeout(sendHeartbeat, 10000); // 每10秒发送一次心跳包
    };
    socket.on('connect', () => {
      sendHeartbeat();
    });
    socket.on('heartbeat_ack', () => {
      console.log('Heartbeat acknowledged by server');
    });
    socket.on('disconnect', () => {
      console.log('Connection to server ended');
    });
  </script>
</body>

</html>

客户端使用 JavaScript 在浏览器环境中,每 10 秒向服务器发送“heartbeat”事件作为心跳包,并在接收到“heartbeat_ack”事件时,打印确认信息。

心跳包实现中的常见问题与解决方法

  1. 网络延迟导致心跳包丢失 在网络环境复杂的情况下,心跳包可能会因为网络延迟而丢失。解决方法之一是增加心跳包的重发机制。例如,在客户端发送心跳包后,启动一个定时器等待服务器的响应。如果在一定时间内未收到响应,则重新发送心跳包。
// 客户端代码增加重发机制示例
const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:8081');

const sendHeartbeat = () => {
  const sendHeartbeatWithRetry = (retryCount = 0) => {
    ws.send('ping');
    const timeout = setTimeout(() => {
      if (retryCount < 3) {
        sendHeartbeatWithRetry(retryCount + 1);
      } else {
        console.log('Heartbeat failed after multiple retries');
      }
    }, 5000);
    ws.once('message', (message) => {
      clearTimeout(timeout);
      if (message === 'pong') {
        console.log('Heartbeat acknowledged by server');
      }
    });
  };
  sendHeartbeatWithRetry();
  setTimeout(sendHeartbeat, 10000);
};

ws.on('open', () => {
  sendHeartbeat();
});

ws.on('close', () => {
  console.log('Connection to server ended');
});
  1. 心跳包与业务数据冲突 当心跳包和业务数据使用相同的通信通道时,可能会出现冲突。一种解决办法是在协议设计上进行区分,比如为心跳包和业务数据定义不同的消息类型。在接收端,根据消息类型进行不同的处理。
// 服务器端区分心跳包和业务数据示例
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8081 });

wss.on('connection', (ws) => {
  let heartbeatTimer;
  const resetHeartbeat = () => {
    clearTimeout(heartbeatTimer);
    heartbeatTimer = setTimeout(() => {
      console.log('Client connection timed out');
      ws.close();
    }, 30000);
  };

  ws.on('message', (message) => {
    const { type, data } = JSON.parse(message);
    if (type === 'heartbeat') {
      resetHeartbeat();
      ws.send(JSON.stringify({ type: 'heartbeat_ack' }));
    } else if (type === 'business_data') {
      // 处理业务数据
      console.log('Received business data:', data);
    }
  });

  ws.on('open', () => {
    resetHeartbeat();
  });

  ws.on('close', () => {
    clearTimeout(heartbeatTimer);
    console.log('Client disconnected');
  });
});
  1. 多连接场景下的心跳管理 在服务器处理多个客户端连接时,心跳管理可能会变得复杂。可以使用数据结构(如 Map)来存储每个连接对应的心跳定时器,方便进行统一管理。
// 服务器端多连接心跳管理示例
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8081 });
const heartbeatTimers = new Map();

wss.on('connection', (ws) => {
  const resetHeartbeat = () => {
    const existingTimer = heartbeatTimers.get(ws);
    if (existingTimer) {
      clearTimeout(existingTimer);
    }
    const newTimer = setTimeout(() => {
      console.log('Client connection timed out');
      ws.close();
      heartbeatTimers.delete(ws);
    }, 30000);
    heartbeatTimers.set(ws, newTimer);
  };

  ws.on('message', (message) => {
    if (message === 'ping') {
      resetHeartbeat();
      ws.send('pong');
    }
  });

  ws.on('open', () => {
    resetHeartbeat();
  });

  ws.on('close', () => {
    const timer = heartbeatTimers.get(ws);
    if (timer) {
      clearTimeout(timer);
      heartbeatTimers.delete(ws);
    }
    console.log('Client disconnected');
  });
});

通过上述设计与实现,在 Node.js 网络通信中能够有效地利用心跳包机制来确保连接的稳定性和可靠性,根据不同的应用场景选择合适的技术方案,并解决常见问题,从而构建出健壮的网络应用。无论是 Net 模块的基础 TCP 通信,还是 WebSocket 和 Socket.IO 的高级实时通信,心跳包都扮演着不可或缺的角色,为应用的持续稳定运行提供保障。