全部文章

Node.js HTTP 服务入门:进程、端口、请求与响应

解释 Node.js 服务从启动到响应的过程,提供可运行的健康检查接口,以及连接失败、404、端口占用和异步阻塞的排查方法。

AI 时代,越来越多前端开始转向全栈。对前端来说,Node 是一个很好的切入点:技术栈接近,上手成本更低,也足够实用,能帮我们更快把想法变成可运行的服务,迅速搭出一个 MVP(最小可行产品)。

这篇文章就来认识一下 Node 服务,围绕两个核心问题展开:Node 服务是什么,它是怎么跑起来的;从服务启动到处理客户端请求,中间发生了什么?

Node 服务从启动到响应的完整链路

一、Node 服务是什么

先说结论:

Node 服务,本质上就是一个长期驻留的 Node 进程。

这个进程通常会做几件事:

  • 监听某个端口;
  • 接收客户端请求;
  • 执行业务逻辑;
  • 访问数据库、缓存、第三方服务;
  • 把结果返回给客户端。

例如:

const http = require('http')

const server = http.createServer((req, res) => {
  res.end('hello')
})

server.listen(3000)

如果只是执行一段普通脚本,Node 代码跑完就结束了。但如果代码里调用了 server.listen(3000),程序就不会退出,而是会一直运行,等待客户端访问这个端口。这个持续运行、对外提供能力的程序,就是常说的 Node 服务。

可以把它记成一句话:

Node 服务 = Node 进程 + 应用代码 + 可供客户端访问的网络入口

二、Node 进程是什么

很多人在刚接触 Node 时,容易把“代码文件”和“运行中的服务”混在一起。

比如执行:

node app.js

这里真正被操作系统启动的,不是 app.js,而是 node。app.js 只是传给 node 的脚本文件。

所以更准确地说,不是“app.js 变成了一个进程”,而是:

操作系统先创建一个 Node 进程,再由这个进程去加载并执行 app.js。

从系统视角看,一个运行中的 Node 进程通常包含这些东西:

  • 一个进程 ID,也就是 PID;
  • 一块独立的虚拟内存空间;
  • 正在执行 JavaScript 的主线程;
  • Node 运行时初始化出来的环境;
  • 打开的文件描述符;
  • 网络连接和监听端口;
  • 定时器、回调、未完成的异步任务。

如果这个进程里已经执行了 server.listen(3000),那它就不只是“在跑一段 JS”,而是在作为一个服务进程对外提供能力。

执行 node app.js 时发生了什么

整个过程大致可以拆成这样:

  1. 在终端输入 node app.js;
  2. 当前 shell 解析命令;
  3. shell 找到 node 可执行程序;
  4. shell 请求操作系统启动这个程序;
  5. 操作系统创建一个新的进程,并分配 PID、内存、文件描述符等资源;
  6. 新进程开始运行 node;
  7. Node 初始化运行时环境;
  8. Node 读取并执行 app.js。

如果 app.js 只是普通脚本,执行完后进程就退出。如果 app.js 里启动了 HTTP 服务、定时器或者还有其他活跃任务,这个进程就会继续存活。

三、Node 运行时提供了什么

JavaScript 本身只是一门语言。如果离开浏览器,单靠语言规范,其实并没有读写文件、监听端口、启动子进程、获取环境变量、直接访问操作系统资源这些能力。

这些能力,是 Node 运行时提供的。也就是说,Node 不只是一个“执行 JS 的工具”,而是一整套服务器端运行环境。

1. 文件系统能力

最常见的是 fs 模块,可以读取、写入和删除文件,创建和遍历目录。

const fs = require('fs')

fs.readFile('./a.txt', 'utf8', (err, data) => {
  console.log(data)
})

2. 网络能力

最常见的是 http、https、net 和 tls:

const http = require('http')

http.createServer((req, res) => {
  res.end('ok')
}).listen(3000)

这意味着 Node 可以直接写 HTTP 服务,而浏览器里的 JavaScript 做不到“自己监听一个端口”。

3. 进程能力

Node 提供 process 和 child_process,可以获取当前进程 PID、环境变量、命令行参数和退出信号。

console.log(process.pid)
console.log(process.env.NODE_ENV)

4. 二进制与流能力

Node 还提供 Buffer 和 stream,用于处理文件传输、网络字节流、图片与音视频,以及大文件上传下载。

5. 定时与调度能力

setTimeout、setInterval、setImmediate 和 process.nextTick 也属于运行时提供的宿主能力。

一句话概括:

Node 做的事,就是把操作系统的一部分能力,包装成 JavaScript API 暴露出来。

四、从服务启动到处理客户端请求,中间发生了什么

这是理解 Node 服务的关键链路。假设有这样一段代码:

const http = require('http')

const server = http.createServer((req, res) => {
  res.end('hello')
})

server.listen(3000)

当浏览器访问 http://localhost:3000,中间大致会发生下面这些步骤。

第 1 步:Node 启动监听

执行 listen(3000) 后,Node 创建监听 socket,操作系统记录 3000 端口归这个 Node 进程监听,进程进入待命状态。

第 2 步:客户端发起 TCP 连接

HTTP 请求不是直接进入 Node 的,而是运行在 TCP 连接之上。所以顺序不是“先发 HTTP”,而是客户端先和服务端建立 TCP 连接,连接建立后,再发送 HTTP 请求报文。

第 3 步:操作系统接收连接

客户端的数据包到达机器后,操作系统会根据端口号查找:“3000 端口是谁在监听?”查到是这个 Node 进程后,操作系统会接受连接、把连接加入队列,并在连接可读时通知 Node。

第 4 步:Node 收到可读通知

