Node系列 · Node基础:http 模块
http模块建立在net之上:每条 HTTP 连接对应一个 socket,自动处理 HTTP 协议解析。理解req/res是"加了协议层的 socket",再学 Express / Koa 这类框架就只是"中间件 + 路由 + 上下文对象"的封装。
一、http 与 net 的关系
http 是基于 net 之上的协议层。req / res 是"加了 HTTP 解析的 socket"——本质都是流:
http 给 net.Socket 加了两层能力:
- 协议解析:自动按 HTTP 格式解析请求行、请求头、请求体
- 协议构造:自动按 HTTP 格式拼装响应行、响应头、响应体
业务代码拿到的是 req(已解析的请求)和 res(待构造的响应),而不是裸 socket。
二、最小 HTTP 服务端
理解 http 最快的方式是看一个能跑的最小例子:createServer 接收 (req, res) 回调,配置 res 然后 end():
const http = require('node:http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('hello\n');
});
server.listen(3000, () => {
console.log('http://127.0.0.1:3000');
});$ node http-server-mini.js &
$ curl http://127.0.0.1:3000
hello三、req 对象:服务器收到的请求
req 是 http.IncomingMessage 实例,继承自 stream.Readable——body 是流:
| 属性 / 方法 | 类型 | 说明 |
|---|---|---|
req.method | string | HTTP 方法:GET / POST / PUT / DELETE 等 |
req.url | string | URL 路径 + 查询字符串(不含协议、host) |
req.headers | object | 请求头(key 全小写) |
req.httpVersion | string | HTTP 版本('1.1' / '2.0') |
req.socket | net.Socket | 底层 socket |
req.on('data', cb) | event | 每个 body chunk 触发 |
req.on('end', cb) | event | body 接收完毕 |
3.1 解析请求 URL
req.url 是相对路径,要拼上 host 才是完整 URL:
const url = require('node:url');
const { URL } = require('node:url');
const server = http.createServer((req, res) => {
// 方式 1:老 API url.parse(⚠️ DEPRECATED,已废弃,请直接用 new URL)
const parsedOld = url.parse(req.url, true);
console.log(parsedOld.pathname, parsedOld.query);
// 方式 2:新 API URL(推荐)
const fullUrl = `http://${req.headers.host}${req.url}`;
const u = new URL(fullUrl);
console.log(u.pathname, u.searchParams);
res.end('ok\n');
});TIP
直接用 new URL(),不再用 url.parse(已废弃)。
3.2 解析请求头
const server = http.createServer((req, res) => {
// 头 key 全小写
const token = req.headers.authorization;
const contentType = req.headers['content-type'];
const userAgent = req.headers['user-agent'];
// 自定义头
const traceId = req.headers['x-trace-id'];
res.end('ok\n');
});四、res 对象:构造响应
res 是 http.ServerResponse 实例,继承自 stream.Writable。
4.1 核心方法
| 方法 | 作用 |
|---|---|
res.statusCode = 200 | 设置状态码(也可在 res.writeHead 里设) |
res.setHeader(name, value) | 设置单个响应头 |
res.getHeader(name) | 读取已设置的响应头 |
res.removeHeader(name) | 删除响应头 |
res.writeHead(status, headers) | 一次性写状态行 + 响应头 |
res.write(chunk) | 写一段响应体(流) |
res.end([chunk]) | 结束响应,可选最后一段数据 |
res.on('finish') | 响应已完全发送到底层 |
4.2 三种写响应的方式
const server = http.createServer((req, res) => {
// 方式 1:writeHead + write + end
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write('{"a":1}');
res.end();
// 方式 2:setHeader + write + end
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.write('{"a":1}');
res.end();
// 方式 3:end 一气呵成(最常用)
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end('{"a":1}');
});4.3 状态码速查
| 状态码 | 含义 | 常见场景 |
|---|---|---|
200 | OK | 成功 |
201 | Created | 创建资源成功 |
204 | No Content | 成功但无 body |
301 / 302 | 重定向 | 永久 / 临时 |
304 | Not Modified | 缓存命中 |
400 | Bad Request | 客户端参数错 |
401 | Unauthorized | 未登录 |
403 | Forbidden | 无权限 |
404 | Not Found | 资源不存在 |
500 | Internal Server Error | 服务端错 |
502 / 503 / 504 | 网关 / 服务 / 网关超时 | 上游问题 |
4.4 一次性设置 JSON
function sendJson(res, status, body) {
res.statusCode = status;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(body));
}
sendJson(res, 200, { ok: true, data: { id: 1 } });五、接收请求体(body)
请求体是流,必须手动累积:
const server = http.createServer(async (req, res) => {
if (req.method !== 'POST') {
res.statusCode = 405;
return res.end();
}
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const raw = Buffer.concat(chunks).toString('utf-8');
let body;
try {
body = JSON.parse(raw);
} catch (e) {
res.statusCode = 400;
return res.end(JSON.stringify({ error: 'invalid json' }));
}
res.end(JSON.stringify({ received: body }));
});如果客户端发的是 Content-Type: application/json,还需要验证大小(防 OOM):
const MAX = 1 * 1024 * 1024; // 1MB
let total = 0;
const chunks = [];
for await (const chunk of req) {
total += chunk.length;
if (total > MAX) {
req.destroy(); // 强制断开连接
return;
}
chunks.push(chunk);
}实际项目里一般用框架(Express + body-parser / Koa + koa-body)处理 body 解析,避免每个接口都写一遍。
六、最小路由
手写路由就是把 (req, res) 回调里用 method + url 做 if/else 分发:
const server = http.createServer((req, res) => {
const { method, url } = req;
if (method === 'GET' && url === '/') {
return sendJson(res, 200, { message: 'home' });
}
if (method === 'GET' && url === '/api/users') {
return sendJson(res, 200, { users: [] });
}
if (method === 'POST' && url === '/api/users') {
// 接收 body...
return sendJson(res, 201, { id: 1 });
}
res.statusCode = 404;
res.end('not found\n');
});写几行能跑,写到十几个接口就崩溃了——这就是 Express / Koa / Fastify 存在的理由。
七、发起 HTTP 请求(客户端)
7.1 http.request 通用方法
const http = require('node:http');
const req = http.request(
{
host: '127.0.0.1',
port: 3000,
path: '/api/users',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
},
(res) => {
console.log('状态:', res.statusCode);
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
console.log('响应:', Buffer.concat(chunks).toString());
});
}
);
req.on('error', (err) => console.error('请求失败:', err.message));
const payload = JSON.stringify({ name: 'Alice' });
req.write(payload);
req.end();7.2 http.get 简化 GET
http.get('http://127.0.0.1:3000/api/users', (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => console.log(body));
}).on('error', (err) => console.error(err));7.3 用 fetch(Node 18+ 内置)
实际项目里更推荐 fetch,API 与浏览器一致:
const res = await fetch('http://127.0.0.1:3000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
});
const data = await res.json();八、超时与错误处理
8.1 客户端超时
const req = http.request({ host, port, path, method }, (res) => { /* ... */ });
req.setTimeout(5000, () => {
req.destroy(new Error('请求超时'));
});
req.on('error', (err) => console.error(err.message));8.2 服务端 keep-alive
HTTP/1.1 默认 Connection: keep-alive,socket 复用减少握手开销:
const server = http.createServer((req, res) => {
// 显式设置超时(默认 5 秒)
res.socket.setTimeout(60_000);
res.socket.on('timeout', () => res.end());
});生产里 keep-alive 通常交给反向代理(Nginx)和框架处理,裸 http 服务几乎不直接调。
九、HTTPS:TLS 套在 HTTP 上
HTTPS = HTTP over TLS:
https 模块 API 与 http 几乎一致,差别只在证书配置。详见 https 模块。
十、最佳实践
| 场景 | 推荐 | 反例 |
|---|---|---|
| 写 Web 服务 | 用框架(Express / Fastify / NestJS) | 手写 http.createServer 接业务 |
| body 解析 | 用中间件(body-parser / koa-body) | 每个接口 for await 累积 |
| 客户端请求 | fetch(Node 18+) | http.request |
| JSON 响应 | 抽 sendJson 工具函数 | 散落 JSON.stringify |
| 大文件下载 | 客户端用流(res.on('data')),服务端 res 已经是流 | JSON.parse(await res.text()) |
| keep-alive | 反向代理 + 框架处理 | 手动管理 socket |
| HTTP/2 | http2 模块或框架支持 | HTTP/1.1 大文件多请求 |
十一、小结
http是net之上的 HTTP 协议层;req/res都是流req是 Readable(body 是流),res是 Writable(body 也是流)- 完整响应:
statusCode+setHeader+end(data);最常用end一气呵成 - body 接收用
for await...of,记得限制大小 - 客户端:Node 18+ 直接用
fetch;老项目用http.request/http.get - 实际项目几乎都用框架;理解
http是看懂框架的基础
