- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
本指南围绕 learnyounode 开源工作坊中的time_server(TCP 时间服务器)练习展开,完整讲解如何利用 Node.js 核心模块net从零搭建一个监听 TCP 连接、按"YYYY-MM-DD hh:mm"格式回写当前时间的服务器,并深入剖析其背后的验证机制与实现原理。读完本文,你将掌握net.createServer()、server.listen()、socket.write()/socket.end()的完整用法,学会手动实现零填充日期格式化,并能通过 learnyounode 自带的验证器确认代码正确性。
练习目标与核心要求
在 exercises/time_server/problem.es.md(及其英文原版 exercises/time_server/problem.md)中,本练习要求你编写一个名为time-server.js的TCP 时间服务器,其行为规范非常明确:
- 服务器监听TCP 连接,监听端口由程序启动时的第一个命令行参数提供;
- 每收到一个连接,就向该连接写入当前日期与 24 小时制时间,格式为
"YYYY-MM-DD hh:mm"; - 输出必须以一个换行符
'\n'结尾; - 月份、日期、小时和分钟必须用零填充为固定两位数,例如
"2013-07-06 17:42"; - 数据发送完毕后关闭该连接。
值得强调的是,这是一个裸 TCP 服务器,完全不涉及 HTTP 协议层。这正是本练习与同仓库中 http_client、http_file_server 等练习的最大区别——它要求你直接使用 Node 核心模块net操作网络套接字。
第一步:用 net.createServer() 搭建服务骨架
net模块是 Node.js 的核心模块之一,包含所有基础的网络功能。在本仓库的 docs-nodejs/net.html(Node.js v12.10.0 官方文档离线镜像)中可以看到,net.createServer([options][, connectionListener])用于创建一个新的 TCP 或 IPC 服务器,它接收两个可选参数:
| 参数 | 类型 | 说明 |
|---|---|---|
options.allowHalfOpen | boolean | 是否允许半开 TCP 连接(对端发来 FIN 后服务端不主动回 FIN),默认false |
options.pauseOnConnect | boolean | 新连接建立后是否立即暂停 socket 数据读取,默认false |
connectionListener | Function | 自动注册为'connection'事件的监听器 |
与 Node 中常见的回调函数(如fs的读取回调只调用一次)不同,createServer()传入的这个回调是connection listener(连接监听器),它会被调用多次——服务器每收到一个 TCP 连接,就会触发一次回调,签名如下:
function callback (socket) { /* ... */ }同时,net.createServer()会返回一个server实例,需要调用server.listen(portNumber)才能真正开始监听端口。文档中给出的典型 Node TCP 服务器骨架是:
const net = require('net') const server = net.createServer(function (socket) { // socket 处理逻辑 }) server.listen(8000)重要提醒:本练习中不要硬编码端口号
8000,必须使用程序启动时通过命令行传入的第一个参数,即process.argv[2]。
深入:listen() 的多种形态
在 docs-nodejs/net.html 中,server.listen()实际上有四种重载形态:server.listen(handle[, backlog][, callback])、server.listen(options[, callback])、server.listen(path[, backlog][, callback])(Unix 域套接字)以及最常用的server.listen([port[, host[, backlog]]][, callback])。本练习使用的是最后一种:只传端口号,让服务器监听在所有网络接口上;listen还支持传入host(如'127.0.0.1')限定监听地址,以及backlog(挂起连接队列长度)和监听成功后的回调函数。
第二步:理解 socket —— 一个双工流
net.createServer()回调收到的socket对象包含大量连接元数据(如socket.localAddress、socket.localPort等),但它本质上是一个Node 双工流(duplex Stream)——既可以读取数据,也可以写入数据,这正是stream.Duplex类的实例(参见 docs-nodejs/net.html 中Class: net.Socket一节)。
对于本练习,我们只需要写入数据然后关闭连接,用到的核心方法是:
socket.write(data):向 socket 写入数据;socket.end():半关闭 socket,即发送 FIN 包;socket.end(data):等价于socket.write(data)后紧接着调用socket.end(),可以一步完成"写入并关闭"。
按照 docs-nodejs/net.html 对socket.end([data][, encoding][, callback])的说明,data可以是字符串、Buffer或Uint8Array,encoding仅在 data 为字符串时使用(默认'utf8')。本练习的响应是一段 UTF-8 文本,因此直接用socket.end(data)即可。
第三步:格式化当前时间(零填充是关键)
服务器输出的核心数据是当前时间,可以通过new Date()获得,然后调用以下日期方法手动拼接格式:
date.getFullYear() date.getMonth() // 注意:从 0 开始,需要 +1 date.getDate() // 返回当月第几天,从 1 开始 date.getHours() date.getMinutes()这里有两个最容易踩坑的细节:
getMonth()从 0 开始计数(一月是 0、二月是 1……),因此必须+1才能得到真实的月份数字;- 月份、日期、小时、分钟都需要零填充为两位数。例如 7 月 6 日必须输出
07-06,下午 5 点 42 分必须输出17:42,而不是7-6或17:4。
仓库参考实现:官方 solution
本仓库自带的参考答案 exercises/time_server/solution/solution.js 展示了最直观的零填充写法:
'use strict' const net = require('net') function zeroFill (i) { return (i < 10 ? '0' : '') + i } function now () { const d = new Date() return d.getFullYear() + '-' + zeroFill(d.getMonth() + 1) + '-' + zeroFill(d.getDate()) + ' ' + zeroFill(d.getHours()) + ':' + zeroFill(d.getMinutes()) } const server = net.createServer(function (socket) { socket.end(now() + '\n') }) server.listen(Number(process.argv[2]))其关键设计值得逐条拆解:
zeroFill(i)用(i < 10 ? '0' : '') + i实现了零填充——小于 10 的数字补一个前导'0',两位数及以上则原样输出,正确处理了 10、11、12 等不受影响的月份/小时;now()中getMonth() + 1修正了从 0 开始计数的月份;- 服务端回调里直接使用
socket.end(now() + '\n'),即"写入格式化时间 + 换行符,然后关闭连接"一步完成; - 监听端口使用
Number(process.argv[2])——显式把第一个命令行参数转换为数字再传给server.listen(),因为process.argv[2]在 shell 中以字符串形式存在。
测试用例中的另一种写法
仓库测试目录 test/time_server/valid_01.js 展示了一种更"函数式"的零填充思路,供开阔思路:
function now (d) { return [ d.getFullYear(), '-', d.getMonth() + 1, '-', d.getDate(), ' ', d.getHours(), ':', d.getMinutes() ].reduce(function (before, current, count) { return before + (count % 2 || ('' + current).length === 2 ? '' : '0') + current }) } require('net').createServer(function (socket) { socket.end(now(new Date()) + '\n') }).listen(process.argv[2] | 0)这里用Array.prototype.reduce在遍历时判断奇偶位置插入'-'/' '/':'分隔符,并通过('' + current).length === 2判断是否需要补零;listen(process.argv[2] | 0)则用位或运算把参数转成整数。官方solution.js的可读性明显更高,也更适合作为学习模板。
备选方案:npm 的 strftime 包
如果你愿意引入第三方依赖,练习文档还提到一个"更冒险"的备选方案——npm 上的strftime包。其strftime(fmt, date)函数接受与 Unix 命令date类似的时间格式字符串,例如:
strftime('%F %H:%M', new Date())即可直接得到"2013-07-06 17:42"这样的零填充输出。不过在 learnyounode 的练习语境中,手写零填充能让你真正理解日期 API 的行为细节,官方也建议优先使用new Date()的方式。
验证:learnyounode 如何检查你的服务器
完成代码后,本练习(英文原版 exercises/time_server/problem.md)提供了验证命令:
$ {appname} verify time-server.js其中{appname}对应 learnyounode 的命令名,即learnyounode verify time-server.js(如果你把文件命名为program.js,则传入program.js)。验证器会返回 PASS/FAIL 结果并给出失败原因。
验证器源码剖析
learnyounode 的每个练习都配有对应的exercise.js作为验证逻辑,本练习的验证器位于 exercises/time_server/exercise.js,其工作流程非常值得研究:
- 文件检查:
filecheck(exercise)确认你提交的time-server.js确实存在; - 并行执行:
execute(exercise)用spawn()并行启动你的提交程序和官方 solution,作为两个子进程; - 动态端口分配:在
addSetup阶段,通过lib/rndport.js中的rndport()生成随机端口——该工具的实现是1024 + Math.floor(Math.random() * 64511)(见 lib/rndport.js),即 1024 到 65535 之间的随机数。你的程序收到submissionPort,官方 solution 收到submissionPort + 1; - 延迟探测:
addProcessor中通过setTimeout(query.bind(this, mode), 500)延迟 500ms 再发起连接,目的是等待两个服务器完成启动; - 客户端连接与对比:
query()内部用net.connect(port)连接两个端口,把收到的响应分别pipe到提交流和 solution 流;若连接失败(服务器未启动、端口未监听等),则触发fail事件并报告fail.connection错误信息; - 标准输出比对:最后
comparestdout(exercise)将你的程序响应与官方 solution 的响应逐字节比对(因为两边都用new Date(),同一秒内格式一致,可以通过比对)。
从测试文件 test/time_server/invalid_01.js(空文件)可以看出,验证器同时覆盖了"提交内容不合格"的失败场景。这一整套机制说明:time_server 验证的核心是你的进程能否在指定端口上、在 500ms 的探测窗口内给出与官方实现一致的格式化时间字符串。
手动验证:用 nc / telnet 自测
在提交验证之前,你也可以完全脱离 learnyounode 手动验证自己的实现。先启动服务器(以端口 8000 为例):
$ node time-server.js 8000然后在另一个终端用nc(netcat)或telnet连接:
$ nc localhost 8000 2013-07-06 17:42或者用 Node 一行式模拟客户端:
$ node -e "require('net').connect(8000, '127.0.0.1').on('data', d => process.stdout.write(d))"如果输出格式为YYYY-MM-DD hh:mm且以换行结尾、月份/日期/小时/分钟均为两位数,即可进入learnyounode verify正式验证环节。
常见错误与排查要点
结合练习要求、官方 solution 与测试用例,以下四类问题最容易导致验证失败:
- 端口写死或读错参数:使用了固定端口(如
8000)而没有读process.argv[2],或忘记用Number()/| 0把字符串参数转成数字。验证器传入的是随机端口,写死端口必然连接失败; - 月份未 +1:
getMonth()从 0 开始,直接拼接会输出2013-06-06这种错误月份; - 忘记零填充:输出
2013-7-6 17:4而非2013-07-06 17:42,导致字符串长度与官方实现不一致,comparestdout比对失败; - 忘记换行符或没有关闭连接:输出必须以
'\n'结尾,且发送后应关闭连接——直接使用socket.end(now() + '\n')可以同时规避这两点。
进阶思考:从练习到真实场景
虽然 time_server 是一个教学练习,但它背后的知识体系直接通向真实网络编程:
- 裸 TCP 服务器的适用范围:
net模块可用于构建自定义协议服务、日志收集管道、数据库代理等不依赖 HTTP 的场景;net.createServer()甚至支持通过server.listen(path)监听 Unix 域套接字实现进程间通信(IPC); - socket 即双工流的思维:理解了
socket是stream.Duplex后,就能把pipe()、背压(backpressure)、'data'/'end'事件等流的知识直接迁移到网络编程中,这正是 learnyounode 后续 http_file_server、http_uppercaserer 等练习的基础; - 可读代码优于技巧:对比官方 solution.js 与测试用的 reduce 写法,前者以明确的
zeroFill辅助函数取胜——当你的服务器逻辑逐渐复杂时,可读性就是可维护性。
至此,你已经完整掌握了用net模块构建 TCP 时间服务器的全部要点:理解连接监听回调、正确调用listen()、用双工流 socket 写出格式化时间,以及理解 learnyounode 验证器如何动态分配端口并比对输出。可以直接上手编写 exercises/time_server/solution/solution.js 同款的time-server.js,并用learnyounode verify time-server.js检验成果。
- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
相关推荐
learnyounode 实战:用 Node.js `net` 模块编写 TCP 时间服务器(time_server 习题全解析)
learnyounode 实战:用 Node.js net 模块编写 TCP 时间服务器(time_server 习题全解析) 本篇技术指南以 learnyou
教程CLIawesome-deepseek-agent 指南:将 DeepSeek 接入轻量级 AI Agent nanobot
awesome deepseek agent 指南:将 DeepSeek 接入轻量级 AI Agent nanobot nanobot 是一个开源轻量级 AI
教程CLISecurity-101 课程仓库协作指南:基于 Markdown 与 Docsify 的网络安全课程开发与多语言翻译流水线解析
Security 101 课程仓库协作指南:基于 Markdown 与 Docsify 的网络安全课程开发与多语言翻译流水线解析 Security 101 是一
教程CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考