Node 并不是一直死循环检查网络。真正盯着网络的是操作系统,Node 更多是被通知的一方。当操作系统发现有新连接到来,或者某个连接上已经有数据,就会通知 Node 去处理。

第 5 步:Node 读取原始 TCP 字节流

这时 Node 拿到的还不是“漂亮的请求对象”,而是原始网络字节。例如客户端发来的可能是:

GET /user HTTP/1.1
Host: localhost:3000
Connection: keep-alive

本质上它就是一段按 HTTP 协议组织起来的字节流。

第 6 步:Node 解析 HTTP 协议

Node 内部会把这些字节解析成请求方法、URL、请求头和请求体,然后构造出熟悉的 req 与 res。

第 7 步:执行回调

直到这一步,才轮到业务逻辑执行:

(req, res) => {
  res.end('hello')
}

第 8 步:写回响应

当调用 res.end('hello'),Node 会把响应状态行、响应头、响应体拼成 HTTP 响应字节,再通过底层 socket 写回客户端。

整条链路可以总结成一句话:

Node 先把“监听端口、接收连接”交给操作系统,等真正有连接和数据到来时,操作系统再通知 Node,Node 才开始解析 HTTP 并执行应用代码。

五、Node 为什么能同时处理很多请求

Node 的 JavaScript 主执行逻辑通常是单线程的,但它并不等于整个运行时只有一个线程,也不等于一次只能服务一个请求。

业务代码通常运行在主线程上。但在很多 Web 场景里,请求的大部分时间其实不是“在算”,而是在“等”:等数据库、等 Redis、等第三方接口、等网络收发。

这些等待中的 I/O 操作,并不会一直堵住主线程。Node 会在当前请求挂起等待结果时,继续去处理别的事件和请求,等结果返回后,再恢复后续逻辑。

所以,Node 的关键不是“一个请求开一个线程”,而是:

  1. 发起异步操作;
  2. 当前请求先挂起;
  3. 主线程继续处理别的请求;
  4. 等结果返回后,再恢复后续逻辑。

举个例子

来看一个典型接口:

app.get('/user', async (req, res) => {
  const data = await db.query('select * from user')
  res.json(data)
})

假设此时有两个页面几乎同时请求 /user,把它们分别叫作请求 A 和请求 B。请求 A 到达 Node、进入路由回调并执行到 await db.query(...) 后,Node 把 SQL 发给数据库,当前请求 A 暂时挂起;这时 Node 会继续处理请求 B。请求 B 也执行到 await db.query(...),谁的数据库结果先回来,谁就先恢复执行,分别调用 res.json(data) 返回响应。

这里 await 暂停的是当前请求对应的这段函数逻辑,不是整个 Node 服务。也正因为这样,Node 才能在等待数据库结果时继续处理其他请求。

所以,Node 特别适合 API 服务、BFF 层、网关服务、实时通信和 I/O 密集型业务,因为它把等待网络、等待数据库、等待外部接口的时间都利用起来了。

六、自己运行一个可检查的 HTTP 服务

将下面的代码保存为 server.cjs,再运行 node server.cjs。使用 .cjs 是为了明确按 CommonJS 加载,避免当前项目的模块配置影响 require 示例。

const http = require('node:http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200);
    res.end(JSON.stringify({ ok: true }));
    return;
  }
  res.writeHead(404);
  res.end(JSON.stringify({ error: 'Not found' }));
});

server.listen(3000, '127.0.0.1', () => {
  console.log('Listening on http://127.0.0.1:3000');
});

浏览器打开 http://127.0.0.1:3000/health,应看到 {"ok":true};访问 /missing 应收到 HTTP 404。这里只监听本机地址,其他设备无法通过服务器公网 IP 直接访问它。

现象 说明 先检查什么
连接被拒绝 还没有收到 HTTP 响应 进程是否存活、地址和端口是否一致
返回 404 已连到服务,但没有匹配接口 路径与请求方法
EADDRINUSE 监听端口已被占用 找到原进程,或换一个测试端口
所有请求一起变慢 可能存在主线程阻塞或资源瓶颈 同步大计算、同步文件读写和外部依赖

async 或 await 不会把同步的大计算自动移到其他线程。例子中的数据库请求只说明异步 I/O 的等待方式,不能据此认为任何业务都不会阻塞。

HTTP API 参考 Node.js 官方文档。本机服务确认正常后,再按 Nginx 与 systemd 部署流程 把服务交给代理和进程管理器。

七、一句话把整件事串起来

如果把整篇文章压缩成一句话,可以概括为:

当执行 node app.js 时,操作系统创建了一个 Node 进程;Node 初始化运行时后执行应用代码;如果代码里调用了 listen(3000),Node 就会向操作系统注册一个监听端口;客户端发起 TCP 连接并发送 HTTP 请求后,操作系统通知 Node 读取数据;Node 再把原始字节解析成 req/res,执行应用逻辑,最后把响应写回客户端。

这就是一个 Node 服务从启动到处理请求的核心主线。

内容更新:

ABOUT THE AUTHOR

我是 AI 煮代码汤

记录 AI、编程和成长路上的日常折腾。这里会分享能直接上手的代码技巧、最近好玩的 AI 工具,也写一点技术之外的真实感受。希望你看完,不只是收藏一篇文章,而是真的多一个可以试试的办法。

KEEP IN TOUCH

继续保持连接.

如果这篇文章对你有帮助,可以用下面的方式继续关注。

RSS 订阅

按自己的节奏阅读更新

没有算法排序,用 RSS 获取新文章,不错过每一次实践更新。

微信公众号·AI 煮代码汤

扫码关注,获取 AI、编程和开发实践的最新分享。
微信公众号 AI 煮代码汤二维码
把这篇笔记分享给同样好奇的人。

搜索文章

输入关键词,发现一篇好文章。

AICookCode / 站内搜索