☰
learnyounode 实战:用 Node.js net 模块构建一个 TCP 时间服务器(time_server 练习全解)
2026/9/29 5:42:21 网站建设 项目流程
  • 教程
  • CLI

【免费下载链接】learnyounode

Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.

项目地址:https://gitcode.com/gh_mirrors/le/learnyounode
点击查看免费下载

本指南围绕 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.allowHalfOpenboolean是否允许半开 TCP 连接(对端发来 FIN 后服务端不主动回 FIN),默认false
options.pauseOnConnectboolean新连接建立后是否立即暂停 socket 数据读取,默认false
connectionListenerFunction自动注册为'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()

这里有两个最容易踩坑的细节:

  1. getMonth()从 0 开始计数(一月是 0、二月是 1……),因此必须+1才能得到真实的月份数字;
  2. 月份、日期、小时、分钟都需要零填充为两位数。例如 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,其工作流程非常值得研究:

  1. 文件检查:filecheck(exercise)确认你提交的time-server.js确实存在;
  2. 并行执行:execute(exercise)用spawn()并行启动你的提交程序和官方 solution,作为两个子进程;
  3. 动态端口分配:在addSetup阶段,通过lib/rndport.js中的rndport()生成随机端口——该工具的实现是1024 + Math.floor(Math.random() * 64511)(见 lib/rndport.js),即 1024 到 65535 之间的随机数。你的程序收到submissionPort,官方 solution 收到submissionPort + 1;
  4. 延迟探测:addProcessor中通过setTimeout(query.bind(this, mode), 500)延迟 500ms 再发起连接,目的是等待两个服务器完成启动;
  5. 客户端连接与对比:query()内部用net.connect(port)连接两个端口,把收到的响应分别pipe到提交流和 solution 流;若连接失败(服务器未启动、端口未监听等),则触发fail事件并报告fail.connection错误信息;
  6. 标准输出比对:最后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 与测试用例,以下四类问题最容易导致验证失败:

  1. 端口写死或读错参数:使用了固定端口(如8000)而没有读process.argv[2],或忘记用Number()/| 0把字符串参数转成数字。验证器传入的是随机端口,写死端口必然连接失败;
  2. 月份未 +1:getMonth()从 0 开始,直接拼接会输出2013-06-06这种错误月份;
  3. 忘记零填充:输出2013-7-6 17:4而非2013-07-06 17:42,导致字符串长度与官方实现不一致,comparestdout比对失败;
  4. 忘记换行符或没有关闭连接:输出必须以'\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.

项目地址:https://gitcode.com/gh_mirrors/le/learnyounode
点击查看免费下载
上一篇:Motion-Primitives滚动进度指示器:提升用户导航体验的完整指南
下一篇:JSON Schema to TypeScript错误处理与调试:常见问题解决方案大全

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询