CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Node.js 后端入门/课时/http 模块
RawBlame
课时目录12 节

Node.js 后端入门

  1. 01Node.js 是什么
  2. 02运行时与 process
  3. 03模块与 npm
  4. 04文件系统与 path
  5. 05http 模块
  6. 06Express 入门
  7. 07事件循环与异步
  8. 08环境变量与配置
  9. 09Streams 基础
  10. 10错误处理
  11. 11测试 node:test
  12. 12综合练习与选型

http 模块

原生 createServer、请求/响应、最小 JSON API。

35 分钟

http 模块

这一课你将能做到

  1. 用零依赖的 node:http 起一个 HTTP 服务器并处理请求、返回响应
  2. 手工解析路径、查询参数和 POST 的 JSON 请求体
  3. 解释 req/res 是两条流,说清状态码与 Content-Type 的作用

为什么要学

Express、Koa 这些框架最终都是对 http 模块的封装。先用原生模块写一遍服务器,你才能回答:路由到底是什么?中间件在哪一层工作?express.json() 到底替你干了什么?

人话:http.createServer 就是 Node 帮你把 TCP 里的字节流翻译成「一个请求对象 req、一个响应对象 res」,剩下的路由判断、参数解析、状态码返回都要自己动手。这课写的每个手工环节,都会在下一课被 Express 一一「收编」——先手工再框架,框架对你就是透明的。

先建立图像

浏览器: GET /hello?name=Java
   │  TCP 连接(默认 127.0.0.1:3000)
   ▼
┌─ http.createServer((req, res) => { ... }) ─┐
│  req: method / url / headers / body(流)     │
│  res: setStatus / setHeader / write / end   │
└─────────────────────────────────────────────┘
   │
   ▼
浏览器收到: 状态码 200 + 响应头 + 响应体

一次请求的生命周期 = 建连接 → 回调拿到 req/res → 你处理 → res.end() → 关闭

req 和 res 本质都是流:req 是可读流(请求体一点一点到),res 是可写流(响应体一点一点发)。这解释了两件事:为什么 POST body 要监听事件来收,为什么大响应不用一次凑齐内存。

跟着做

步骤 1:最小服务器

新建 node-http 目录,npm init -y,新建 server.js:

// server.js
const http = require('node:http');

const server = http.createServer((req, res) => {
  console.log('收到请求:', req.method, req.url);

  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.end('hello from node http');
});

const PORT = 3000;
server.listen(PORT, () => {
  console.log(`服务器已启动: http://localhost:${PORT}`);
});
node server.js
# 服务器已启动: http://localhost:3000

浏览器访问 http://localhost:3000,看到 hello from node http;终端会打印 收到请求: GET /。浏览器还会额外发一次 GET /favicon.ico,属正常现象。

按 Ctrl+C 停止。改了代码要手动重启(下一课的 nodemon / node --watch 解决这个)。

步骤 2:加路由与查询参数

升级 server.js,用 new URL() 解析请求:

// server.js
const http = require('node:http');

const server = http.createServer((req, res) => {
  // 第二个参数是基准地址,用来把 req.url 解析成结构化对象
  const url = new URL(req.url, `http://${req.headers.host}`);
  const pathname = url.pathname;         // /hello
  const name = url.searchParams.get('name') ?? 'world';

  if (pathname === '/hello') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end(`hello, ${name}`);
    return;
  }

  res.statusCode = 404;
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.end('Not Found');
});

server.listen(3000, () => console.log('http://localhost:3000'));
node server.js

测试(新开一个终端,或用浏览器地址栏):

curl "http://localhost:3000/hello?name=Java"
# hello, Java

curl "http://localhost:3000/other"
# Not Found

结构:每个分支负责一条路由,处理完 return,兜底返回 404。这就是所有框架路由的本质。

步骤 3:返回 JSON

API 服务的标配。在 server.js 里加一条路由:

  if (pathname === '/api/time') {
    const data = { now: new Date().toISOString() };
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.end(JSON.stringify(data));
    return;
  }
