广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Node.js 中如何用 http.server 监听端口并管理连接?

Node.js 的 http 模块内置了 HTTP 服务器能力,通过 http.createServer 可以得到一个 Server 实例。要让服务真正对外提供访问,需要调用 listen 方法绑定端口;而服务运行期间的稳定性,很大程度取决于对底层 socket 连接的管理是否到位。本文从监听、连接事件、超时参数和优雅关闭几个层面,拆解 http.server 的使用细节。

Node.js 中如何用 http.server 监听端口并管理连接?

监听端口:listen 方法与错误处理

创建 HTTP 服务的第一步通常是这样的:

const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('hello');
});
server.listen(3000, () => {
  console.log('server listening on port 3000');
});

listen 方法可以接受多种参数形式。除了常见的端口号加回调函数,还可以传入主机名、端口、backlog 以及回调,例如 server.listen(3000, '127.0.0.1', 511, callback)。backlog 参数会影响内核连接队列的长度,但在多数操作系统中实际行为可能存在差异,一般不需要手动调整,除非你已经明确观察到高并发下连接被拒绝的情况。

端口被占用、权限不足或地址不可用时,listen 不会直接抛出异常,而是在 server 对象上触发 error 事件。如果没有监听 error 事件,进程会直接崩溃。稳健的启动逻辑必须处理这个事件:

server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error('端口已被占用,请更换端口或停止占用进程');
    process.exit(1);
  } else if (err.code === 'EACCES') {
    console.error('没有权限监听该端口');
    process.exit(1);
  } else {
    console.error('服务器启动失败:', err);
  }
});

除了 error 事件,server 还提供 listening 事件。在 listen 回调中执行逻辑与监听 listening 事件是等价的,但使用事件可以让启动代码与业务逻辑分离得更清晰。一个常见做法是将启动、错误处理和关闭逻辑封装成一个独立模块,避免在主文件中堆叠大量生命周期代码。

连接事件与 socket 管理

http.server 本身继承自 net.Server,因此它会触发 connection 事件。每当有新的 TCP 连接建立时,该事件会收到一个 net.Socket 对象。这个 socket 对象是 HTTP 请求和响应的底层载体,理解它的生命周期对连接管理至关重要。

server.on('connection', (socket) => {
  console.log('新的 TCP 连接建立:', socket.remoteAddress, socket.remotePort);
  socket.on('close', () => {
    console.log('TCP 连接关闭');
  });
});

在 HTTP 请求处理函数中,req.socket 和 res.socket 都指向同一个底层 socket。我们可以利用这个特性,在业务逻辑中访问客户端地址、设置 socket 超时或记录连接时长。需要注意的是,HTTP keep-alive 机制会让一个 TCP 连接承载多个请求,因此 connection 事件触发的次数通常少于 request 事件触发的次数。如果你的日志里发现 connection 数量异常增长,很可能是因为客户端没有正确复用连接,或者服务端没有正确设置 keep-alive 相关参数。

另一个容易忽略的问题是半开连接。当客户端异常断网或进程被强制终止时,TCP 连接可能不会立即关闭,服务端 socket 会长时间处于不可用但未释放的状态。可以通过监听 socket 的 timeout 事件并主动销毁连接来缓解这一问题。不过更推荐的做法是依赖 server 级别的超时参数,让 Node.js 自动处理空闲连接。

超时与 keep-alive 参数详解

Node.js 的 HTTP 服务器提供了多个与连接超时相关的配置项,它们在不同阶段起作用。理解这些参数之间的区别,是解决连接堆积问题的基础。

keepAliveTimeout 表示服务器在完成一次响应后,等待下一个请求的空闲时间。默认值从 Node.js 8 开始为 5 秒。如果在这个时间内没有新的请求到达,连接会被关闭。这个值需要与客户端的 keep-alive 策略配合。如果客户端采用短连接,每次请求后主动关闭,则此参数影响不大;但如果客户端使用持久连接,过短的 keepAliveTimeout 会导致频繁重建连接,增加握手开销。

