Skip to content

Node系列 · Node基础:http 模块

http 模块建立在 net 之上:每条 HTTP 连接对应一个 socket,自动处理 HTTP 协议解析。理解 req / res 是"加了协议层的 socket",再学 Express / Koa 这类框架就只是"中间件 + 路由 + 上下文对象"的封装。

一、http 与 net 的关系

http 是基于 net 之上的协议层。req / res 是"加了 HTTP 解析的 socket"——本质都是流:

httpnet.Socket 加了两层能力:

  1. 协议解析:自动按 HTTP 格式解析请求行、请求头、请求体
  2. 协议构造:自动按 HTTP 格式拼装响应行、响应头、响应体

业务代码拿到的是 req(已解析的请求)和 res(待构造的响应),而不是裸 socket。

二、最小 HTTP 服务端

理解 http 最快的方式是看一个能跑的最小例子:createServer 接收 (req, res) 回调,配置 res 然后 end()

javascript
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');
});
bash
$ node http-server-mini.js &
$ curl http://127.0.0.1:3000
hello

三、req 对象:服务器收到的请求

reqhttp.IncomingMessage 实例,继承自 stream.Readable——body 是流:

属性 / 方法类型说明
req.methodstringHTTP 方法:GET / POST / PUT / DELETE
req.urlstringURL 路径 + 查询字符串(不含协议、host)
req.headersobject请求头(key 全小写)
req.httpVersionstringHTTP 版本('1.1' / '2.0'
req.socketnet.Socket底层 socket
req.on('data', cb)event每个 body chunk 触发
req.on('end', cb)eventbody 接收完毕

3.1 解析请求 URL

req.url 是相对路径,要拼上 host 才是完整 URL:

javascript
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 解析请求头

javascript
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 对象:构造响应

reshttp.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 三种写响应的方式

javascript
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 状态码速查

状态码含义常见场景
200OK成功
201Created创建资源成功
204No Content成功但无 body
301 / 302重定向永久 / 临时
304Not Modified缓存命中
400Bad Request客户端参数错
401Unauthorized未登录
403Forbidden无权限
404Not Found资源不存在
500Internal Server Error服务端错
502 / 503 / 504网关 / 服务 / 网关超时上游问题

4.4 一次性设置 JSON

javascript
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)

请求体是,必须手动累积:

javascript
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):

javascript
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 + urlif/else 分发:

javascript
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 通用方法

javascript
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

javascript
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 与浏览器一致:

javascript
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 客户端超时

javascript
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 复用减少握手开销:

javascript
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/2http2 模块或框架支持HTTP/1.1 大文件多请求

十一、小结

  • httpnet 之上的 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 是看懂框架的基础