curl http://localhost:3000/api/time
# {"now":"2026-10-06T06:45:12.345Z"}

要点:JSON.stringify 负责把对象变字符串,Content-Type: application/json 告诉调用方按 JSON 解析。两件事缺一不可——只 stringify 不设头,前端 response.json() 可能拿不到预期行为。

步骤 4:接收 POST 的 JSON 请求体

req 的 body 不会自动给你,要像读文件一样把它「读」出来:

// server.js 增加
function readBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.setEncoding('utf8');
    req.on('data', (chunk) => { body += chunk; });     // 一块一块到
    req.on('end', () => resolve(body));                // 读完触发 end
    req.on('error', reject);
  });
}

// 在 createServer 回调里增加:
  if (pathname === '/api/echo' && req.method === 'POST') {
    const raw = await readBody(req);
    let data;
    try {
      data = JSON.parse(raw);
    } catch {
      res.statusCode = 400;
      res.setHeader('Content-Type', 'application/json; charset=utf-8');
      res.end(JSON.stringify({ error: '请求体不是合法 JSON' }));
      return;
    }
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.end(JSON.stringify({ youSent: data }));
    return;
  }

记得把 createServer 的回调改成 async。用 curl 测:

curl -X POST http://localhost:3000/api/echo ^
  -H "Content-Type: application/json" ^
  -d "{\"msg\":\"hi\"}"
# {"youSent":{"msg":"hi"}}

curl -X POST http://localhost:3000/api/echo -d "not-json"
# {"error":"请求体不是合法 JSON"}

(PowerShell 里 curl 是 Invoke-WebRequest 的别名,带引号的 JSON 可改用 curl.exe 或用 Node 测试:node -e "fetch('http://localhost:3000/api/echo',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({msg:'hi'})}).then(r=>r.json()).then(console.log)"——Node 20 内置 fetch。)

步骤 5:Content-Type 与静态返回

再体会一次响应头的作用——返回 HTML:

  if (pathname === '/page') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    res.end('<h1>标题</h1><p>这是 http 模块吐出的页面</p>');
    return;
  }

浏览器访问 http://localhost:3000/page,<h1> 被渲染成标题——因为浏览器看到 text/html 就按 HTML 解析。如果 Content-Type 写成 text/plain,同样的内容就显示为源码。Content-Type 决定接收方怎么解读字节。

讲清楚要点

  1. createServer(callback) 注册的是「每个请求执行一次」的函数:server.listen(PORT) 才真正开始监听。回调里的代码没有 if/else 兜底就等于所有路由共用一段逻辑。
  2. req 是可读流,res 是可写流:POST/PUT 的 body 分块到达,必须监听 data/end 拼起来(或用框架的 body 解析中间件);res 调 end() 才算响应完成,end 后不能再写。
  3. 状态码 + Content-Type 是响应的两大元信息:200 成功、201 已创建、400 客户端参数错、401 未认证、404 找不到、500 服务器内部错。JSON 接口永远 application/json; charset=utf-8。
  4. 解析 URL 用 new URL(req.url, base):req.url 只有路径和查询串(如 /hello?name=Java),配 host 基准后,url.pathname、url.searchParams 才可用。
  5. 原生 http 没有路由表、没有中间件、没有 body 解析:这些正是 Express 补齐的部分。手写过一遍,第 6 课每个概念你都有对照物。

你可能会懵的地方

  • 为什么浏览器访问会「请求两次」? 第二次通常是 GET /favicon.ico——浏览器自动要网站图标。服务器没处理就返回 404,无害。加个判断直接回 204 可消除噪音。

  • req.url 和完整网址的区别? 服务器端拿到的 req.url 永远只有 /path?query 部分,主机名在 req.headers.host 里。所以 new URL(req.url, ...) 需要第二个基准参数。

  • POST body 为什么不是 req.body 直接可读? 因为数据是从网络一条一条到达的流,http 模块不替你缓存拼装(省内存、可边收边处理)。拼装这个体力活就是 Express express.json() 中间件的工作。

  • res.end() 之后还能改状态码吗? 不能。响应头在第一次 write/end 时发出,之后再 setHeader/statusCode 会报 ERR_HTTP_HEADERS_SENT。所以「先定状态码和头,再写体,最后 end」的顺序要固定。

  • 这服务器能上生产吗? 能跑但裸:没有日志、没有 body 大小限制(恶意大 body 会吃内存)、没有 HTTPS、没有集群。练手和内网工具够用;对外服务用框架 + 反向代理(Nginx)。

