人人都会AI编程

请求报文解析、响应报文构造

更新时间:2026-07-11

使用 Node.js 原生的 http 模块搭建服务时,每个请求会触发 createServer 回调,并传入 IncomingMessage 对象(通常命名为 req)和 ServerResponse 对象(res)。req 是一个 可读流,它包含了客户端发送过来的所有请求信息。开发者需要根据不同的需求,手动解析报文中的各个部分。

1. 请求行:方法、URL 和协议版本

请求报文的第一行称为“请求行”,包含 HTTP 方法、请求路径和 HTTP 协议版本。req 对象已经为你拆解好了这三个信息:

console.log(req.method);       // 例如:GET、POST、PUT
console.log(req.url);          // 例如:/api/user?id=1 (包含路径和查询字符串)
console.log(req.httpVersion);  // 例如:1.1

req.method 决定业务逻辑分支,req.url 则同时包含路径和查询参数。

2. 请求 URL 与查询字符串

原生的 http 模块并没有像 Express 那样直接将 req.query 提供给你,需要手动拆解 URL。Node.js 内置了 url 模块来完成这件事:

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const pathname = parsedUrl.pathname;  // 纯路径,如 /api/user
  const query = parsedUrl.query;        // 查询参数对象,如 { id: '1' }

  console.log('路径:', pathname);
  console.log('查询参数:', query);
});

自 Node.js v11 起,推荐使用 URL 类(全局可用,无需引入)来替代旧式的 url.parse

const myUrl = new URL(req.url, `http://${req.headers.host}`);
console.log(myUrl.pathname);  // /api/user
console.log(myUrl.searchParams.get('id'));  // 1

URL 类的 searchParams 提供了类似 Map 的接口,可以直接 .get() 获取单个参数,或 .has() 判断是否存在某参数,在代码可读性上比 url.parse 更好。但需要注意,new URL 的第二个参数必须是完整的基准地址,这里用 req.headers.host 动态拼接最为可靠。

3. 请求头

所有请求头都被集中放置在 req.headers 对象中。HTTP 头部名称在 Node.js 中会自动转换为小写,例如 Content-Type 会变成 req.headers['content-type']

console.log(req.headers['content-type']); // 'application/json'
console.log(req.headers['cookie']);       // 原始 Cookie 字符串

为了获取 Cookies,通常需要额外解析。可以不依赖第三方库,自己编写简单的解析函数:

function parseCookies(cookieHeader) {
  const cookies = {};
  if (!cookieHeader) return cookies;
  cookieHeader.split(';').forEach(pair => {
    const [key, ...rest] = pair.trim().split('=');
    if (key) cookies[key] = rest.join('=');
  });
  return cookies;
}

const cookies = parseCookies(req.headers.cookie);
console.log(cookies); // { token: 'abc123', theme: 'dark' }

4. 请求体(Body)

req 是一个流,请求体数据并不会在一次回调中全部到达,而是以数据块(chunk)的形式分批传输。因此,解析请求体必须监听 data 事件拼接数据,并在 end 事件中处理完整内容。

let body = [];

req.on('data', (chunk) => {
  body.push(chunk);   // chunk 是 Buffer
});

req.on('end', () => {
  body = Buffer.concat(body).toString();
  console.log('完整请求体:', body);

  // 根据 Content-Type 解析
  const contentType = req.headers['content-type'];
  if (contentType === 'application/json') {
    try {
      const jsonData = JSON.parse(body);
      console.log('JSON 数据:', jsonData);
    } catch (e) {
      // 返回 400 错误
    }
  } else if (contentType === 'application/x-www-form-urlencoded') {
    // 解析表单数据
    const params = new URLSearchParams(body);
    const formData = Object.fromEntries(params);
    console.log('表单数据:', formData);
  }
});

这种手动拼接的模式在实际项目中十分累赘,因此在多数场景下,我们会配合 Express 等框架内置的中间件(如 express.json()express.urlencoded())一次性完成解析。但理解原生流程对于排查问题、自定义高效中间件仍然非常重要。


9.1.3 响应报文构造

res 是一个 ServerResponse 对象,同时也是 可写流。构造响应报文的过程就是按顺序写入状态行、响应头、响应体,最后结束响应。

1. 状态码与状态消息

