- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
本文基于 learnyounode 系列习题中的http_file_server(HTTP 文件服务器)一题展开,讲解如何用 Node.js 核心模块http搭建一个将同一个文本文件以流(Stream)方式返回给每次请求的 HTTP 服务器,并解释request/response为何是流、fs.createReadStream()与src.pipe(dst)如何把文件流无缝接到 HTTP 响应流。读完本文,你将掌握 HTTP 服务器的最小可运行实现、命令行参数的解析方法,以及 learnyounode 自动化校验对createReadStream用法的判定规则,能够独立完成并通过该习题。
一、习题目标与输入约定
本节习题(日文版原文见 exercises/http_file_server/problem.ja.md,英文版见 problem.md)要求编写一个 HTTP文件服务器:对于收到的每一次请求,都返回同一个文本文件的内容。题目对程序输入做了两项硬性约定:
- 第一个命令行参数:服务器要监听的端口号;
- 第二个命令行参数:要返回的文本文件的路径。
同时,题目明确给出了一条强制约束——必须使用fs.createReadStream()方法,以流的形式把文件内容送入响应(英文原题措辞为 “Youmustuse thefs.createReadStream()method to stream the file contents to the response”)。
需要说明的是,英文原题还要求先创建一个名为http-file-server.js的文件,然后在项目根目录用{appname} verify http-file-server.js校验,其中{appname}在本仓库对应的 CLI 名称是learnyounode(见 package.json 中的bin字段)。
二、为什么选用http模块而不是net
题目在提示部分特别强调:本题要创建的是 HTTP 服务器,而不是普通的 TCP 服务器,因此应该使用 Node 核心模块http,而不是net。
http与net相似,都提供了名为createServer()的函数,但http.createServer()创建的是能够讲 HTTP 协议的服务器:它会解析请求行、请求头,并负责把响应头与响应体序列化后写回客户端。- 从仓库的后续习题看,同一阶段还包含基于
net的 time_server(TCP 时间服务器)与基于http的 http_client、http_collect 等,正是为了让学习者区分这两类服务器的适用场景。
因此,本题的正确技术选型是:
const http = require('http')三、http.createServer()与 request/response 两个对象
http.createServer()接收一个回调函数作为参数,该回调会在服务器每次收到连接/请求时被调用一次,其签名固定为:
function listener (request, response) { /* ... */ }两个参数的含义:
request:代表本次 HTTP 请求的对象,用于获取请求相关属性,例如请求头(headers)、查询字符串(query-string)等;response:代表对应的响应对象,用于向客户端回送数据,既包括响应头部(headers),也包括响应体(body)。
题目的提示还给出了一个关键概念:request和response在 Node.js 中本身都是 Stream!这意味着一方面可以直接用流的抽象(如pipe、data事件)来收发数据;另一方面,它让“把文件流接入响应流”这种场景有了天然的对接点——这正是本题的核心考点。
http.createServer()会返回服务器实例,调用server.listen(portNumber)即可在指定端口开始监听。题目给出的一般形态如下:
const http = require('http') const server = http.createServer(function (req, res) { // request handling logic... }) server.listen(8000)四、用fs.createReadStream()与pipe完成文件流对接
文件系统核心模块fs提供了一批流式(streaming)API。本题要用的是fs.createReadStream():
- 把命令行第二个参数(文本文件路径)传给
fs.createReadStream(),会得到一个表示该文件的流对象(ReadStream); - 使用
src.pipe(dst)可以把src流的数据"管道"式地导入dst流; - 因此,可以一行代码把文件系统流接到HTTP 响应流上:
fs.createReadStream(process.argv[3]).pipe(res)关于 pipe 的语义(详见仓库内置的 Node 文档 docs-nodejs/stream.html 与 docs-nodejs/fs.html):pipe会监听源流上的data事件并把数据写入目标流,同时处理背压(back-pressure),即当目标流写入速度跟不上时暂停源流读取,从而避免内存被大量未消费的数据撑爆。这正是用流式方案而不是一次性readFile读取大文件的价值所在。
五、一份可直接运行并通过校验的参考实现
仓库为本题提供了官方参考实现 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) // 把文件流 pipe 进响应流 }) server.listen(Number(process.argv[2])) // 监听第一个命令行参数指定的端口对照题目要求拆解这份实现,有三个容易被忽略的细节值得注意:
- 端口要用
Number()转换:process.argv[2]拿到的是字符串,而server.listen()在旧版 Node 中对字符串端口与数字端口的处理不同。官方实现(以及测试用例 test/http_file_server/valid_01.js 中的process.argv[2] | 0)都显式把端口转为数字,这是稳妥的写法。 - 先用
res.writeHead(200, ...)写响应头:HTTP 响应必须先写头部再写主体。这里显式声明了content-type: text/plain,与测试文件一致。 - 参数索引:
process.argv[2]是端口,process.argv[3]是文件路径。如果搞错索引,就会出现 "文件不存在" 或 "端口无法监听" 之类的运行时错误(参见下文反例)。
六、learnyounode 是如何自动校验这道题的
理解校验器有助于写出能一次通过的代码。本题的自动化校验逻辑在 exercises/http_file_server/exercise.js,整体流程如下:
- 准备测试数据:在系统临时目录生成一个随机文本文件
_learnyounode_<pid>.txt(内容由boganipsum生成,见 exercise.js),并借助 lib/rndport.js 生成随机端口(1024 + Math.floor(Math.random() * 64511),即 1024~65535 范围内),把端口与文件路径注入到提交程序和参考解法的命令行参数中。 - 延迟并发请求:等服务器启动 500ms 后,通过
hyperquest向http://localhost:<port>发起 GET 请求(exercise.js),把响应体接到各自的 stdout 流上,最长等待 5 秒。 - 对比输出:在
verify模式下,把你的程序的响应体与官方解法的响应体经comparestdout做逐字节对比;run模式下则只请求你的程序并把响应体转发到终端,供你肉眼检查。 - 强制使用
createReadStream:这是本题最特殊的校验点。通过workshopper-wrappedexec包装子进程执行环境,记录程序运行时调用的全部fs.*方法(wrapData.fsCalls),随后在addVerifyProcessor中检查:只要出现过任何一个不是createReadStream的fs调用,就判定失败(exercise.js):
const badCalls = Object.keys(exercise.wrapData.fsCalls).filter(function (m) { exercise.emit('fail', exercise.__('fail.no_createReadStream', { method: 'fs.' + m + '()' })) return !(/createReadStream/).test(m) }) callback(null, badCalls.length === 0)也就是说,如果你用fs.readFileSync()或fs.readFile()一次性读文件再res.end(),即使响应内容完全一致,也会因为命中 "fail.no_createReadStream" 而无法通过。这一点由测试反例直接印证:
- 有效用例 test/http_file_server/valid_01.js 使用
fs.createReadStream(process.argv[3]).pipe(res); - 反例 test/http_file_server/invalid_02.js 使用
fs.readFileSync(...)读入后res.end(),属于典型错误写法; - 反例 test/http_file_server/invalid_03.js 则同时犯了参数索引错误(用
process.argv[2]当作文件路径)与readFileSync两个问题。
另外可以注意到,校验器目前在 exercise.js 的注释中写明 TODO:尚未对content-type与statusCode做自动校验——这属于"从源码结构看"的实现现状,意味着目前核心判据是响应内容一致 + 只使用createReadStream。但官方解法依然主动设置了200与text/plain,作为最佳实践值得保留。
七、运行与验证命令
在仓库中安装好依赖并全局链接learnyounode之后,按以下方式操作:
learnyounode run http-file-server.js learnyounode verify http-file-server.jsrun:只启动你的程序并用临时文件对它发请求,把响应体打印出来,方便自查;verify:同时运行你的程序与官方解法、随机生成测试文件与端口,做响应对比 +createReadStream用法检查。
对应关系可以在练习页选择http_file_server后按提示执行。若想先跑通整个环境,可参考仓库根目录 README.md 的安装与使用说明。
八、相关文档与延伸学习
http模块完整文档已内置在本仓库的 docs-nodejs/http.html,可供离线查阅createServer、listen、请求/响应对象的全部 API;- 流的通用机制(
pipe、背压、Readable/Writable)见 docs-nodejs/stream.html; - 文件流 API(
createReadStream等)见 docs-nodejs/fs.html; - 同一系列的相关习题可以形成完整学习链路:
- http_client:作为 HTTP 客户端发起请求,与本题的服务器端视角互补;
- http_uppercaserer:同样基于
http.createServer(),但用through2-map处理请求流,可加深对"request 也是流"的理解; - http_json_api_server:在 HTTP 服务器上解析 URL 与查询参数并返回 JSON;
- juggling_async:异步并发收集多个 HTTP 响应,是对流式读取的补充练习。
小结
http_file_server这一题的核心价值在于:它把"HTTP 服务器"与"流式传输"两个概念第一次组合在一起。掌握http.createServer()的回调签名、server.listen(port)的启动方式、fs.createReadStream(path).pipe(res)的一行式对接,并理解 learnyounode 校验器对fs调用方法的强制检查,即可高质量通过本题,也为后续处理请求流、响应流的习题打下基础。
- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
相关推荐
GameDevMind 游戏数据库选型与优化:从关系型到 NoSQL 缓存的完整实践指南
GameDevMind 游戏数据库选型与优化:从关系型到 NoSQL 缓存的完整实践指南 游戏服务器的数据层既要支撑账号、支付、统计等强一致性业务,又要服务排行
教程CLIPlano 全链路追踪实战:基于 OpenTelemetry 与 W3C Trace Context 的 AI Agent 可观测性指南
Plano 全链路追踪实战:基于 OpenTelemetry 与 W3C Trace Context 的 AI Agent 可观测性指南 本文以 Plano 的
教程CLI在 Claude Cowork 中打开并查看 PDF:pdf-viewer 插件 `/pdf-viewer:open` 命令完整实战指南
在 Claude Cowork 中打开并查看 PDF:pdf viewer 插件 /pdf viewer:open 命令完整实战指南 导读 :本文聚焦 Clau
教程CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考