http 模块与 HTTP 事务
本教程共 76 篇 · 第 26 篇 · 更新于 2026-07-25 · 约 6 分钟阅读
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(响应对象)是回调的两个参数。req 是 IncomingMessage 的实例,继承自可读流;res 是 ServerResponse 的实例,继承自可写流。这意味着你可以用流的那一套 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必须在write或end之前调用。一旦开始写 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,导致日志里大片 EPIPE 和 ECONNRESET。这些错误通常是因为客户端提前断开了连接,服务端还在往里面写数据。加上监听器,至少能让进程稳住。
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 会自动处理背压和流量控制,比手动监听 data 再 write 更省心。不过要注意,pipe 不会帮你设响应头,如果需要指定 Content-Type,还是得手调。
常用状态码速查
写 HTTP 服务时,状态码是和服务端「态度」最直接的表达。这几个最常用:
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | OK | 正常返回数据 |
| 201 | Created | 资源创建成功 |
| 301/302 | 重定向 | 资源换了地址 |
| 400 | Bad Request | 客户端参数有误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 500 | Internal Server Error | 服务端内部错误 |