Node.js — V8、libuv 与事件循环,以及裸写 HTTP 服务器
Node.js — V8、libuv 与事件循环,以及裸写 HTTP 服务器
这一章讲什么: 前端那半本书用的是浏览器里的 JavaScript;从这一章起, 同样的语言跑到服务器上——靠的就是 Node。先拆 Node 的三块内脏 (V8、内置模块、libuv 事件循环),再用纯 Node、不用任何框架手写一个 HTTP 服务器。手写是为了受罪:等你被 if-else 路由和手动拼流折磨过, Express 为什么存在就不言而喻了。
1. Node 是什么:浏览器模型的服务器版
回答「Node 是什么」最好的类比是浏览器:浏览器本身是一个 JavaScript 运行时—— 核心引擎(解析 + JIT 即时编译,把高频执行的代码现场编译成机器码)+ 沙箱(限制权限的运行容器)+ 额外的 Web API(DOM、fetch 之类)1。Node 照搬这个模型: 也用一个核心引擎,也提供一个运行时容器,只是额外能力从「画图」换成了 「接触操作系统」2。
具体是三块3:
┌─────────────────────────────────────────────┐
│ 你的 JavaScript 代码 │
├──────────────┬──────────────────────────────┤
│ V8 引擎 │ 内置核心模块 (fs/http/…) │
│ (跑 JS) │ (多数用 C/C++/Rust 写成, │
│ │ 是 Node 的「标准库」) │
├──────────────┴──────────────────────────────┤
│ libuv:跨平台 C 库,提供异步 I/O、 │
│ 事件循环和内部线程池 │
└─────────────────────────────────────────────┘
图说:V8 不会跟操作系统打交道(这是浏览器的安全传统),
所以文件、网络、进程这些能力全是核心模块经 libuv 借来的。V8 是从 Chrome 里搬来的 JavaScript 引擎,负责语言本体:类型、函数、类、 模块系统4。内置核心模块(fs、http、path、crypto、dns、net、timers) 是 Node 的标准库——它们多数用 C/C++/Rust 写成、在 JavaScript 环境里跑, 这就是 Native Modules 机制,性能敏感的部分因此能享受原生语言的速度5。
2. 事件循环:可伸缩性的发动机
libuv 是 Node 可伸缩性的心脏:它把「等」这件事从主线程上拿走—— 定时器、网络连接、文件系统,全变成异步 I/O;确实没法异步的同步任务, 才交给内部线程池去「装成异步」6。
事件循环(event loop)是 libuv 里的任务调度器:一个持续转圈的循环, 每圈叫一个 tick;每圈内部又分阶段(phase),每个阶段守着一条 回调队列7。顺序 worth 记住:
| 阶段 | 干什么 |
|---|---|
| timers | 到点的 setTimeout/setInterval 回调入队——注意定时器不保证准点,到点后回调排在下一 tick |
| pending callbacks | 上一轮没执行完的 I/O 回调 |
| poll | 执行 I/O 回调;文件、DNS 这类交给线程池,fetch 这类用操作系统原生异步 |
| check | setImmediate(无延迟的定时器,名字骗人:它也得排队到下一 tick) |
| close callbacks | 连接关闭的收尾 |
两个本章最常踩的点。第一,同步调用绕过 libuv:readFileSync 直接砸在
主线程上,卡到完成为止9。第二,回调本身可能不是异步的——一个跑得
久的回调会堵住整条队列,后面的回调只能等10。结论就是 Node 的能力边界:
它天生为高并发、I/O 密集的工作负载而生;CPU 重的活(worker threads
从 v10.5 起有)不是它的主场,那种活 Go 或 Rust 更合适11。
3. 主线例子:纯 Node 手写 HTTP 服务器
用核心 http 模块,十几行起一个服务器:
import http from "http";
const server = http.createServer((req, res) => {
res.end("hello world");
});
server.listen(8000, () => console.log("Server started on port 8000"));createServer 收一个处理函数,每个请求进来时以两个对象调用它:
req(Request)装请求的一切——URL、头部(headers,随请求附带的说明信息)、动词;res(Response)是
你要寄回去的答复——状态码、头部、正文。res.end 必须调,不调响应永远
不完结,浏览器永远空等**12。此时任何 URL 都回同样的 "hello world",
因为没有路由区分13。
第一处受罪:手写路由
if (req.url === "/") → "hello world"
else if (req.url === "/a") → "welcome to route a"
else if (req.url === "/b") → "welcome to route b"
else → "good bye"能跑,但这坨 if-else 只要再上规模就成了灾难——这就是 Express 存在的 全部理由:下一章它会给你结构化的路由和中间件管道14。
第二处受罪:POST、流与 Buffer
处理 /c 的 POST 要写成这样:
req.on("data", (chunk) => { body.push(chunk); }); // 每来一块,收一块
req.on("end", () => { // 收完了
const params = Buffer.concat(body); // 拼成一个 Buffer
res.end(`You submitted: ${params.toString()}`);
});为什么?因为 Node 很低层,不替你藏细节:请求体是以流(stream)来的——
数据分块(切成一段段)到达,网络延迟决定哪块先到;所以要用事件(data/end)一块块接。
Buffer 是内存里装任意二进制数据的容器,拼完再转回字符串15。
测它要用 curl(命令行发网络请求的工具):
curl --header "Content-Type: application/json" --request POST \
--data '{"userid": 1,"message": "hello"}' \
"http://localhost:8000/c"4. HTTP 共同语言:动词、幂等、状态码、头部
这一课是后面所有章节(Express、认证、部署)的公共词汇。
动词描述意图:GET 取数据、POST 创建、PUT 整体替换、PATCH 部分更新、 DELETE 删除。GET 的参数走 URL(问号后的 querystring,或藏在路径里的 params);POST/PUT 的参数走请求体,通常是 JSON17。
幂等性(idempotency):一个操作执行多次和执行一次效果相同,它就幂等。 GET、PUT、DELETE 幂等;POST 不幂等——同一个 POST 提交两遍,通常会 创建两条记录。设计需要重试的 API 时,这条分界线是要命的设计约束18。
状态码,先记最常撞见的几个:200 成功;301 永久搬家;400 请求本身有问题 (参数错或缺);401 没权限(未认证);403 认证了但没授权;404 路径不存在; 500 服务器内部错(出于安全给得很含糊);502 网关上游坏了;503 服务不可用19。
头部(headers,随请求一起发送的说明信息)是请求的元数据。请求侧:Host(目标域)、Authorization
(权限凭证,第 13 章的 JWT 就住这里)、Cookie、Origin(哪个域发的请求,
跨域问题的入口)、Content-Type(正文是什么格式)。响应侧:Set-Cookie、
Access-Control-Allow-Origin/Methods(跨域放行)、Content-Type20。
书里还有一句大实话:现实世界的 API 经常不守这些约定——有的出错也回 200 把错误埋在正文里,有的拿 POST 当 GET 用。对接第三方时,文档比规范 更值得相信21。
5. 调试与自动重启
VSCode 内置调试器:Run and Debug → 选 Node.js → 点行号左侧打断点,
代码跑到那里就停;左侧 VARIABLES 看当前作用域变量、WATCH 盯你关心的值、
CALL STACK 看调用链、DEBUG CONSOLE 能现场敲表达式;顶栏的继续/单步越过/
单步进入/单步跳出/停止控制推进22。改代码要重启进程这件事,简单场景用
Node 自带的 --watch(node --watch server.js),讲究的场景(指定忽略
目录、配合调试器)用 nodemon——"start": "nodemon server.js",
本地文件一变自动重启;想让调试会话也享受自动重启,把 launch.json 的
runtimeExecutable 换成 nodemon 并加 "restart": true23。
6. 作者的判断与证据
- 「Node 适合 I/O 密集的高并发负载;CPU 密集选 Go/Rust」是明确的能力边界 判断,依据是事件循环 + 单主线程的架构事实11。
- 「手写路由是受罪,Express 来救场」是刻意设计的教学安排,非性能论证14。
- 「真实 API 经常违反 HTTP 约定,以文档为准」是经验之谈,无法证伪但极其实用21。
7. 边界与局限
- 事件循环只讲了阶段顺序,没展开
process.nextTick和微任务队列 (Promise)与宏任务的插队关系——面试常客,本书略过。 - stream 只露了一面:可写流、转换流、背压这些真实项目高频概念没有展开。
- cluster/多进程没提——单机榨干多核的标配做法缺席。
- 版本号(Node 26.2.0、watch 自 18.11 稳定)是 2026 时点。
判断(我们的,不是书里的): 本章的教学顺序(先手写受罪、再给框架) 和 Express 章形成一次完整的「问题→解药」配对,这比直接从 Express 开讲 更能建立正确的心智模型——你会知道框架「遮住」了什么,将来调试底层行为 时才知道往哪看。「同步调用绕过 libuv」和「长回调堵队列」两条,是 Node 世界里性能 bug 的两条主要来路,原书把它们放在架构讲解里讲,位置放得对。 如果错,会错在: 对完全没有后端经验的读者,「受罪」段的细节密度 (流、Buffer、事件)可能先于理解堆积起来;这章自认面向「中级开发者」, 零基础读者的摩擦力会比作者预期的大。
8. 可带走的
- Node = V8(跑 JS)+ 核心模块(接触 OS)+ libuv(异步 I/O 与事件循环)。
- 事件循环按 tick 转,每 tick 分 timers → pending → poll → check → close 阶段。
- 定时器不准点;同步调用(如 readFileSync)直接堵主线程;长回调堵队列。
- Node 的主场是 I/O 密集高并发;CPU 密集不是。
- Request 装进来的(URL/动词/头/体),Response 装回去的;res.end 不调,响应不完。
- POST 体是流:data 事件一块块收,end 事件收齐,Buffer 拼接。
- GET/PUT/DELETE 幂等,POST 不幂等。
- 状态码速记:200/301/400/401/403/404/500/502/503。
- 改代码自动重启:--watch 起步,nodemon 进阶。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| Node 是运行时不是服务器框架 | Chapter 8: Understanding Node.js | text/43-ch08-chapter-8-understanding-node-js.txt:81(搜「not a server-specific」) |
| 浏览器类比 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:87(搜「analogy to the web browser」) |
| 三组件:V8/核心模块/libuv | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:141(搜「three main components」) |
| V8 与 JIT | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:181(搜「Just-In-Time」) |
| Native Modules 与核心模块 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:215(搜「native code, like C/C++ or Rust, but run」) · text/43-ch08-chapter-8-understanding-node-js.txt:243(搜「standard library」) |
| libuv 与线程池 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:251(搜「cross-platform」) |
| 事件循环与 tick/阶段 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:299(搜「event」) · text/43-ch08-chapter-8-understanding-node-js.txt:323(搜「tick」) |
| 阶段顺序 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:343(搜「run in the following order」) |
| 同步绕过 libuv | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:431(搜「bypass」) |
| 长回调堵队列 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:467(搜「block the event loop」) |
| Node 的能力边界 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:475(搜「asynchronous I/O-bound」) |
| createServer 与 res.end | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:880(搜「createServer」) · text/43-ch08-chapter-8-understanding-node-js.txt:937(搜「res.end」) |
| 手写路由的局限 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1565(搜「not ideal」) · text/43-ch08-chapter-8-understanding-node-js.txt:1581(搜「middleware pipeline」) |
| 流与 Buffer | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1458(搜「stream」) · text/43-ch08-chapter-8-understanding-node-js.txt:1514(搜「container in memory」) |
| HTTP 动词 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1094(搜「HTTP verb」) |
| 幂等性 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1158(搜「idempotency」) |
| 状态码清单 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1196(搜「Status code」) |
| 请求/响应头部 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1254(搜「Headers provide」) |
| 现实 API 不守约定 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1591(搜「burying the actual error」) |
| VSCode 调试器 | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1615(搜「built-in debugger」) |
| nodemon 与 launch.json | 同上 | text/43-ch08-chapter-8-understanding-node-js.txt:1803(搜「nodemon」) · text/43-ch08-chapter-8-understanding-node-js.txt:1950(搜「runtimeExecutable」) |
Footnotes
-
出处:「Chapter 8: Understanding Node.js」第 87–95 段(
text/43-ch08-chapter-8-understanding-node-js.txt:89,搜「runtime environment」;:95,搜「Just-In-Time」)。 ↩ -
出处:同上第 131–135 段(
text/43-ch08-chapter-8-understanding-node-js.txt:133,搜「similar model to the browser」)。 ↩ -
出处:同上第 139–159 段(
text/43-ch08-chapter-8-understanding-node-js.txt:141,搜「three main components」)。 ↩ -
出处:同上第 167–191 段(
text/43-ch08-chapter-8-understanding-node-js.txt:185,搜「frequently executed paths」;:191,搜「ES Module system」)。 ↩ -
出处:同上第 205–243 段(
text/43-ch08-chapter-8-understanding-node-js.txt:215,搜「native code, like C/C++ or Rust, but run」;:243,搜「standard library」)。 ↩ -
出处:同上第 245–291 段(
text/43-ch08-chapter-8-understanding-node-js.txt:253,搜「event loop and internal thread pool」;:287,搜「not threads」)。 ↩ -
出处:同上第 293–335 段(
text/43-ch08-chapter-8-understanding-node-js.txt:303,搜「runner in libuv, similar to the Chrome e」;:323,搜「tick」;:331,搜「phases」)。 ↩ -
出处:同上第 343–421 段(
text/43-ch08-chapter-8-understanding-node-js.txt:365,搜「not provide guaranteed」;:403,搜「ironically」;:409,搜「close event」)。 ↩ -
出处:同上第 423–441 段(
text/43-ch08-chapter-8-understanding-node-js.txt:435,搜「block the main thread」)。 ↩ -
出处:同上第 465–471 段(
text/43-ch08-chapter-8-understanding-node-js.txt:467,搜「long-running callback」)。 ↩ -
出处:同上第 473–493 段(
text/43-ch08-chapter-8-understanding-node-js.txt:475,搜「I/O-bound」;:491,搜「better fit」)。 ↩ ↩2 -
出处:同上第 876–954 段(
text/43-ch08-chapter-8-understanding-node-js.txt:929,搜「HTTP status codes」;:943,搜「never complete」)。 ↩ -
出处:同上第 1016–1024 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1024,搜「same code」)。 ↩ -
出处:同上第 1565–1581 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1573,搜「difficult to read」;:1581,搜「middleware pipeline」)。 ↩ ↩2 -
出处:同上第 1400–1532 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1458,搜「sent as a stream」;:1464,搜「when each piece」;:1514,搜「container in memory」)。 ↩ -
出处:同上第 1534–1563 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1548,搜「without using a browser」;:1554,搜「Content-Type: application/json」)。 ↩ -
出处:同上第 1088–1145 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1094,搜「verb」;:1100,搜「GET」;:1144,搜「most commonly as JSON」)。 ↩ -
出处:同上第 1154–1180 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1162,搜「same result as performing it once」;:1178,搜「two separate records」)。 ↩ -
出处:同上第 1196–1250 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1206,搜「indicates success.」;:1240,搜「limited detail」)。 ↩ -
出处:同上第 1252–1320 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1256,搜「metadata」;:1274,搜「JWT authentication」;:1308,搜「Access-Control-Allow-Origin」)。 ↩ -
出处:同上第 1583–1599 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1591,搜「burying the actual error」;:1599,搜「never assume」)。 ↩ ↩2 -
出处:同上第 1603–1787 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1615,搜「built-in debugger」;:1777,搜「continue button」)。 ↩ -
出处:同上第 1793–1955 段(
text/43-ch08-chapter-8-understanding-node-js.txt:1803,搜「nodemon」;:1950,搜「runtimeExecutable」)。 ↩