错法对照

错法问题正解
忘了 server.listen进程不退出但端口没开,浏览器拒绝连接createServer 后必须 listen(PORT, cb)
先 res.end() 再 setHeaderERR_HTTP_HEADERS_SENT状态码/头 → 写体 → end
GET 路由里直接读 req.bodyGET 没有 body,得到 undefined参数用 url.searchParams,body 只在 POST/PUT
JSON 接口不设 Content-Type调用方按文本处理,response.json() 失败设 application/json; charset=utf-8
不限长度的 body 直接 body += chunk恶意请求可撑爆内存检查 content-length,超限回 413 并 req.destroy()
改完代码不重启新逻辑不生效,怀疑人生Ctrl+C 重启,或用 node --watch

卡点急救

  1. EADDRINUSE: address already in use:3000 端口被上一个没关干净的进程占着。关掉旧终端;或换端口;Windows 查占用:netstat -ano | findstr :3000 后 taskkill /PID <pid> /F。
  2. 浏览器一直转圈不返回:某个分支没调用 res.end(),连接挂起。检查所有分支是否都有 end,兜底 404 分支必不可少。
  3. 中文乱码:响应头没带 charset=utf-8,或源文件本身不是 UTF-8 保存。两处都改。
  4. POST 收到的 body 是空字符串:curl 没带 -d 或客户端没发 body;也常见于忘了等 end 事件就 JSON.parse。
  5. 局域网手机访问不到:listen(3000) 默认监听所有网卡没问题,查 Windows 防火墙放行,或临时用 listen(3000, '0.0.0.0')。

面试常问

  • 用原生 http 模块创建服务器的流程? http.createServer(handler) 创建实例,handler 在每个请求到达时收到 req(可读流)与 res(可写流);server.listen(port) 监听端口;handler 内完成路由判断、头部设置、写响应体、res.end() 结束响应。

  • Node 如何处理 POST 请求体? req 是可读流:监听 data 事件累积 chunk、end 事件表示接收完毕,可在此期间做长度限制与编码转换;框架的 body parser 做的正是这件事(如 express.json 按 content-type 解析并挂到 req.body)。

  • 常见的 HTTP 状态码有哪些?语义是什么? 2xx 成功(200 OK、201 Created、204 No Content);3xx 重定向(301 永久、302 临时、304 缓存);4xx 客户端错误(400 参数、401 未认证、403 禁止、404 不存在、429 太频繁);5xx 服务端错误(500 内部错误、502 网关错、503 不可用)。

  • 为什么说 Node 适合 I/O 密集的 Web 服务? 请求处理多为等待数据库/网络返回,Node 的事件循环在等待期间可处理其他连接,少量线程即可承载高并发;相反 CPU 密集会阻塞循环,需要子进程或换技术栈。

自测三问

  1. http.createServer 的回调什么时候执行?listen 之前会执行吗?
  2. 写出「返回 JSON 数据」必不可少的三行代码(状态码、头、体)。
  3. 为什么读 POST body 必须监听 data 和 end 事件?

小结一句话

原生 http 模块 = 「把 TCP 字节翻译成 req/res 两条流」:路由自己 if、body 自己收、头自己设——手工走一遍,Express 的每一层封装就都看得懂了。

延伸

  • Node.js 官方文档 http
  • MDN HTTP 状态码
  • 下一课:Express 入门——用中间件和路由表把本课的手工活全部收编。
加载本节练习…
← 上一课文件系统与 path下一课 →Express 入门