可以通过 res.statusCode 直接设置状态码,也可以调用 res.writeHead(statusCode, statusMessage, headers) 一次性写入状态行和响应头。

// 方式一:分开设置
res.statusCode = 200;
res.statusMessage = 'OK';

// 方式二:一次性设置(常用)
res.writeHead(200, 'OK', {
  'Content-Type': 'application/json'
});

如果不显式设置状态码,Node.js 默认为 200。实际开发中建议在每个分支明确设置,确保状态码符合 HTTP 规范。

2. 响应头设置

响应头可以在调用 writeHead 时作为对象传入,也可以使用 res.setHeader(key, value) 分步设置。一旦调用 res.write()res.end() 发送了响应体,头部将自动发送,之后不能再修改。

res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('X-Powered-By', 'Node.js');
res.setHeader('Set-Cookie', ['token=abc123; HttpOnly', 'lang=zh-CN']);

常见的响应头包括:

  • Content-Type:告诉客户端返回内容的格式,如 text/htmlapplication/jsonimage/png
  • Content-Length:响应体的字节长度。如果返回的是流且长度已知,设置此头可帮助客户端精确感知传输进度。
  • Set-Cookie:设置 Cookie,多个 Cookie 需要通过多个头或数组指定。

3. 响应体发送

响应体通过 res.write() 分批写入,或直接通过 res.end() 一次性写入并结束。

// 返回纯文本
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('Hello, World!');

// 返回 HTML
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('<h1>欢迎</h1>');

// 返回 JSON
const data = { status: 'success', message: '操作成功' };
res.end(JSON.stringify(data));

res.end() 可以接受一个字符串或 Buffer 作为参数,并同时触发发送和关闭连接。若需要分段写入,可以使用 res.write 多次写入后再调用 res.end

res.write('<html><body>');
res.write('<p>第一段</p>');
res.write('<p>第二段</p>');
res.end('</body></html>');

这种流式输出适用于分步生成的页面或文件下载场景。

4. 一个完整的报文构造示例

http.createServer((req, res) => {
  // 解析 URL(略)
  
  // 收集请求体(略)
  // ... 在 end 事件中业务处理完毕

  // 构造响应
  if (req.url === '/api/user' && req.method === 'POST') {
    // 假设拿到解析后的数据
    const user = { id: 1, name: 'Alice' };

    res.writeHead(201, {
      'Content-Type': 'application/json',
      'X-Request-Id': Date.now().toString()
    });
    res.end(JSON.stringify(user));
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end('Not Found');
  }
}).listen(3000);

5. 常见响应类型的 Content-Type 对照

| 数据类型 | Content-Type |
|-------------------|---------------------------------------------|
| JSON | application/json; charset=utf-8 |
| HTML | text/html; charset=utf-8 |
| 纯文本 | text/plain; charset=utf-8 |
| 表单提交 | application/x-www-form-urlencoded |
| 文件下载(二进制)| application/octet-stream |
| JPEG 图片 | image/jpeg |
| PNG 图片 | image/png |

设置正确的 Content-Type 和字符集,可以避免客户端乱码或错误解析。尤其是 JSON 接口,务必指定 charset=utf-8,因为 JSON 规范本身默认使用 UTF-8。

6. 结束响应后的注意事项

一旦调用了 res.end(),底层连接就会进入关闭流程(除非使用了 Connection: keep-alive 且显式不关闭)。之后不能再对 res 进行任何写入操作,否则会抛出错误。因此,在分支逻辑中要确保只有一个代码路径最终调用 end


小结

http 模块作为 Node.js 最底层的 Web 服务能力,将 HTTP 报文的解析和构造过程完全开放给了开发者。请求解析需要手动处理 URL、查询字符串、Cookie、流式请求体;响应构造则需要自行规划状态码、头部、内容类型,并精确控制流的写入和结束。

这些操作在真实项目中通常会被 Express、Koa 等框架封装和简化,但理解原生的报文处理机制,有助于你:

  • 在框架内部出问题时进行高效调试
  • 编写自定义的高性能中间件
  • 在需要极致控制时(如流式上传、大文件下载)摆脱框架束缚

下一节我们将继续探讨如何使用 http 模块发起客户端请求,并实现简单的反向代理功能。