编程语言研究书库

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 这类用操作系统原生异步
checksetImmediate(无延迟的定时器,名字骗人:它也得排队到下一 tick)
close callbacks连接关闭的收尾

8

两个本章最常踩的点。第一,同步调用绕过 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"

16

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. 可带走的

  1. Node = V8(跑 JS)+ 核心模块(接触 OS)+ libuv(异步 I/O 与事件循环)。
  2. 事件循环按 tick 转,每 tick 分 timers → pending → poll → check → close 阶段。
  3. 定时器不准点;同步调用(如 readFileSync)直接堵主线程;长回调堵队列。
  4. Node 的主场是 I/O 密集高并发;CPU 密集不是。
  5. Request 装进来的(URL/动词/头/体),Response 装回去的;res.end 不调,响应不完。
  6. POST 体是流:data 事件一块块收,end 事件收齐,Buffer 拼接。
  7. GET/PUT/DELETE 幂等,POST 不幂等。
  8. 状态码速记:200/301/400/401/403/404/500/502/503。
  9. 改代码自动重启:--watch 起步,nodemon 进阶。

9. 原文地图

主题原书章原文位置
Node 是运行时不是服务器框架Chapter 8: Understanding Node.jstext/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

  1. 出处:「Chapter 8: Understanding Node.js」第 87–95 段(text/43-ch08-chapter-8-understanding-node-js.txt:89,搜「runtime environment」;:95,搜「Just-In-Time」)。 ↩

  2. 出处:同上第 131–135 段(text/43-ch08-chapter-8-understanding-node-js.txt:133,搜「similar model to the browser」)。 ↩

  3. 出处:同上第 139–159 段(text/43-ch08-chapter-8-understanding-node-js.txt:141,搜「three main components」)。 ↩

  4. 出处:同上第 167–191 段(text/43-ch08-chapter-8-understanding-node-js.txt:185,搜「frequently executed paths」;:191,搜「ES Module system」)。 ↩

  5. 出处:同上第 205–243 段(text/43-ch08-chapter-8-understanding-node-js.txt:215,搜「native code, like C/C++ or Rust, but run」;:243,搜「standard library」)。 ↩

  6. 出处:同上第 245–291 段(text/43-ch08-chapter-8-understanding-node-js.txt:253,搜「event loop and internal thread pool」;:287,搜「not threads」)。 ↩

  7. 出处:同上第 293–335 段(text/43-ch08-chapter-8-understanding-node-js.txt:303,搜「runner in libuv, similar to the Chrome e」;:323,搜「tick」;:331,搜「phases」)。 ↩

  8. 出处:同上第 343–421 段(text/43-ch08-chapter-8-understanding-node-js.txt:365,搜「not provide guaranteed」;:403,搜「ironically」;:409,搜「close event」)。 ↩

  9. 出处:同上第 423–441 段(text/43-ch08-chapter-8-understanding-node-js.txt:435,搜「block the main thread」)。 ↩

  10. 出处:同上第 465–471 段(text/43-ch08-chapter-8-understanding-node-js.txt:467,搜「long-running callback」)。 ↩

  11. 出处:同上第 473–493 段(text/43-ch08-chapter-8-understanding-node-js.txt:475,搜「I/O-bound」;:491,搜「better fit」)。 ↩ ↩2

  12. 出处:同上第 876–954 段(text/43-ch08-chapter-8-understanding-node-js.txt:929,搜「HTTP status codes」;:943,搜「never complete」)。 ↩

  13. 出处:同上第 1016–1024 段(text/43-ch08-chapter-8-understanding-node-js.txt:1024,搜「same code」)。 ↩

  14. 出处:同上第 1565–1581 段(text/43-ch08-chapter-8-understanding-node-js.txt:1573,搜「difficult to read」;:1581,搜「middleware pipeline」)。 ↩ ↩2

  15. 出处:同上第 1400–1532 段(text/43-ch08-chapter-8-understanding-node-js.txt:1458,搜「sent as a stream」;:1464,搜「when each piece」;:1514,搜「container in memory」)。 ↩

  16. 出处:同上第 1534–1563 段(text/43-ch08-chapter-8-understanding-node-js.txt:1548,搜「without using a browser」;:1554,搜「Content-Type: application/json」)。 ↩

  17. 出处:同上第 1088–1145 段(text/43-ch08-chapter-8-understanding-node-js.txt:1094,搜「verb」;:1100,搜「GET」;:1144,搜「most commonly as JSON」)。 ↩

  18. 出处:同上第 1154–1180 段(text/43-ch08-chapter-8-understanding-node-js.txt:1162,搜「same result as performing it once」;:1178,搜「two separate records」)。 ↩

  19. 出处:同上第 1196–1250 段(text/43-ch08-chapter-8-understanding-node-js.txt:1206,搜「indicates success.」;:1240,搜「limited detail」)。 ↩

  20. 出处:同上第 1252–1320 段(text/43-ch08-chapter-8-understanding-node-js.txt:1256,搜「metadata」;:1274,搜「JWT authentication」;:1308,搜「Access-Control-Allow-Origin」)。 ↩

  21. 出处:同上第 1583–1599 段(text/43-ch08-chapter-8-understanding-node-js.txt:1591,搜「burying the actual error」;:1599,搜「never assume」)。 ↩ ↩2

  22. 出处:同上第 1603–1787 段(text/43-ch08-chapter-8-understanding-node-js.txt:1615,搜「built-in debugger」;:1777,搜「continue button」)。 ↩

  23. 出处:同上第 1793–1955 段(text/43-ch08-chapter-8-understanding-node-js.txt:1803,搜「nodemon」;:1950,搜「runtimeExecutable」)。 ↩

On this page