- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
本篇技术指南基于 learnyounode 第 8 个练习"HTTP 文件服务器"(对应文档 exercises/http_file_server/problem.zh-cn.md)展开,讲解如何用 Node.js 核心模块http编写一个"每次请求都返回同一文本文件"的 HTTP 服务器,并强制要求使用fs.createReadStream()以流(stream)的方式把文件内容管道(pipe)进响应。读完本文,你将掌握http.createServer()的请求-响应模型、request/response的流本质、src.pipe(dst)的流式传输写法,以及 learnyounode 的自动校验机制是如何验证你是否真的用了流式 API 的。
练习目标:参数驱动、流式响应的文件服务器
这道题的目标非常聚焦:编写一个名为http-file-server.js的脚本,实现一个 HTTP 文件服务器,把"每次收到的请求"都响应为同一个文本文件的内容。它与前面基于net模块的 time_server 练习形成对照——本练习要求的是HTTP 服务器,而不是通用 TCP 服务器。
程序通过两个命令行参数接收配置:
| 参数位置 | 含义 | 用法 |
|---|---|---|
第一个参数(process.argv[2]) | 服务器要监听的端口号 | 传给server.listen(portNumber) |
第二个参数(process.argv[3]) | 要返回给客户端的文本文件路径 | 传给fs.createReadStream() |
原文档特别强调:在这一题中,你必须使用fs.createReadStream()方法以 stream 的形式作出请求响应。这句话不是建议,而是校验的硬性门槛——仓库的验证器会检查你的程序是否只调用了createReadStream这一文件读取 API(详见下文"验证机制"一节)。
为什么用 http 而不是 net
原文档明确指出:因为需要创建的是 HTTP 服务而不是普通 TCP 服务,所以应使用 Node 核心模块http。它与net模块的关系非常密切:
net模块提供net.createServer(),创建的是基于 TCP 的裸连接服务器,适合自定义二进制协议或字节流传输(这正是上一题 time_server 的场景);http模块提供http.createServer(),内部同样建立在 TCP 之上,但它额外实现了 HTTP 协议解析:能识别请求行、请求头(header)、查询字符串(query-string),并能向客户端写出响应头和响应体。
因此对"返回文件内容给浏览器/客户端"这种场景,http是正确选择。官方模块文档详见仓库内下载好的 docs-nodejs/http.html。
http.createServer 的回调模型
http.createServer()接收一个回调函数作为参数,服务器每次收到连接/请求时都会调用一次。回调签名固定为:
function callback (request, response) { /* ... */ }两个参数的含义:
request:代表本次 HTTP 请求的对象,用来获取请求的属性,例如请求头(header)和查询字符串(query-string);response:代表本次请求对应的响应对象,用来向客户端发送数据,既包括响应头(headers),也包括响应主体(body)。
http.createServer()还会返回一个服务器实例,你需要调用server.listen(portNumber)去监听特定端口。一个典型的 Node HTTP 服务器骨架如下(来自原文档提示):
const http = require('http') const server = http.createServer(function (req, res) { // 处理请求的逻辑... }) server.listen(8000)request 与 response 都是 Node 流
这是本练习的核心认知:原文档特别强调,request和response也都是 Node stream。这意味着:
- 你可以用流式处理(streaming)所抽象的那些方法来实现数据的发送和接收;
- 这正是本练习把
fs文件流与http响应流"对接"的底层依据——pipe()方法要求两端都是流,而response恰好是可写流(Writable),可以直接作为pipe的目标。
核心写法:fs.createReadStream().pipe(res)
fs核心模块含有针对文件的流式 API。fs.createReadStream()会为指定的文件创建一个可读流(Readable),该方法返回的流对象支持src.pipe(dst)语法,把数据从src流传输到dst流。用这种形式,可以轻松地把一个文件系统的 stream 和一个 HTTP 响应的 stream 连接起来。
官方给出的"最小接线"写法是:
fs.createReadStream(process.argv[3]).pipe(res)这一行的执行逻辑:Node 从磁盘以 chunk(数据块)形式读取文件,每读到一块就立即写入res(HTTP 响应),全程不需要把整个文件读入内存,也不需要手动调用res.end()——当文件流读完并结束(end事件)时,pipe会自动结束响应流,客户端收到完整响应体。
完整可运行的参考实现
仓库的官方解法位于 exercises/http_file_server/solution/solution.js,全文如下:
'use strict' const http = require('http') const fs = require('fs') const server = http.createServer(function (req, res) { res.writeHead(200, { 'content-type': 'text/plain' }) fs.createReadStream(process.argv[3]).pipe(res) }) server.listen(Number(process.argv[2]))要点拆解:
res.writeHead(200, { 'content-type': 'text/plain' }):在写出主体前先写响应头,状态码 200 表示成功,content-type声明为纯文本;fs.createReadStream(process.argv[3]).pipe(res):把第二个参数指定的文件以流的方式灌入响应,这是题目强制的核心写法;server.listen(Number(process.argv[2])):监听第一个参数给定的端口。注意这里用Number()把字符串参数转成数字,因为listen()对端口参数的类型有严格要求。
启动方式(原文档对应的验证命令,{appname}即 learnyounode 命令):
$ learnyounode verify http-file-server.js也可以先手动运行再测试:
$ node http-file-server.js 8000 /path/to/file.txt然后用浏览器或curl访问http://localhost:8000查看返回的文件内容。
验证机制:如何保证你真的用了 createReadStream
这道题不允许用"把整个文件读进内存再res.end()"的笨办法,验证逻辑位于 exercises/http_file_server/exercise.js。从源码结构可以还原出完整的校验链路:
- 运行期对比:练习使用
workshopper-exercise/execute以spawn()并行启动你的提交脚本和官方解法,测试时通过hyperquest向http://localhost:<port>发起请求,把两者的 stdout 交给comparestdout对比,同时设置了longCompareOutput = true以应对长行输出(见 exercise.js); - 随机端口与临时文件:
addSetup阶段通过 lib/rndport.js(1024 + Math.floor(Math.random() * 64511))为提交脚本和解法分别分配端口,并在系统临时目录(os.tmpdir())写入一个随机生成的测试文本文件,把端口和文件路径以命令行参数的形式unshift进子进程参数(见 exercise.js); - 核心约束检查:
addVerifyProcessor阶段通过wrappedexec包装子进程的全局环境,收集运行期所有被调用的fs.*()方法记录(exercise.wrapData.fsCalls),逐一过滤——只要出现任何不是createReadStream的文件读取方法调用,就触发fail.no_createReadStream错误并判定失败(见 exercise.js)。
这意味着如果你用fs.readFileSync()或fs.readFile()来实现,即使 HTTP 行为完全正确(客户端照样能拿到文件内容),验证也必然失败。仓库的无效样例正好印证了这一点:
- test/http_file_server/invalid_01.js:空文件,没有任何服务器实现;
- test/http_file_server/invalid_02.js:
readFileSync读取后res.end()返回,功能正确但违背流式要求,属于"行为对、方法错"的典型反例; - test/http_file_server/invalid_03.js:参数取错(把端口号当成文件路径)且同样使用
readFileSync,双重错误; - test/http_file_server/valid_01.js:与官方解法一致的流式实现,是合法通过的参考。
从这些测试用例可以推断:learnyounode 的练习设计不仅检验"服务器能不能跑通",还强制学员养成正确的流式 I/O 思维——这也是本练习放在 Node 入门系列中间位置的用意所在。
练习在课程体系中的位置与延伸
从仓库的 exercises 目录结构看,本练习是"网络与流"主题链上承前启后的一环:
- 前置:http_collect(如何收集 HTTP 响应数据,对照本练习的"流式不缓存")、time_server(基于
net的 TCP 服务器,与本练习的http服务器形成模块对照); - 后置:http_uppercaserer(把请求体转大写返回,练习 request 这一可读流的读取与回写)、http_json_api_server(用
url模块解析路径并按 JSON 响应)。
理解了request/response皆为流的本质,后续练习中"读取请求流 → 变换 → 写回响应流"的思路就会水到渠成。更深入的http模块细节可查阅仓库自带的 docs-nodejs/http.html,流式文件 API 可查阅 docs-nodejs/fs.html 与 docs-nodejs/stream.html。
小结
本题的全部要点可以浓缩为三行代码:用http.createServer()建 HTTP 服务器,用server.listen()绑定第一个参数指定的端口,用fs.createReadStream(process.argv[3]).pipe(res)把文件流接入响应流。其背后是两条重要的 Node 心智模型:HTTP 响应对象本身就是可写流,以及pipe()是连接任意可读流与可写流的标准接插件。通过 learnyounode 的自动化校验(文件读取方法白名单 + 随机端口对比测试),这个练习把"流式思维"从口头概念落实成了可自动验证的硬性代码规范。
- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
相关推荐
Plano 全链路追踪实战:基于 OpenTelemetry 与 W3C Trace Context 的 AI Agent 可观测性指南
Plano 全链路追踪实战:基于 OpenTelemetry 与 W3C Trace Context 的 AI Agent 可观测性指南 本文以 Plano 的
教程CLIlearnyounode 实战:用 Node http 模块与 fs.createReadStream 构建 HTTP 文件服务器
learnyounode 实战:用 Node http 模块与 fs.createReadStream 构建 HTTP 文件服务器 本文基于 learnyoun
教程CLIGameDevMind 游戏数据库选型与优化:从关系型到 NoSQL 缓存的完整实践指南
GameDevMind 游戏数据库选型与优化:从关系型到 NoSQL 缓存的完整实践指南 游戏服务器的数据层既要支撑账号、支付、统计等强一致性业务,又要服务排行
教程CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考