首页 / Node.js 教程 / http 模块与 HTTP 事务

Node.js 教程

http 模块与 HTTP 事务

本教程共 76 篇 · 第 26 篇 · 更新于 2026-07-25 · 约 6 分钟阅读

Node.jshttp服务器请求响应HTTP

26. http 模块与 HTTP 事务

本节目标:用内置 http 模块创建服务器,理解请求响应的完整生命周期。

Node.js 最出名的标签就是「用 JavaScript 写服务端」。而实现这一切的基石,就是内置的 http 模块。它很轻量,没有框架的封装,却能让你看清一个 HTTP 请求从进门到送走的完整生命周期。

这一章我会带你拆解一次 HTTP 事务的每个环节:服务器怎么启动、请求里能掏出什么信息、响应又该如何组装。读完之后,哪怕不用 Express,你也能手写一个能跑的服务器。

启动一个最简服务器

http.createServer() 是入口。你传一个回调函数给它,每次有请求进来,这个函数就会被触发。

import http from 'node:http';

const server = http.createServer((req, res) => {
  res.end('Hello, Node.js!');
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

req(请求对象)和 res(响应对象)是回调的两个参数。reqIncomingMessage 的实例,继承自可读流;resServerResponse 的实例,继承自可写流。这意味着你可以用流的那一套 API 来处理它们。

server.listen() 让进程挂起在端口上等待连接。Node.js 不会退出,因为服务器内部注册了 I/O 事件,事件循环一直有事做。

Tip

在 v24 LTS 中,建议统一使用 node: 前缀导入内置模块,这样不会和 npm 包冲突,语义也更清晰。

HTTP 事务的生命周期

一次 HTTP 事务可以拆成三步:接收请求 → 处理逻辑 → 发送响应。下面按数据流动的顺序细说。

读取请求行与头部

请求刚进来时,最先能读到的是方法、URL 和协议版本。Node.js 把这些挂在 req 对象上:

const { method, url, headers } = req;
console.log(`${method} ${url}`);
console.log('User-Agent:', headers['user-agent']);

headers 里的键名会被强制转为小写。不管客户端传的是 Content-Type 还是 content-type,到这儿都是后者。如果你需要保留原始大小写,可以用 req.rawHeaders,它是一个交替存放键值的数组。

// rawHeaders 示例
// ['Content-Type', 'application/json', 'X-Custom', '123']

读取请求体

GET 请求一般没有 body,但 POST、PUT、PATCH 经常会带。req 是可读流,body 的数据会分批以 Buffer 的形式推给你:

let body = [];
req.on('data', chunk => {
  body.push(chunk);
});
req.on('end', () => {
  body = Buffer.concat(body).toString();
  console.log('Received body:', body);
});

这里我踩过一个坑:直接用 chunk.toString() 拼接字符串,如果数据是中文且被截断在两个字节的中间,就会出现乱码。最稳妥的做法是先收集所有 Buffer,再用 Buffer.concat() 一次性转字符串。

组装响应

响应对象 res 有三个关键动作:设状态码、设响应头、写响应体。

res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true }));

如果你习惯一次性写完头部,可以用 writeHead

res.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'Node.js v24',
});
res.end(JSON.stringify({ ok: true }));
Warning

writeHead 必须在 writeend 之前调用。一旦开始写 body,头部就会被锁定,再改状态码或加响应头都会静默失效。

res.end() 既发送最后一块数据,也关闭这次连接。如果不调用它,客户端会一直处于等待状态,直到超时。

错误处理别漏掉

请求流和响应流都可能出错。如果 req 上没挂 error 监听器,异常会直接抛出来,把进程砸崩。

req.on('error', err => {
  console.error('Request error:', err);
  res.statusCode = 400;
  res.end('Bad Request');
});

res.on('error', err => {
  console.error('Response error:', err);
});

我在生产环境里见过因为没处理 res 的 error,导致日志里大片 EPIPEECONNRESET。这些错误通常是因为客户端提前断开了连接,服务端还在往里面写数据。加上监听器,至少能让进程稳住。

Echo 服务器:把学到的串起来

下面这个例子把请求方法、URL、头部和 body 全部读出来,再以 JSON 格式原样返回给客户端。它是个很好的调试工具,也是理解 HTTP 事务完整流程的标本。

import http from 'node:http';

const server = http.createServer((req, res) => {
  const { method, url, headers } = req;
  let body = [];

  req.on('error', err => {
    console.error(err);
    res.statusCode = 400;
    res.end();
  });

  res.on('error', err => {
    console.error(err);
  });

  req.on('data', chunk => body.push(chunk));

  req.on('end', () => {
    body = Buffer.concat(body).toString();

    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json');

    res.end(
      JSON.stringify({
        method,
        url,
        headers,
        body,
      })
    );
  });
});

server.listen(3000, () => {
  console.log('Echo server at http://localhost:3000');
});

测试一下:

curl -X POST http://localhost:3000/test \
  -H "Content-Type: text/plain" \
  -d "hello world"

你会看到服务端把你发过去的所有信息都打包还了回来。

用 pipe 简化数据搬运

既然 req 是可读流、res 是可写流,中间没有额外加工的时候,可以直接 pipe

import http from 'node:http';

const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/echo') {
    req.pipe(res);
  } else {
    res.statusCode = 404;
    res.end('Not Found');
  }
});

server.listen(3000);

pipe 会自动处理背压和流量控制,比手动监听 datawrite 更省心。不过要注意,pipe 不会帮你设响应头,如果需要指定 Content-Type,还是得手调。

常用状态码速查

写 HTTP 服务时,状态码是和服务端「态度」最直接的表达。这几个最常用:

状态码含义使用场景
200OK正常返回数据
201Created资源创建成功
301/302重定向资源换了地址
400Bad Request客户端参数有误
401Unauthorized未认证
403Forbidden无权限
404Not Found资源不存在
500Internal Server Error服务端内部错误