requestTimeout 从 Node.js 14 开始引入,表示服务器等待整个请求体传输完成的最长时间。默认值为 0,即不限制。如果请求体较大或客户端上传速度很慢,可以设置一个合理的值,例如 30 秒,避免慢请求占用连接和内存资源。headersTimeout 则限制服务器等待请求头传输完成的时间,默认值为 60 秒。如果攻击者只建立连接而不发送请求头,这个参数可以防止连接被长时间无效占用。

const server = http.createServer((req, res) => {
  res.end('ok');
});
server.keepAliveTimeout = 10000;   // 10 秒
server.requestTimeout = 30000;     // 30 秒
server.headersTimeout = 15000;     // 15 秒
server.maxHeadersCount = 2000;     // 限制请求头数量
server.listen(3000);

值得注意的是,keepAliveTimeout 和 headersTimeout 之间有一个隐含关系:如果 keepAliveTimeout 设置得比 headersTimeout 小,那么在 keep-alive 阶段,连接可能在没有收到完整请求头时就被关闭。Node.js 文档建议将 keepAliveTimeout 设置为大于 headersTimeout 的值,以避免在慢客户端场景下出现意外断开。不过实际场景中,如果客户端请求头传输非常慢,可能本身就不适合继续占用连接,因此需要根据服务特性权衡。

还有一个 server.timeout 属性,它作用于 socket 的空闲超时。默认值为 0,表示不启用。如果设置了非零值,socket 在空闲超过该时间后会触发 timeout 事件,但不会自动关闭,需要手动调用 socket.end() 或 socket.destroy()。这与 keepAliveTimeout 的行为不同,后者在超时后会自动关闭连接。实际项目中建议直接使用 keepAliveTimeout,避免自己处理 socket 的 timeout 事件带来额外复杂度。

优雅关闭与连接数控制

服务需要重启或发布时,直接调用 process.exit() 会导致正在处理的请求被强制中断。更合理的做法是调用 server.close(),它会停止接收新的连接,并等待现有连接处理完成后再关闭。但 server.close() 不会主动断开已经建立的 keep-alive 连接,如果客户端一直保持连接,进程可能迟迟无法退出。

为了解决这个问题,可以在调用 close() 之前,先对所有活跃连接执行 destroy() 或 end()。下面的示例展示了如何在关闭前清理连接:

const sockets = new Set();
server.on('connection', (socket) => {
  sockets.add(socket);
  socket.on('close', () => {
    sockets.delete(socket);
  });
});
function shutdown() {
  server.close(() => {
    console.log('所有连接已关闭');
    process.exit(0);
  });
  for (const socket of sockets) {
    socket.destroy();
    sockets.delete(socket);
  }
  // 设置一个硬超时,防止进程挂起
  setTimeout(() => {
    console.error('关闭超时,强制退出');
    process.exit(1);
  }, 10000).unref();
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

连接数控制是另一个容易忽视的环节。Node.js 默认不会限制同一时刻的并发连接数,如果业务层没有做好限流,大量连接可能耗尽文件描述符或内存。可以通过在 connection 事件中统计当前连接数,并在超过阈值时拒绝新连接:

let activeConnections = 0;
const MAX_CONNECTIONS = 1000;
server.on('connection', (socket) => {
  if (activeConnections >= MAX_CONNECTIONS) {
    socket.write('HTTP/1.1 503 Service Unavailable\r\n');
    socket.write('Connection: close\r\n\r\n');
    socket.end();
    return;
  }
  activeConnections += 1;
  socket.on('close', () => {
    activeConnections -= 1;
  });
});

这段代码直接操作底层 socket 返回了一个最小化的 503 响应。实际生产中,更推荐在应用层通过反向代理、负载均衡或专门的限流组件来控制连接数,因为底层 socket 的拒绝方式无法提供丰富的响应体,而且需要手动处理 HTTP 协议细节。不过了解这种直接控制方式,有助于你在极端情况下快速止血。

最后要强调,连接管理并不是孤立的配置项调整。端口监听、事件绑定、超时参数和优雅关闭彼此关联,测试时需要模拟慢客户端、突然断网、大量并发连接等场景,观察服务在压力下的行为是否稳定。只有把这些细节串联起来,才能真正做到对 http.server 的掌控。

广告位 · 文章底部(后台-模块管理替换此区域)