一篇面向实战的后端入门博客:从
http.createServer、请求/响应报文 ,到 路由分发 与 静态资源服务 的完整链路。示例均可独立运行,不依赖外部讲义路径。
目录
- 导读:知识架构与权威参考
- [1. HTTP模块概述](#1. HTTP模块概述)
- [1.1 核心概念解析](#1.1 核心概念解析)
- [1.2 HTTP模块的四大核心对象](#1.2 HTTP模块的四大核心对象)
- [1.3 网络基础名词(与 HTTP 配合理解)](#1.3 网络基础名词(与 HTTP 配合理解))
- [1.4 HTTP协议基础回顾](#1.4 HTTP协议基础回顾)
- [1.5 经典应用场景](#1.5 经典应用场景)
- [2. 创建HTTP服务](#2. 创建HTTP服务)
- [2.1 基础服务器创建](#2.1 基础服务器创建)
- [2.2 服务器启动注意事项](#2.2 服务器启动注意事项)
- [2.3 企业级服务器配置](#2.3 企业级服务器配置)
- [2.4 市面应用实例](#2.4 市面应用实例)
- [3. 请求报文处理](#3. 请求报文处理)
- [3.1 获取请求行信息](#3.1 获取请求行信息)
- [3.2 获取URL查询字符串](#3.2 获取URL查询字符串)
- [3.3 获取请求体信息](#3.3 获取请求体信息)
- [3.4 请求处理的经典应用场景](#3.4 请求处理的经典应用场景)
- [3.5 HTTP 客户端:发起出站请求](#3.5 HTTP 客户端:发起出站请求)
- [4. 响应报文设置](#4. 响应报文设置)
- [4.1 设置响应行](#4.1 设置响应行)
- [4.2 设置响应头](#4.2 设置响应头)
- [4.3 设置响应体](#4.3 设置响应体)
- [4.4 完整响应设置综合示例](#4.4 完整响应设置综合示例)
- [5. 路由处理](#5. 路由处理)
- [5.1 基于路径的路由](#5.1 基于路径的路由)
- [5.2 基于请求方式和路径的路由](#5.2 基于请求方式和路径的路由)
- [5.3 路由处理的企业级模式](#5.3 路由处理的企业级模式)
- [6. 静态文件服务器](#6. 静态文件服务器)
- [6.1 基础静态文件服务](#6.1 基础静态文件服务)
- [6.2 流式静态文件服务(性能优化版本)](#6.2 流式静态文件服务(性能优化版本))
- [6.3 带缓存的静态文件服务器](#6.3 带缓存的静态文件服务器)
- [6.4 静态文件服务器的经典应用场景](#6.4 静态文件服务器的经典应用场景)
- [6.5 多页面静态站点联调示例](#6.5 多页面静态站点联调示例)
- [6.6 静态文件服务的安全加固:路径穿越防护](#6.6 静态文件服务的安全加固:路径穿越防护)
- [7. 企业级最佳实践](#7. 企业级最佳实践)
- [7.1 模块化架构设计](#7.1 模块化架构设计)
- [7.2 错误处理机制](#7.2 错误处理机制)
- [7.3 日志系统](#7.3 日志系统)
- [7.4 安全性最佳实践](#7.4 安全性最佳实践)
- [8. 性能优化策略](#8. 性能优化策略)
- [8.1 连接复用与Keep-Alive](#8.1 连接复用与Keep-Alive)
- [8.2 集群模式](#8.2 集群模式)
- [8.3 压缩传输](#8.3 压缩传输)
- [8.4 Server-Sent Events(SSE)--- 服务端主动推送](#8.4 Server-Sent Events(SSE)— 服务端主动推送)
- [9. 总结与展望](#9. 总结与展望)
- [9.1 核心知识点总结](#9.1 核心知识点总结)
- [9.2 学习路线建议](#9.2 学习路线建议)
- [9.3 未来发展趋势](#9.3 未来发展趋势)
- [9.4 实践项目建议](#9.4 实践项目建议)
- [10. 核心案例速查与知识点归纳](#10. 核心案例速查与知识点归纳)
- [10.1 案例索引(按学习顺序)](#10.1 案例索引(按学习顺序))
- [10.2 请求对象速查](#10.2 请求对象速查)
- [10.3 响应对象速查](#10.3 响应对象速查)
- [10.4 路由设计对照](#10.4 路由设计对照)
- [10.5 最小登录联调(HTML + 服务端)](#10.5 最小登录联调(HTML + 服务端))
- [10.6 常见错误与对策](#10.6 常见错误与对策)
- [10.7 与框架的关系(归纳)](#10.7 与框架的关系(归纳))
导读:知识架构与权威参考
本文解决什么问题
| 能力 | 核心 API / 概念 | 学完能做什么 |
|---|---|---|
| 起服务 | http.createServer、server.listen |
本地可访问的 Web 入口 |
| 读请求 | req.method、req.url、req.headers、流式 data/end |
打日志、解析表单、REST 路由 |
| 写响应 | statusCode、setHeader、writeHead、write/end |
返回 HTML/JSON/文件流 |
| 路由 | pathname + method |
多页面、登录 POST、404 |
| 静态站 | fs + path + Content-Type |
托管前端构建产物 |
| HTTP 客户端 | http.get、http.request、https |
调用外部 API、BFF 层数据聚合 |
| 安全加固 | path.resolve + 白名单校验 |
防路径穿越、限速、安全头 |
| 实时推送 | SSE + text/event-stream |
订单状态、进度条、AI 流式输出 |
知识脉络(Mermaid)
#mermaid-svg-wbCvtEuqHmInZeqq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wbCvtEuqHmInZeqq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wbCvtEuqHmInZeqq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wbCvtEuqHmInZeqq .error-icon{fill:#552222;}#mermaid-svg-wbCvtEuqHmInZeqq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wbCvtEuqHmInZeqq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wbCvtEuqHmInZeqq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wbCvtEuqHmInZeqq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wbCvtEuqHmInZeqq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wbCvtEuqHmInZeqq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wbCvtEuqHmInZeqq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wbCvtEuqHmInZeqq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wbCvtEuqHmInZeqq .marker.cross{stroke:#333333;}#mermaid-svg-wbCvtEuqHmInZeqq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wbCvtEuqHmInZeqq p{margin:0;}#mermaid-svg-wbCvtEuqHmInZeqq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wbCvtEuqHmInZeqq .cluster-label text{fill:#333;}#mermaid-svg-wbCvtEuqHmInZeqq .cluster-label span{color:#333;}#mermaid-svg-wbCvtEuqHmInZeqq .cluster-label span p{background-color:transparent;}#mermaid-svg-wbCvtEuqHmInZeqq .label text,#mermaid-svg-wbCvtEuqHmInZeqq span{fill:#333;color:#333;}#mermaid-svg-wbCvtEuqHmInZeqq .node rect,#mermaid-svg-wbCvtEuqHmInZeqq .node circle,#mermaid-svg-wbCvtEuqHmInZeqq .node ellipse,#mermaid-svg-wbCvtEuqHmInZeqq .node polygon,#mermaid-svg-wbCvtEuqHmInZeqq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wbCvtEuqHmInZeqq .rough-node .label text,#mermaid-svg-wbCvtEuqHmInZeqq .node .label text,#mermaid-svg-wbCvtEuqHmInZeqq .image-shape .label,#mermaid-svg-wbCvtEuqHmInZeqq .icon-shape .label{text-anchor:middle;}#mermaid-svg-wbCvtEuqHmInZeqq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wbCvtEuqHmInZeqq .rough-node .label,#mermaid-svg-wbCvtEuqHmInZeqq .node .label,#mermaid-svg-wbCvtEuqHmInZeqq .image-shape .label,#mermaid-svg-wbCvtEuqHmInZeqq .icon-shape .label{text-align:center;}#mermaid-svg-wbCvtEuqHmInZeqq .node.clickable{cursor:pointer;}#mermaid-svg-wbCvtEuqHmInZeqq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wbCvtEuqHmInZeqq .arrowheadPath{fill:#333333;}#mermaid-svg-wbCvtEuqHmInZeqq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wbCvtEuqHmInZeqq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wbCvtEuqHmInZeqq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wbCvtEuqHmInZeqq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wbCvtEuqHmInZeqq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wbCvtEuqHmInZeqq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wbCvtEuqHmInZeqq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wbCvtEuqHmInZeqq .cluster text{fill:#333;}#mermaid-svg-wbCvtEuqHmInZeqq .cluster span{color:#333;}#mermaid-svg-wbCvtEuqHmInZeqq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wbCvtEuqHmInZeqq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wbCvtEuqHmInZeqq rect.text{fill:none;stroke-width:0;}#mermaid-svg-wbCvtEuqHmInZeqq .icon-shape,#mermaid-svg-wbCvtEuqHmInZeqq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wbCvtEuqHmInZeqq .icon-shape p,#mermaid-svg-wbCvtEuqHmInZeqq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wbCvtEuqHmInZeqq .icon-shape .label rect,#mermaid-svg-wbCvtEuqHmInZeqq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wbCvtEuqHmInZeqq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wbCvtEuqHmInZeqq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wbCvtEuqHmInZeqq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务
响应侧
请求侧
服务端
createServer
listen 端口
请求行 method/url
请求头 headers
请求体 流式读取
状态码/描述
响应头 Content-Type等
write / end / pipe
路径+方法路由
静态文件服务
权威文档(建议对照)
| 主题 | 链接 |
|---|---|
Node.js http 模块 |
nodejs.org/api/http.html |
Node.js url 模块 |
nodejs.org/api/url.html |
| HTTP 方法 | MDN --- HTTP 方法 |
| HTTP 首部 | MDN --- HTTP 标头 |
| HTTP 状态码 | MDN --- HTTP 状态码 |
| Node 安全实践 | Node.js Security Best Practices |
技术栈中的位置
- Express / Koa / Fastify :在原生
http之上封装路由与中间件;理解本章有助于读框架源码。 - Vite / Webpack dev server:开发时静态资源 + 代理,本质仍是 HTTP 请求响应。
- Nginx / Caddy:生产环境常作反向代理,Node 进程监听内网端口对外提供动态 API。
1. HTTP模块概述
1.1 核心概念解析
HTTP模块是Node.js内置的核心模块之一,提供了创建HTTP服务器和客户端的功能。它基于事件驱动、非阻塞I/O模型,使其特别适合处理高并发的网络请求。
名词解析:
- 事件驱动:一种编程范式,程序的流程由事件决定,如用户操作、传感器输出或消息传递
- 非阻塞I/O:输入/操作操作不会阻塞后续代码的执行,通过回调函数处理结果
- HTTP服务器:能够处理HTTP协议请求并返回响应的服务端程序
1.2 HTTP模块的四大核心对象
#mermaid-svg-5aY1q2P53ddhw9F4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5aY1q2P53ddhw9F4 .error-icon{fill:#552222;}#mermaid-svg-5aY1q2P53ddhw9F4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5aY1q2P53ddhw9F4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5aY1q2P53ddhw9F4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5aY1q2P53ddhw9F4 .marker.cross{stroke:#333333;}#mermaid-svg-5aY1q2P53ddhw9F4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5aY1q2P53ddhw9F4 p{margin:0;}#mermaid-svg-5aY1q2P53ddhw9F4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5aY1q2P53ddhw9F4 .cluster-label text{fill:#333;}#mermaid-svg-5aY1q2P53ddhw9F4 .cluster-label span{color:#333;}#mermaid-svg-5aY1q2P53ddhw9F4 .cluster-label span p{background-color:transparent;}#mermaid-svg-5aY1q2P53ddhw9F4 .label text,#mermaid-svg-5aY1q2P53ddhw9F4 span{fill:#333;color:#333;}#mermaid-svg-5aY1q2P53ddhw9F4 .node rect,#mermaid-svg-5aY1q2P53ddhw9F4 .node circle,#mermaid-svg-5aY1q2P53ddhw9F4 .node ellipse,#mermaid-svg-5aY1q2P53ddhw9F4 .node polygon,#mermaid-svg-5aY1q2P53ddhw9F4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5aY1q2P53ddhw9F4 .rough-node .label text,#mermaid-svg-5aY1q2P53ddhw9F4 .node .label text,#mermaid-svg-5aY1q2P53ddhw9F4 .image-shape .label,#mermaid-svg-5aY1q2P53ddhw9F4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5aY1q2P53ddhw9F4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5aY1q2P53ddhw9F4 .rough-node .label,#mermaid-svg-5aY1q2P53ddhw9F4 .node .label,#mermaid-svg-5aY1q2P53ddhw9F4 .image-shape .label,#mermaid-svg-5aY1q2P53ddhw9F4 .icon-shape .label{text-align:center;}#mermaid-svg-5aY1q2P53ddhw9F4 .node.clickable{cursor:pointer;}#mermaid-svg-5aY1q2P53ddhw9F4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5aY1q2P53ddhw9F4 .arrowheadPath{fill:#333333;}#mermaid-svg-5aY1q2P53ddhw9F4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5aY1q2P53ddhw9F4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5aY1q2P53ddhw9F4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5aY1q2P53ddhw9F4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5aY1q2P53ddhw9F4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5aY1q2P53ddhw9F4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5aY1q2P53ddhw9F4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5aY1q2P53ddhw9F4 .cluster text{fill:#333;}#mermaid-svg-5aY1q2P53ddhw9F4 .cluster span{color:#333;}#mermaid-svg-5aY1q2P53ddhw9F4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5aY1q2P53ddhw9F4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5aY1q2P53ddhw9F4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5aY1q2P53ddhw9F4 .icon-shape,#mermaid-svg-5aY1q2P53ddhw9F4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5aY1q2P53ddhw9F4 .icon-shape p,#mermaid-svg-5aY1q2P53ddhw9F4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5aY1q2P53ddhw9F4 .icon-shape .label rect,#mermaid-svg-5aY1q2P53ddhw9F4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5aY1q2P53ddhw9F4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5aY1q2P53ddhw9F4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5aY1q2P53ddhw9F4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内置模块
createServer返回值
回调函数第一参数
回调函数第二参数
HTTP模块
http对象
http.Server对象
http.ClientRequest对象
http.ServerResponse对象
require'http'
服务器实例
请求对象
响应对象
对象详解:
- http对象 :通过
require('http')导入的内置模块,提供创建服务器和客户端的方法 - http.Server对象 :
http.createServer()方法的返回值,代表一个HTTP服务器实例 - http.ClientRequest对象:请求对象,包含客户端发送的请求信息
- http.ServerResponse对象:响应对象,用于向客户端发送响应数据
1.3 网络基础名词(与 HTTP 配合理解)
| 名词 | 含义 | 与 Node 的关系 |
|---|---|---|
| IP 地址 | 主机在网络中的数字标识 | server.listen(port, host) 的 host |
| 端口 | 同一 IP 上区分服务的编号 | listen(8080);80/443 为常见 Web 端口 |
| 域名 | IP 的易记别名 | 浏览器 Host 头、反向代理转发 |
| URL | 协议 + 主机 + 端口 + 路径 + 查询 + 锚点 | req.url 含路径与 ?query |
【代码注释】
- 在 Node 服务端,
req.url通常只有 路径 + 查询串 ,例如/search?wd=node,不含http://、域名、端口。 - 浏览器地址栏的完整 URL 需自行拼接:
const base = 'http://' + req.headers.host,再new URL(base + req.url)(见 §3.2 方法三)。 req.headers.host形如localhost:8080,反向代理后可能是公网域名;写日志、鉴权、生成跳转链接时都要用到。- 这与
fs.readFile('./x')相对 cwd 的坑不同,但同属「不要假设客户端发来的字符串就是完整 URL」。
1.4 HTTP协议基础回顾
请求报文结构:
请求行:请求方式 URL 协议版本
请求头:键值对形式
空行:
请求体:载荷数据
响应报文结构:
响应行:状态码 状态描述 协议版本
响应头:键值对形式
空行:
响应体:返回给客户端的内容
1.5 经典应用场景
实际应用案例:
- Web应用服务器:如Express.js、Koa.js框架底层都基于HTTP模块
- API服务:RESTful API、GraphQL API服务器
- 微服务架构:服务间通信的HTTP接口
- 代理服务器:正向代理、反向代理、负载均衡
- 实时通信:基于HTTP的WebSocket升级、Server-Sent Events
2. 创建HTTP服务
2.1 基础服务器创建
完整可运行示例:
javascript
const http = require('http');
const server = http.createServer((req, res) => {
console.log('收到请求,客户端IP:', req.socket.remoteAddress);
res.end('<h1>Welcome to My WebSite</h1>');
});
server.listen(8080, () => {
console.log('服务器运行在 http://localhost:8080');
});
// server.listen(8080, '127.0.0.1', () => { ... });
【代码注释】
require('http')为内置模块,无需npm install;createServer返回Server实例,每个 HTTP 请求触发一次回调。- 回调参数:
req(IncomingMessage,只读请求)、res(ServerResponse,写响应);必须对每个 请求调用res.end()或write+end,否则客户端一直等待。 req.socket.remoteAddress为客户端 IP,IPv4 映射时常见::ffff:127.0.0.1,日志展示可.slice(7)去前缀。listen(8080)绑定本机 8080 端口;listen(80)时浏览器可写http://localhost省略端口。生产常用0.0.0.0监听所有网卡。- 改代码后需 Ctrl+C 停进程再
node app.js;开发可用nodemon app.js热重载。端口占用报EADDRINUSE时换端口或lsof -i :8080结束进程。
2.2 服务器启动注意事项
常见问题处理:
- 端口占用问题
javascript
// 错误示例:端口被占用
// Error: listen EADDRINUSE: address already in use :::8080
// 解决方案一:更换端口
server.listen(3000, () => {
console.log('服务器运行在端口3000');
});
// 解决方案二:关闭占用端口的进程
// Windows: netstat -ano | findstr 8080
// macOS/Linux: lsof -i:8080
- 代码热重载
javascript
// 开发环境建议使用nodemon实现代码修改自动重启
// 安装:npm install -g nodemon
// 运行:nodemon app.js
2.3 企业级服务器配置
生产环境最佳实践:
javascript
const http = require('http');
// 创建服务器时的配置选项 【代码注释】
const server = http.createServer({
// 超时时间设置
timeout: 30000, // 服务器超时时间(毫秒)
keepAliveTimeout: 5000, // 保持连接超时时间
// 请求头大小限制
maxHeadersCount: 50, // 最大请求头数量
}, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('企业级服务器配置示例');
});
// 错误处理 【代码注释】 捕获服务器级别的错误
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
console.error('端口已被占用,请更换端口或关闭占用进程');
} else {
console.error('服务器错误:', error);
}
});
// 优雅关闭 【代码注释】 处理进程终止信号
process.on('SIGTERM', () => {
server.close(() => {
console.log('服务器已优雅关闭');
process.exit(0);
});
});
server.listen(8080, '0.0.0.0', () => {
console.log('企业级服务器启动成功');
});
【代码注释】
createServer(options, listener)的timeout、keepAliveTimeout控制连接与 Keep-Alive 空闲超时,防止慢客户端占满连接。server.on('error')捕获监听失败(如EADDRINUSE),应在进程启动阶段处理,避免未捕获异常退出。SIGTERM+server.close():优雅关闭------停止接受新连接,等已有请求处理完再退出,容器/K8s 滚动发布常用。listen(8080, '0.0.0.0')允许局域网其他机器访问;仅本机调试可用127.0.0.1。
2.4 市面应用实例
知名网站的Node.js应用:
- LinkedIn:使用Node.js重构移动端后端,性能提升20倍
- Netflix:使用Node.js构建高并发的API网关
- PayPal:使用Node.js重构支付系统,响应时间减少35%
- Uber:实时处理海量请求的计费系统
3. 请求报文处理
3.1 获取请求行信息
#mermaid-svg-1pXKFzdd3bareeST{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1pXKFzdd3bareeST .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1pXKFzdd3bareeST .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1pXKFzdd3bareeST .error-icon{fill:#552222;}#mermaid-svg-1pXKFzdd3bareeST .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1pXKFzdd3bareeST .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1pXKFzdd3bareeST .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1pXKFzdd3bareeST .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1pXKFzdd3bareeST .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1pXKFzdd3bareeST .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1pXKFzdd3bareeST .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1pXKFzdd3bareeST .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1pXKFzdd3bareeST .marker.cross{stroke:#333333;}#mermaid-svg-1pXKFzdd3bareeST svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1pXKFzdd3bareeST p{margin:0;}#mermaid-svg-1pXKFzdd3bareeST .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1pXKFzdd3bareeST .cluster-label text{fill:#333;}#mermaid-svg-1pXKFzdd3bareeST .cluster-label span{color:#333;}#mermaid-svg-1pXKFzdd3bareeST .cluster-label span p{background-color:transparent;}#mermaid-svg-1pXKFzdd3bareeST .label text,#mermaid-svg-1pXKFzdd3bareeST span{fill:#333;color:#333;}#mermaid-svg-1pXKFzdd3bareeST .node rect,#mermaid-svg-1pXKFzdd3bareeST .node circle,#mermaid-svg-1pXKFzdd3bareeST .node ellipse,#mermaid-svg-1pXKFzdd3bareeST .node polygon,#mermaid-svg-1pXKFzdd3bareeST .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1pXKFzdd3bareeST .rough-node .label text,#mermaid-svg-1pXKFzdd3bareeST .node .label text,#mermaid-svg-1pXKFzdd3bareeST .image-shape .label,#mermaid-svg-1pXKFzdd3bareeST .icon-shape .label{text-anchor:middle;}#mermaid-svg-1pXKFzdd3bareeST .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1pXKFzdd3bareeST .rough-node .label,#mermaid-svg-1pXKFzdd3bareeST .node .label,#mermaid-svg-1pXKFzdd3bareeST .image-shape .label,#mermaid-svg-1pXKFzdd3bareeST .icon-shape .label{text-align:center;}#mermaid-svg-1pXKFzdd3bareeST .node.clickable{cursor:pointer;}#mermaid-svg-1pXKFzdd3bareeST .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1pXKFzdd3bareeST .arrowheadPath{fill:#333333;}#mermaid-svg-1pXKFzdd3bareeST .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1pXKFzdd3bareeST .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1pXKFzdd3bareeST .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pXKFzdd3bareeST .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1pXKFzdd3bareeST .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pXKFzdd3bareeST .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1pXKFzdd3bareeST .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1pXKFzdd3bareeST .cluster text{fill:#333;}#mermaid-svg-1pXKFzdd3bareeST .cluster span{color:#333;}#mermaid-svg-1pXKFzdd3bareeST div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1pXKFzdd3bareeST .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1pXKFzdd3bareeST rect.text{fill:none;stroke-width:0;}#mermaid-svg-1pXKFzdd3bareeST .icon-shape,#mermaid-svg-1pXKFzdd3bareeST .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pXKFzdd3bareeST .icon-shape p,#mermaid-svg-1pXKFzdd3bareeST .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1pXKFzdd3bareeST .icon-shape .label rect,#mermaid-svg-1pXKFzdd3bareeST .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pXKFzdd3bareeST .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1pXKFzdd3bareeST .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1pXKFzdd3bareeST :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP请求行
请求方法
URL路径
协议版本
GET/POST/PUT/DELETE
路径名+查询字符串
HTTP/1.1 HTTP/2
完整示例代码:
javascript
const http = require('http');
const { table } = require('table'); // 引入table模块用于表格化输出 【代码注释】
const server = http.createServer((req, res) => {
// 处理favicon请求 【代码注释】 浏览器会自动请求favicon.ico
if (req.url === '/favicon.ico') {
return res.end();
}
// 请求行信息获取 【代码注释】
console.log('========== 请求行信息 ==========');
console.log('请求方式:', req.method); // GET, POST, PUT, DELETE等
console.log('请求URL:', req.url); // 包含路径名和查询字符串
console.log('协议版本:', req.httpVersion); // HTTP/1.1 或 HTTP/2
// 获取客户端IP地址 【代码注释】
console.log('客户端IP:', req.socket.remoteAddress);
// 请求头信息获取与美化输出 【代码注释】
console.log('\n========== 请求头信息 ==========');
console.log('客户端浏览器信息:', req.headers['user-agent']);
console.log('接受的语言:', req.headers['accept-language']);
console.log('接受的编码:', req.headers['accept-encoding']);
// 使用table模块美化输出请求头 【代码注释】
console.log('\n完整请求头:');
console.log(table(Object.entries(req.headers)));
// 设置响应头 【代码注释】 指定内容类型和字符编码
res.setHeader('Content-type', 'text/html;charset=utf-8');
// 返回响应内容 【代码注释】
res.end('<h1>HTTP服务 - 请求信息处理</h1>');
});
server.listen(8080, () => {
console.log('请求信息演示服务器运行在 http://localhost:8080');
});
【代码注释】
- 请求行 :
req.method(GET/POST 等)、req.url(路径+?query)、req.httpVersion(多为1.1)。 - 请求头 :
req.headers键名全小写;user-agent识别浏览器,accept-language决定国际化。 /favicon.ico是浏览器自动请求,直接res.end()空响应即可,避免干扰日志。- 第三方
table包仅用于终端美化输出,与 HTTP 协议无关;生产日志更常用 JSON 行或 Winston。 - 响应前设置
Content-type: text/html;charset=utf-8,否则中文可能乱码。
3.2 获取URL查询字符串
方法一:使用url模块(传统方法)
javascript
const http = require('http');
const url = require('url'); // 【代码注释】 Node.js内置的URL解析模块
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') {
return res.end();
}
// 解析URL 【代码注释】 第二个参数true表示将查询字符串解析为对象
const urlInfo = url.parse(req.url, true);
console.log('完整URL:', urlInfo.href);
console.log('路径名:', urlInfo.pathname); // 如 /search
console.log('查询字符串对象:', urlInfo.query); // 如 { wd: 'nodejs', type: 'blog' }
// 获取特定查询参数 【代码注释】
const searchKeyword = urlInfo.query.wd;
const searchType = urlInfo.query.type;
// 生成动态响应内容 【代码注释】
const content = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>搜索结果</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.result { padding: 10px; border: 1px solid #ddd; margin: 10px 0; }
.keyword { color: #0066cc; font-weight: bold; }
</style>
</head>
<body>
<h1>搜索结果</h1>
<div class="result">
<p>搜索关键词: <span class="keyword">${searchKeyword || '未指定'}</span></p>
<p>搜索类型: <span class="keyword">${searchType || '全部'}</span></p>
<p>来源: ${urlInfo.query.origin || '直接访问'}</p>
</div>
<p>测试URL示例:</p>
<ul>
<li><a href="?wd=nodejs&type=blog&origin=google">搜索 nodejs 博客</a></li>
<li><a href="?wd=react&type=docs&origin=github">搜索 react 文档</a></li>
</ul>
</body>
</html>
`;
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(content);
});
server.listen(8080, () => {
console.log('搜索演示服务器运行在 http://localhost:8080');
});
【代码注释】
url.parse(req.url, true)第二参数true把?wd=nodejs&type=blog解析为对象query,值均为字符串。pathname不含查询串,路由应用pathname判断,用query取参数,不要对整个req.url做字符串相等比较。url.parse为传统 API,仍见于老项目;新项目推荐URL+searchParams(与浏览器一致)。- 重复键
?a=1&a=2时query.a可能是数组,需业务层兼容。
方法二:使用URL类(现代方法)
javascript
const http = require('http');
const { URL } = require('url'); // 【代码注释】 ES6风格导入URL类
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') {
return res.end();
}
try {
// 创建URL对象 【代码注释】 需要提供完整的URL地址
const myUrl = new URL('http://localhost:8080' + req.url);
console.log('URL路径:', myUrl.pathname);
console.log('查询参数:', myUrl.searchParams);
// 使用searchParams API 【代码注释】 提供了丰富的操作方法
const param1 = myUrl.searchParams.get('category'); // 获取单个参数
const param2 = myUrl.searchParams.get('id'); // 获取另一个参数
const allParams = Object.fromEntries(myUrl.searchParams); // 转换为普通对象
// searchParams的其他有用方法 【代码注释】
console.log('是否包含category参数:', myUrl.searchParams.has('category'));
console.log('所有参数名称:', [...myUrl.searchParams.keys()]);
console.log('所有参数值:', [...myUrl.searchParams.values()]);
const responseHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>URL解析演示</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 30px; background: #f5f5f5; }
.container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.param { background: #f8f9fa; padding: 10px; margin: 5px 0; border-left: 4px solid #007bff; }
h1 { color: #333; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<div class="container">
<h1>URL参数解析结果</h1>
<div class="param">分类: <strong>${param1 || '未指定'}</strong></div>
<div class="param">ID: <strong>${param2 || '未指定'}</strong></div>
<div class="param">原始查询字符串: <code>${myUrl.search}</code></div>
<div class="param">完整参数对象: <code>${JSON.stringify(allParams)}</code></div>
<h3>测试示例:</h3>
<ul>
<li><a href="/product?category=electronics&id=123">电子产品页面</a></li>
<li><a href="/product?category=books&id=456&sort=price">图书页面(带排序)</a></li>
</ul>
</div>
</body>
</html>
`;
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(responseHtml);
} catch (error) {
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('无效的URL格式');
}
});
server.listen(8080, () => {
console.log('现代URL解析服务器运行在 http://localhost:8080');
});
【代码注释】
new URL('http://localhost:8080' + req.url)在本地开发时常用固定 host;反向代理后应读req.headers['x-forwarded-proto']与host拼真实公网 URL。searchParams.get/has/keys与浏览器URLSearchParamsAPI 一致,便于前后端共用同一套参数名。try/catch包裹:畸形 URL 返回 400,避免未处理异常导致连接挂起。Object.fromEntries(myUrl.searchParams)便于打日志或传给模板引擎。
方法三:URL 类 + 手动拼接主机(与 req.url 仅含路径时配合)
javascript
const http = require('http');
const { URL } = require('url');
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') return res.end();
// 【代码注释】req.url 通常只有 pathname+search,需补全协议与主机才能 new URL
const urlInfo = new URL('http://127.0.0.1:8080' + req.url);
console.log(urlInfo.searchParams.get('a'));
console.log(urlInfo.searchParams.get('b'));
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(`
<h1>查询参数演示</h1>
<p>a = ${urlInfo.searchParams.get('a') ?? '无'}</p>
<p>b = ${urlInfo.searchParams.get('b') ?? '无'}</p>
<p><a href="?a=1&b=2">测试 ?a=1&b=2</a></p>
`);
});
server.listen(8080, () => console.log('http://127.0.0.1:8080'));
【代码注释】
new URL('http://127.0.0.1:8080' + req.url):因req.url无协议与主机,必须补全基址才能构造 WHATWGURL对象。searchParams.get('a')无参数时返回null,示例用?? '无'显示占位。- 查询串与请求方法无关 :GET 的
?wd=node、POST 登录页的?from=home、PUT 带 query 均可;不要误以为只有 GET 才有?。 - 请求体 (POST 表单、JSON API)与查询串是两套数据:一个在 URL,一个在
data/end事件里读。
3.3 获取请求体信息
#mermaid-svg-Iwcetev3CmlCunok{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Iwcetev3CmlCunok .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Iwcetev3CmlCunok .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Iwcetev3CmlCunok .error-icon{fill:#552222;}#mermaid-svg-Iwcetev3CmlCunok .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Iwcetev3CmlCunok .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Iwcetev3CmlCunok .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Iwcetev3CmlCunok .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Iwcetev3CmlCunok .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Iwcetev3CmlCunok .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Iwcetev3CmlCunok .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Iwcetev3CmlCunok .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Iwcetev3CmlCunok .marker.cross{stroke:#333333;}#mermaid-svg-Iwcetev3CmlCunok svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Iwcetev3CmlCunok p{margin:0;}#mermaid-svg-Iwcetev3CmlCunok .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Iwcetev3CmlCunok .cluster-label text{fill:#333;}#mermaid-svg-Iwcetev3CmlCunok .cluster-label span{color:#333;}#mermaid-svg-Iwcetev3CmlCunok .cluster-label span p{background-color:transparent;}#mermaid-svg-Iwcetev3CmlCunok .label text,#mermaid-svg-Iwcetev3CmlCunok span{fill:#333;color:#333;}#mermaid-svg-Iwcetev3CmlCunok .node rect,#mermaid-svg-Iwcetev3CmlCunok .node circle,#mermaid-svg-Iwcetev3CmlCunok .node ellipse,#mermaid-svg-Iwcetev3CmlCunok .node polygon,#mermaid-svg-Iwcetev3CmlCunok .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Iwcetev3CmlCunok .rough-node .label text,#mermaid-svg-Iwcetev3CmlCunok .node .label text,#mermaid-svg-Iwcetev3CmlCunok .image-shape .label,#mermaid-svg-Iwcetev3CmlCunok .icon-shape .label{text-anchor:middle;}#mermaid-svg-Iwcetev3CmlCunok .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Iwcetev3CmlCunok .rough-node .label,#mermaid-svg-Iwcetev3CmlCunok .node .label,#mermaid-svg-Iwcetev3CmlCunok .image-shape .label,#mermaid-svg-Iwcetev3CmlCunok .icon-shape .label{text-align:center;}#mermaid-svg-Iwcetev3CmlCunok .node.clickable{cursor:pointer;}#mermaid-svg-Iwcetev3CmlCunok .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Iwcetev3CmlCunok .arrowheadPath{fill:#333333;}#mermaid-svg-Iwcetev3CmlCunok .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Iwcetev3CmlCunok .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Iwcetev3CmlCunok .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Iwcetev3CmlCunok .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Iwcetev3CmlCunok .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Iwcetev3CmlCunok .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Iwcetev3CmlCunok .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Iwcetev3CmlCunok .cluster text{fill:#333;}#mermaid-svg-Iwcetev3CmlCunok .cluster span{color:#333;}#mermaid-svg-Iwcetev3CmlCunok div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Iwcetev3CmlCunok .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Iwcetev3CmlCunok rect.text{fill:none;stroke-width:0;}#mermaid-svg-Iwcetev3CmlCunok .icon-shape,#mermaid-svg-Iwcetev3CmlCunok .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Iwcetev3CmlCunok .icon-shape p,#mermaid-svg-Iwcetev3CmlCunok .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Iwcetev3CmlCunok .icon-shape .label rect,#mermaid-svg-Iwcetev3CmlCunok .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Iwcetev3CmlCunok .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Iwcetev3CmlCunok .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Iwcetev3CmlCunok :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET/DELETE
POST/PUT
HTTP请求
请求方式
无请求体
有请求体
data事件
分块接收数据
end事件
数据接收完成
完整示例代码:
javascript
const http = require('http');
const qs = require('querystring'); // 【代码注释】 用于解析查询字符串格式的数据
const path = require('path');
const fs = require('fs');
const server = http.createServer((req, res) => {
// 处理表单提交 【代码注释】
if (req.url === '/favicon.ico') {
return res.end();
}
// 定义字符串用于拼接请求体数据 【代码注释】
let reqBody = '';
/*
监听data事件 【代码注释】
1. 当请求体数据到达时触发
2. 请求对象本质上是一个可读流
3. 数据可能分多次到达,需要累积拼接
4. chunk参数是Buffer类型,+=操作会自动转换为字符串
*/
req.on('data', (chunk) => {
reqBody += chunk;
console.log(`接收数据块: ${chunk.length} 字节`);
});
/*
监听end事件 【代码注释】
1. 当请求体数据全部接收完毕时触发
2. 在此回调中处理完整的请求数据
3. 只有在end事件中才能确保数据完整
*/
req.on('end', () => {
// 解析请求体数据 【代码注释】 将字符串解析为对象
const body = qs.parse(reqBody);
// 构建文件存储内容 【代码注释】
const content = `
联系人信息表单
==================
姓名:${body.name}
邮箱:${body.email}
电话:${body.phone}
生日:${body.birthday}
留言内容:
${body.message}
提交时间:${new Date().toLocaleString()}
`;
// 定义存储文件路径 【代码注释】 以客户端IP作为文件名
const clientIp = req.socket.remoteAddress.slice(7); // 去除IPv6前缀 ::ffff:
const filename = path.join(__dirname, 'dbs', `${clientIp}_${Date.now()}.txt`);
// 确保目录存在 【代码注释】
const dir = path.dirname(filename);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 写入文件 【代码注释】 异步写入避免阻塞
fs.writeFile(filename, content, (err) => {
if (err) {
console.error('文件写入失败:', err);
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>提交失败</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
.error { color: #dc3545; font-size: 24px; }
</style>
</head>
<body>
<div class="error">❌ 很遗憾,表单提交失败!</div>
<p><a href="/">返回首页</a></p>
</body>
</html>
`);
} else {
console.log('表单数据已保存到:', filename);
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>提交成功</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
.success { color: #28a745; font-size: 24px; }
.info { background: #f8f9fa; padding: 20px; margin: 20px auto; max-width: 500px; border-radius: 8px; }
</style>
</head>
<body>
<div class="success">✅ 恭喜您,表单提交成功!</div>
<div class="info">
<h3>提交的信息:</h3>
<p>姓名:${body.name}</p>
<p>邮箱:${body.email}</p>
<p>电话:${body.phone}</p>
</div>
<p><a href="/">返回首页</a></p>
</body>
</html>
`);
}
});
});
});
server.listen(8080, () => {
console.log('表单处理服务器运行在 http://localhost:8080');
});
【代码注释】
req在 HTTP 层是可读流 :POST 体通过req.on('data')分块到达,在req.on('end')里才保证拼完整,禁止 在data之前qs.parse(reqBody)。reqBody += chunk会把 Buffer 转成字符串;application/x-www-form-urlencoded表单用qs.parse即可;若是application/json应JSON.parse。Content-Type: application/x-www-form-urlencoded时字段名为name、email等,与 HTML<input name="...">一一对应。- GET/HEAD/DELETE 通常没有 请求体,不要对 GET 注册
data监听还期待有数据。 - 写盘前
mkdirSync(..., { recursive: true });响应用end返回 HTML,成功/失败分支都要setHeader再end,且每个请求只end一次。 - 大表单或文件上传应改用
multipart/form-data+ 专用解析(或 Expressmulter),本示例适合课堂「联系人表单」场景。
配套HTML表单:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>联系表单</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<style>
.form-box { margin-top: 60px; }
.form-header { text-align: center; margin-bottom: 30px; }
</style>
</head>
<body>
<div class="container form-box">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="form-header">
<h1>联系我们</h1>
<p class="text-muted">请填写以下信息,我们会尽快回复您</p>
</div>
<form action="http://localhost:8080" method="post">
<div class="form-group">
<label for="name">您的姓名</label>
<input name="name" type="text" class="form-control" id="name"
placeholder="请输入姓名" required>
</div>
<div class="form-group">
<label for="email">电子邮箱</label>
<input name="email" type="email" class="form-control" id="email"
placeholder="example@email.com" required>
</div>
<div class="form-group">
<label for="phone">联系电话</label>
<input name="phone" type="tel" class="form-control" id="phone"
placeholder="请输入电话号码" required>
</div>
<div class="form-group">
<label for="birthday">出生日期</label>
<input name="birthday" type="date" class="form-control" id="birthday" required>
</div>
<div class="form-group">
<label for="message">留言内容</label>
<textarea name="message" class="form-control" rows="6" id="message"
placeholder="请输入您的留言内容..." required></textarea>
</div>
<button type="submit" class="btn btn-primary btn-block">提交表单</button>
</form>
</div>
</div>
</div>
</body>
</html>
【代码注释】
method="post"+action="http://localhost:8080":提交时浏览器发 POST ,请求体为name=xx&email=yy&...格式,服务端在end里qs.parse。action必须与服务端listen端口一致;改端口后 HTML 里的 URL 也要改。required为浏览器端校验,服务端仍需校验,不能信任客户端。- Bootstrap CDN 仅样式;课堂可把
action改为/submit并在服务端用pathname路由。
3.4 请求处理的经典应用场景
API接口设计模式:
javascript
// RESTful API 设计示例
const server = http.createServer((req, res) => {
const [pathname, queryString] = req.url.split('?');
switch (req.method) {
case 'GET':
// 获取资源列表或单个资源
handleGetRequest(pathname, res);
break;
case 'POST':
// 创建新资源
handlePostRequest(pathname, req, res);
break;
case 'PUT':
// 更新资源
handlePutRequest(pathname, req, res);
break;
case 'DELETE':
// 删除资源
handleDeleteRequest(pathname, res);
break;
default:
sendErrorResponse(res, 405, 'Method Not Allowed');
}
});
3.5 HTTP 客户端:发起出站请求
Node.js 不仅能接收 客户端请求,也能作为HTTP 客户端主动向第三方发起请求,常用于对接外部 API、构建 BFF(Backend For Frontend)层、反向代理等场景。
#mermaid-svg-acxmHDB2hhdxAAwS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-acxmHDB2hhdxAAwS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-acxmHDB2hhdxAAwS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-acxmHDB2hhdxAAwS .error-icon{fill:#552222;}#mermaid-svg-acxmHDB2hhdxAAwS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-acxmHDB2hhdxAAwS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-acxmHDB2hhdxAAwS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-acxmHDB2hhdxAAwS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-acxmHDB2hhdxAAwS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-acxmHDB2hhdxAAwS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-acxmHDB2hhdxAAwS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-acxmHDB2hhdxAAwS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-acxmHDB2hhdxAAwS .marker.cross{stroke:#333333;}#mermaid-svg-acxmHDB2hhdxAAwS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-acxmHDB2hhdxAAwS p{margin:0;}#mermaid-svg-acxmHDB2hhdxAAwS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-acxmHDB2hhdxAAwS .cluster-label text{fill:#333;}#mermaid-svg-acxmHDB2hhdxAAwS .cluster-label span{color:#333;}#mermaid-svg-acxmHDB2hhdxAAwS .cluster-label span p{background-color:transparent;}#mermaid-svg-acxmHDB2hhdxAAwS .label text,#mermaid-svg-acxmHDB2hhdxAAwS span{fill:#333;color:#333;}#mermaid-svg-acxmHDB2hhdxAAwS .node rect,#mermaid-svg-acxmHDB2hhdxAAwS .node circle,#mermaid-svg-acxmHDB2hhdxAAwS .node ellipse,#mermaid-svg-acxmHDB2hhdxAAwS .node polygon,#mermaid-svg-acxmHDB2hhdxAAwS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-acxmHDB2hhdxAAwS .rough-node .label text,#mermaid-svg-acxmHDB2hhdxAAwS .node .label text,#mermaid-svg-acxmHDB2hhdxAAwS .image-shape .label,#mermaid-svg-acxmHDB2hhdxAAwS .icon-shape .label{text-anchor:middle;}#mermaid-svg-acxmHDB2hhdxAAwS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-acxmHDB2hhdxAAwS .rough-node .label,#mermaid-svg-acxmHDB2hhdxAAwS .node .label,#mermaid-svg-acxmHDB2hhdxAAwS .image-shape .label,#mermaid-svg-acxmHDB2hhdxAAwS .icon-shape .label{text-align:center;}#mermaid-svg-acxmHDB2hhdxAAwS .node.clickable{cursor:pointer;}#mermaid-svg-acxmHDB2hhdxAAwS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-acxmHDB2hhdxAAwS .arrowheadPath{fill:#333333;}#mermaid-svg-acxmHDB2hhdxAAwS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-acxmHDB2hhdxAAwS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-acxmHDB2hhdxAAwS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-acxmHDB2hhdxAAwS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-acxmHDB2hhdxAAwS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-acxmHDB2hhdxAAwS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-acxmHDB2hhdxAAwS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-acxmHDB2hhdxAAwS .cluster text{fill:#333;}#mermaid-svg-acxmHDB2hhdxAAwS .cluster span{color:#333;}#mermaid-svg-acxmHDB2hhdxAAwS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-acxmHDB2hhdxAAwS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-acxmHDB2hhdxAAwS rect.text{fill:none;stroke-width:0;}#mermaid-svg-acxmHDB2hhdxAAwS .icon-shape,#mermaid-svg-acxmHDB2hhdxAAwS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-acxmHDB2hhdxAAwS .icon-shape p,#mermaid-svg-acxmHDB2hhdxAAwS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-acxmHDB2hhdxAAwS .icon-shape .label rect,#mermaid-svg-acxmHDB2hhdxAAwS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-acxmHDB2hhdxAAwS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-acxmHDB2hhdxAAwS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-acxmHDB2hhdxAAwS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户请求
出站请求
返回数据
聚合响应
浏览器
Node 服务
外部 API / 微服务
方法一:http.get() --- 简单 GET 请求
javascript
const http = require('http');
// http.get 是 http.request 的便捷封装,自动调用 req.end()
// 适合只需要 GET + 读响应体的场景
http.get('http://jsonplaceholder.typicode.com/todos/1', (res) => {
console.log('响应状态码:', res.statusCode);
console.log('响应头:', res.headers['content-type']);
let data = '';
// 响应对象也是可读流,需分块拼接
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
const todo = JSON.parse(data);
console.log('获取到的 TODO:', todo);
// { userId: 1, id: 1, title: '...', completed: false }
});
}).on('error', (err) => {
// 网络错误(DNS 失败、连接超时等)在这里捕获
console.error('请求失败:', err.message);
});
【代码注释】
http.get(url, callback)中callback的参数是IncomingMessage(即响应对象res),不是完整数据,需要再通过data/end事件拼接。- 注意区分两个
res:服务器收到的请求用req,向外发出的请求的响应 也叫res,是IncomingMessage类型(只读流)。 - 非 2xx 状态码不会触发
error事件 ------404、500 仍走data/end,需自己判断res.statusCode。 http.get只能发 GET 请求;POST/PUT/DELETE需用http.request。
方法二:http.request() --- 发送 POST JSON 请求
javascript
const http = require('http');
// 要发送的请求体(JSON 格式)
const postData = JSON.stringify({
title: 'Node.js HTTP 客户端',
body: '使用 http.request 发送 POST 请求',
userId: 1,
});
// 请求配置项
const options = {
hostname: 'jsonplaceholder.typicode.com',
port: 80,
path: '/posts',
method: 'POST',
headers: {
'Content-Type': 'application/json', // 告诉服务端发送的是 JSON
'Content-Length': Buffer.byteLength(postData), // 必须用字节长度,非字符数
},
};
const req = http.request(options, (res) => {
console.log(`状态码: ${res.statusCode}`); // 201 Created
let responseData = '';
res.setEncoding('utf8');
res.on('data', (chunk) => { responseData += chunk; });
res.on('end', () => {
console.log('服务端响应:', JSON.parse(responseData));
});
});
// 捕获请求级错误(网络层)
req.on('error', (err) => {
console.error('出站请求错误:', err.message);
});
// 写入请求体并结束请求(不调用 end 请求不会发出)
req.write(postData);
req.end();
【代码注释】
Content-Length必须用 字节长度Buffer.byteLength(str)而非str.length,中文字符 UTF-8 下每个字符占 3 字节,不一致会导致请求截断。req.end()是发送请求的触发点;不调用end()请求永不发出 ------这与服务端的res.end()语义对称。options.hostname不含http://;如需 HTTPS 则用require('https')且port: 443。req.on('error')捕获的是网络/连接级错误(DNS 失败、超时、ECONNREFUSED),HTTP 4xx/5xx 不在此列。
方法三:BFF 层聚合多个 API(实战场景)
javascript
const http = require('http');
const https = require('https'); // HTTPS 出站请求用 https 模块
// 封装为 Promise,便于 async/await 使用
function fetchJSON(url) {
return new Promise((resolve, reject) => {
const mod = url.startsWith('https') ? https : http;
mod.get(url, (res) => {
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode}: ${url}`));
}
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch (e) { reject(e); }
});
}).on('error', reject);
});
}
// BFF 服务:聚合用户信息 + 代办事项
const server = http.createServer(async (req, res) => {
if (req.url !== '/dashboard') {
res.writeHead(404);
return res.end('Not Found');
}
try {
// 并发请求两个外部 API(用 Promise.all 节省等待时间)
const [user, todos] = await Promise.all([
fetchJSON('https://jsonplaceholder.typicode.com/users/1'),
fetchJSON('https://jsonplaceholder.typicode.com/todos?userId=1&_limit=5'),
]);
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
user: { id: user.id, name: user.name, email: user.email },
recentTodos: todos.map(t => ({ id: t.id, title: t.title, done: t.completed })),
}));
} catch (err) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '上游服务异常', detail: err.message }));
}
});
server.listen(8080, () => console.log('BFF 服务运行在 http://localhost:8080/dashboard'));
【代码注释】
Promise.all([fetchA, fetchB])并发发起多个外部请求,总耗时取最慢那个,比串行快得多------这是 BFF 层聚合的核心技巧。- 封装
fetchJSON时统一处理流拼接和 JSON 解析,调用侧用async/await编写,可读性接近同步代码。 - 上游 API 异常时返回 502 Bad Gateway(而非 500),语义更准确------502 表示网关从上游收到无效响应。
- 真实项目用
node-fetch、axios、got等库替代手写 Promise;但理解底层有助于调试连接超时、重定向、证书等问题。 https.get与http.getAPI 完全一样,切换只需换模块名------底层多了 TLS 握手。
4. 响应报文设置
4.1 设置响应行
javascript
const http = require('http');
const server = http.createServer((req, res) => {
/*
设置响应状态码 【代码注释】
常用状态码:
- 200: 成功
- 201: 创建成功
- 301: 永久重定向
- 302: 临时重定向
- 400: 客户端请求错误
- 404: 资源未找到
- 500: 服务器内部错误
*/
res.statusCode = 201; // 设置为201表示资源创建成功
/*
设置响应状态描述 【代码注释】
状态描述是对状态码的文本说明
*/
res.statusMessage = 'Happy'; // 自定义状态描述
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>响应状态演示</title>
</head>
<body>
<h1>响应状态码: ${res.statusCode}</h1>
<p>状态描述: ${res.statusMessage}</p>
</body>
</html>
`);
});
server.listen(8080, () => {
console.log('响应状态演示服务器运行在 http://localhost:8080');
});
【代码注释】
- 响应行 :
res.statusCode(数字,如 200、404)+res.statusMessage(文本说明,可自定义但不建议随意编造标准含义)。 - 浏览器 DevTools → Network 可看到状态码;API 常用 201 创建、301/302 重定向、400 客户端错、404 未找到、500 服务端错。
- 仅改
statusCode不writeHead时,仍须在end前设置;writeHead会一次性发送状态行+头。
4.2 设置响应头
javascript
const http = require('http');
const server = http.createServer((req, res) => {
/*
方法一:使用setHeader设置单个响应头 【代码注释】
格式:res.setHeader('名称', '值')
可以多次调用setHeader设置不同的响应头
*/
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.setHeader('Server', 'MyServer 1.0.1');
res.setHeader('X-Powered-By', 'Node.js');
res.setHeader('Cache-Control', 'max-age=3600');
/*
方法二:使用writeHead同时设置状态码、状态描述和响应头 【代码注释】
这个方法会发送响应头,应该在write()或end()之前调用
一旦响应头发送,就不能再修改了
*/
res.writeHead(200, 'OK', {
'Server': 'Nginx 2.1.1',
'Token': 'secure-token-12345',
'Content-Type': 'text/html; charset=utf-8',
'Set-Cookie': ['user=admin', 'session=abc123']
});
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>响应头设置演示</title>
<style>
body { font-family: Arial, sans-serif; margin: 30px; }
.header-info { background: #f8f9fa; padding: 15px; border-left: 4px solid #007bff; }
</style>
</head>
<body>
<h1>响应头设置演示</h1>
<div class="header-info">
<p>打开浏览器开发者工具 -> Network标签,查看响应头信息</p>
<ul>
<li>Server: Nginx 2.1.1</li>
<li>Content-Type: text/html; charset=utf-8</li>
<li>Token: secure-token-12345</li>
</ul>
</div>
</body>
</html>
`);
});
server.listen(8080, () => {
console.log('响应头设置演示服务器运行在 http://localhost:8080');
});
【代码注释】
setHeader可多次调用,在首次写入响应体之前 设置;Content-Type告诉浏览器如何解析 body(HTML/JSON/文件下载)。writeHead(status, message, headers)会立即发送 响应头,之后不宜再setHeader(已发出)。Set-Cookie可数组形式设置多个 Cookie;Cache-Control控制缓存;Access-Control-Allow-Origin用于跨域(见 §7.4 HTML 联调)。- 示例中先
setHeader再writeHead仅为演示,实际项目选一种方式即可,避免重复或覆盖。
4.3 设置响应体
javascript
const http = require('http');
const server = http.createServer((req, res) => {
// 设置响应头 【代码注释】
res.setHeader('Content-type', 'text/html;charset=utf-8');
/*
方法一:多次write写入响应体 【代码注释】
write方法可以多次调用,向响应体中写入数据
适合分块传输大数据
*/
res.write('<!DOCTYPE html>');
res.write('<html><head><meta charset="UTF-8"><title>响应体演示</title></head><body>');
res.write('<h1>响应体设置演示</h1>');
res.write('<hr>');
// 动态生成内容 【代码注释】
for (let i = 1; i <= 5; i++) {
res.write(`<p>这是第 ${i} 段内容</p>`);
res.write(`<p>随机数: ${Math.random()}</p>`);
}
/*
方法二:使用end方法结束响应 【代码注释】
end方法可以接收参数,作为最后一次写入响应体的内容
调用end后,响应就完成了,不能再写入数据
*/
res.end('<hr><p style="color: green;">响应结束</p></body></html>');
/*
错误示例:end后不能再write 【代码注释】
res.end后,响应已完成,再次调用write会报错
try {
res.write('这行代码不会执行');
} catch (error) {
console.error('错误:', error.message);
}
*/
});
server.listen(8080, () => {
console.log('响应体设置演示服务器运行在 http://localhost:8080');
});
【代码注释】
res.write(chunk)可多次调用,适合分块输出大 HTML 或流式数据;最后一次用res.end(最后一块)结束响应。res.end()之后不能再write,否则抛ERR_HTTP_HEADERS_SENT或类似错误------每个请求只能结束一次。- 也可直接
res.end(整个字符串)一次写完小页面;静态小文件readFile后end(data)同理。 - 分块写时也要先设好
Content-Type,否则浏览器可能按错误编码解析。
4.4 完整响应设置综合示例
javascript
const http = require('http');
const server = http.createServer((req, res) => {
// 根据不同的请求路径返回不同的响应 【代码注释】
if (req.url === '/api/info') {
// JSON数据响应 【代码注释】
res.writeHead(200, 'OK', {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*' // CORS跨域设置
});
res.end(JSON.stringify({
status: 'success',
data: {
name: 'Node.js HTTP服务器',
version: '1.0.0',
features: ['异步非阻塞', '事件驱动', '高性能']
},
timestamp: new Date().toISOString()
}));
}
else if (req.url === '/download') {
// 文件下载响应 【代码注释】
const fileContent = '这是一个示例文件内容\n可以用于演示文件下载功能';
res.writeHead(200, 'OK', {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Disposition': 'attachment; filename="example.txt"',
'Content-Length': fileContent.length
});
res.end(fileContent);
}
else if (req.url === '/redirect') {
// 重定向响应 【代码注释】
res.writeHead(301, 'Moved Permanently', {
'Location': 'https://nodejs.org'
});
res.end();
}
else if (req.url === '/error') {
// 错误响应 【代码注释】
res.writeHead(500, 'Internal Server Error', {
'Content-Type': 'text/html; charset=utf-8'
});
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>服务器错误</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
.error { color: #dc3545; font-size: 48px; }
</style>
</head>
<body>
<div class="error">500</div>
<h1>Internal Server Error</h1>
<p>服务器遇到了一些问题</p>
</body>
</html>
`);
}
else {
// 默认响应 【代码注释】
res.writeHead(200, 'OK', {
'Content-Type': 'text/html; charset=utf-8'
});
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HTTP响应演示</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #333; border-bottom: 3px solid #007bff; padding-bottom: 10px; }
.endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 4px; border-left: 4px solid #28a745; }
.endpoint a { color: #007bff; text-decoration: none; font-weight: bold; }
.endpoint a:hover { text-decoration: underline; }
.description { color: #666; font-size: 14px; margin-top: 5px; }
</style>
</head>
<body>
<div class="container">
<h1>HTTP响应演示页面</h1>
<p>点击下面的链接体验不同的响应类型:</p>
<div class="endpoint">
<a href="/api/info">/api/info</a>
<div class="description">返回JSON格式的API数据</div>
</div>
<div class="endpoint">
<a href="/download">/download</a>
<div class="description">触发文件下载功能</div>
</div>
<div class="endpoint">
<a href="/redirect">/redirect</a>
<div class="description">301永久重定向到Node.js官网</div>
</div>
<div class="endpoint">
<a href="/error">/error</a>
<div class="description">模拟500服务器内部错误</div>
</div>
</div>
</body>
</html>
`);
}
});
server.listen(8080, () => {
console.log('HTTP响应综合演示服务器运行在 http://localhost:8080');
});
【代码注释】
/api/info:Content-Type: application/json+JSON.stringify,是 REST API 最简形态;Access-Control-Allow-Origin: *便于浏览器跨域读 JSON(生产应限制域名)。/download:Content-Disposition: attachment触发下载;Content-Length与 body 字节数一致可避免客户端截断。/redirect:301+Location头,浏览器自动跳转;end()可无 body。/error:演示500,应对未捕获异常时也应返回统一 JSON/HTML,勿把堆栈直接给前端。
5. 路由处理
5.1 基于路径的路由
#mermaid-svg-m1Z7rl64NDNaMQkj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m1Z7rl64NDNaMQkj .error-icon{fill:#552222;}#mermaid-svg-m1Z7rl64NDNaMQkj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m1Z7rl64NDNaMQkj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m1Z7rl64NDNaMQkj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m1Z7rl64NDNaMQkj .marker.cross{stroke:#333333;}#mermaid-svg-m1Z7rl64NDNaMQkj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m1Z7rl64NDNaMQkj p{margin:0;}#mermaid-svg-m1Z7rl64NDNaMQkj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-m1Z7rl64NDNaMQkj .cluster-label text{fill:#333;}#mermaid-svg-m1Z7rl64NDNaMQkj .cluster-label span{color:#333;}#mermaid-svg-m1Z7rl64NDNaMQkj .cluster-label span p{background-color:transparent;}#mermaid-svg-m1Z7rl64NDNaMQkj .label text,#mermaid-svg-m1Z7rl64NDNaMQkj span{fill:#333;color:#333;}#mermaid-svg-m1Z7rl64NDNaMQkj .node rect,#mermaid-svg-m1Z7rl64NDNaMQkj .node circle,#mermaid-svg-m1Z7rl64NDNaMQkj .node ellipse,#mermaid-svg-m1Z7rl64NDNaMQkj .node polygon,#mermaid-svg-m1Z7rl64NDNaMQkj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m1Z7rl64NDNaMQkj .rough-node .label text,#mermaid-svg-m1Z7rl64NDNaMQkj .node .label text,#mermaid-svg-m1Z7rl64NDNaMQkj .image-shape .label,#mermaid-svg-m1Z7rl64NDNaMQkj .icon-shape .label{text-anchor:middle;}#mermaid-svg-m1Z7rl64NDNaMQkj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-m1Z7rl64NDNaMQkj .rough-node .label,#mermaid-svg-m1Z7rl64NDNaMQkj .node .label,#mermaid-svg-m1Z7rl64NDNaMQkj .image-shape .label,#mermaid-svg-m1Z7rl64NDNaMQkj .icon-shape .label{text-align:center;}#mermaid-svg-m1Z7rl64NDNaMQkj .node.clickable{cursor:pointer;}#mermaid-svg-m1Z7rl64NDNaMQkj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-m1Z7rl64NDNaMQkj .arrowheadPath{fill:#333333;}#mermaid-svg-m1Z7rl64NDNaMQkj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-m1Z7rl64NDNaMQkj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-m1Z7rl64NDNaMQkj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m1Z7rl64NDNaMQkj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-m1Z7rl64NDNaMQkj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m1Z7rl64NDNaMQkj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-m1Z7rl64NDNaMQkj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m1Z7rl64NDNaMQkj .cluster text{fill:#333;}#mermaid-svg-m1Z7rl64NDNaMQkj .cluster span{color:#333;}#mermaid-svg-m1Z7rl64NDNaMQkj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-m1Z7rl64NDNaMQkj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m1Z7rl64NDNaMQkj rect.text{fill:none;stroke-width:0;}#mermaid-svg-m1Z7rl64NDNaMQkj .icon-shape,#mermaid-svg-m1Z7rl64NDNaMQkj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m1Z7rl64NDNaMQkj .icon-shape p,#mermaid-svg-m1Z7rl64NDNaMQkj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-m1Z7rl64NDNaMQkj .icon-shape .label rect,#mermaid-svg-m1Z7rl64NDNaMQkj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m1Z7rl64NDNaMQkj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m1Z7rl64NDNaMQkj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m1Z7rl64NDNaMQkj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} / or /index
/login
/register
其他
客户端请求
解析URL路径
路径匹配
首页处理器
登录页面处理器
注册页面处理器
404处理器
完整实现代码:
javascript
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
/*
解析URL获取路径名 【代码注释】
parse方法将URL解析为一个对象,pathname属性包含路径部分
例如:http://localhost:8080/login?redirect=/home 的pathname是 '/login'
*/
const pathname = url.parse(req.url).pathname;
console.log(`访问路径: ${pathname}`);
/*
使用switch语句进行路由分发 【代码注释】
根据不同的路径名返回不同的内容
这是多页面应用的基础路由实现方式
*/
switch (pathname) {
case '/':
case '/index':
// 首页处理逻辑 【代码注释】
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>首页</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; margin: 0; padding: 0; background: #f4f4f4; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; text-align: center; }
.content { max-width: 1200px; margin: 30px auto; padding: 20px; }
.nav-links { text-align: center; margin: 30px 0; }
.nav-links a { display: inline-block; margin: 10px 20px; padding: 12px 30px; background: white; color: #667eea; text-decoration: none; border-radius: 25px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.3s; }
.nav-links a:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.15); }
</style>
</head>
<body>
<div class="header">
<h1>欢迎来到我的网站</h1>
<p>这是一个基于Node.js HTTP模块构建的多页面应用</p>
</div>
<div class="content">
<div class="nav-links">
<a href="/">首页</a>
<a href="/login">登录</a>
<a href="/register">注册</a>
</div>
<div style="text-align: center; margin-top: 50px;">
<h2>网站功能介绍</h2>
<ul style="list-style: none; padding: 0;">
<li style="padding: 10px;">✅ 用户注册与登录系统</li>
<li style="padding: 10px;">✅ 路由与页面分发</li>
<li style="padding: 10px;">✅ RESTful API接口</li>
<li style="padding: 10px;">✅ 静态文件服务</li>
</ul>
</div>
</div>
</body>
</html>
`);
break;
case '/login':
// 登录页面处理逻辑 【代码注释】
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户登录</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.login-container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); width: 400px; }
h1 { text-align: center; color: #333; margin-bottom: 30px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; color: #555; font-weight: bold; }
input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 5px; box-sizing: border-box; font-size: 14px; }
input:focus { outline: none; border-color: #667eea; }
button { width: 100%; padding: 12px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; font-weight: bold; }
button:hover { background: #5568d3; }
.links { text-align: center; margin-top: 20px; }
.links a { color: #667eea; text-decoration: none; }
</style>
</head>
<body>
<div class="login-container">
<h1>用户登录</h1>
<form method="post" action="/login">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<button type="submit">登录</button>
</form>
<div class="links">
<p>还没有账号?<a href="/register">立即注册</a></p>
<p><a href="/">返回首页</a></p>
</div>
</div>
</body>
</html>
`);
break;
case '/register':
// 注册页面处理逻辑 【代码注释】
res.setHeader('Content-type', 'text/html;charset=utf-8');
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户注册</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.register-container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); width: 450px; }
h1 { text-align: center; color: #333; margin-bottom: 30px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; color: #555; font-weight: bold; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; box-sizing: border-box; }
input:focus { outline: none; border-color: #667eea; }
button { width: 100%; padding: 12px; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; font-weight: bold; margin-top: 10px; }
button:hover { background: #218838; }
.links { text-align: center; margin-top: 20px; }
.links a { color: #667eea; text-decoration: none; }
</style>
</head>
<body>
<div class="register-container">
<h1>用户注册</h1>
<form method="post" action="/register">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" placeholder="请输入邮箱" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码:</label>
<input type="password" id="confirmPassword" name="confirmPassword" placeholder="请再次输入密码" required>
</div>
<button type="submit">注册</button>
</form>
<div class="links">
<p>已有账号?<a href="/login">立即登录</a></p>
<p><a href="/">返回首页</a></p>
</div>
</div>
</body>
</html>
`);
break;
default:
// 404页面处理逻辑 【代码注释】
res.writeHead(404, 'Not Found', {
'Content-type': 'text/html;charset=utf-8'
});
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>页面未找到</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; background: #f4f4f4; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.error-container { text-align: center; background: white; padding: 50px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
.error-code { font-size: 120px; font-weight: bold; color: #667eea; margin: 0; }
.error-message { font-size: 24px; color: #333; margin: 20px 0; }
.back-link { display: inline-block; margin-top: 30px; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 25px; }
.back-link:hover { background: #5568d3; }
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">404</div>
<div class="error-message">抱歉,您访问的页面不存在</div>
<p>您访问的路径:<code>${pathname}</code></p>
<a href="/" class="back-link">返回首页</a>
</div>
</body>
</html>
`);
}
});
server.listen(8080, () => {
console.log('路由演示服务器运行在 http://localhost:8080');
});
5.2 基于请求方式和路径的路由
javascript
const http = require('http');
const url = require('url');
const fs = require('fs');
const path = require('path');
const qs = require('querystring');
const server = http.createServer((req, res) => {
const pathname = url.parse(req.url).pathname;
/*
路由表设计 【代码注释】
结合请求方式和路径进行更精细的路由控制
这是RESTful API设计的基础
*/
// 首页路由 - 支持所有请求方式 【代码注释】
if (pathname === '/' || pathname === '/index') {
const resBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>首页</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; margin: 0; padding: 0; background: #f4f4f4; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; text-align: center; }
.content { max-width: 1200px; margin: 30px auto; padding: 20px; }
.login-btn { display: inline-block; padding: 15px 40px; background: white; color: #667eea; text-decoration: none; border-radius: 30px; font-weight: bold; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
</style>
</head>
<body>
<div class="header">
<h1>用户认证系统</h1>
<p>演示基于请求方式和路径的路由处理</p>
</div>
<div class="content" style="text-align: center;">
<h2>欢迎访问用户认证系统</h2>
<p>这是一个完整的用户登录注册演示系统</p>
<a href="/login" class="login-btn">前往登录</a>
</div>
</body>
</html>
`;
res.writeHead(200, 'OK', {
'Content-type': 'text/html;charset=utf-8'
});
res.end(resBody);
}
// 登录页面 - GET请求显示登录表单 【代码注释】
else if (pathname === '/login' && req.method === 'GET') {
// 读取HTML文件 【代码注释】
fs.readFile(path.join(__dirname, 'login.html'), (err, data) => {
if (err) {
// 文件读取失败 【代码注释】
res.writeHead(500, 'Internal Server Error', {
'Content-type': 'text/html;charset=utf-8'
});
res.end('<h1>500 服务器内部错误</h1>');
} else {
// 发送文件内容 【代码注释】
res.end(data);
}
});
}
// 登录处理 - POST请求处理登录逻辑 【代码注释】
else if (pathname === '/login' && req.method === 'POST') {
let reqBody = '';
// 接收表单数据 【代码注释】
req.on('data', (chunk) => {
reqBody += chunk;
});
// 处理登录请求 【代码注释】
req.on('end', () => {
// 解析请求体 【代码注释】
const body = qs.parse(reqBody);
// 模拟登录验证 【代码注释】 表单字段名 pwd 或 password 二选一兼容
let resBody = '';
const pwd = body.pwd || body.password;
if (body.username === 'admin' && pwd === '123456') {
// 登录成功 【代码注释】
resBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录成功</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; background: #d4edda; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.success-container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); text-align: center; }
.success-icon { font-size: 60px; color: #28a745; margin-bottom: 20px; }
h1 { color: #28a745; }
.back-link { display: inline-block; margin-top: 20px; padding: 10px 20px; background: #28a745; color: white; text-decoration: none; border-radius: 5px; }
</style>
</head>
<body>
<div class="success-container">
<div class="success-icon">✅</div>
<h1>登录成功!</h1>
<p>欢迎回来,${body.username}!</p>
<a href="/" class="back-link">返回首页</a>
</div>
</body>
</html>
`;
} else {
// 登录失败 【代码注释】
resBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录失败</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; background: #f8d7da; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.error-container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); text-align: center; }
.error-icon { font-size: 60px; color: #dc3545; margin-bottom: 20px; }
h1 { color: #dc3545; }
.back-link { display: inline-block; margin-top: 20px; padding: 10px 20px; background: #dc3545; color: white; text-decoration: none; border-radius: 5px; }
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">❌</div>
<h1>登录失败</h1>
<p>用户名或密码错误</p>
<a href="/login" class="back-link">重新登录</a>
</div>
</body>
</html>
`;
}
// 发送响应 【代码注释】
res.writeHead(200, 'OK', {
'Content-type': 'text/html;charset=utf-8'
});
res.end(resBody);
});
}
// 404页面 - 未匹配的路由 【代码注释】
else {
res.writeHead(404, 'Not Found', {
'Content-type': 'text/html;charset=utf-8'
});
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>404 页面未找到</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; background: #f4f4f4; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.error-container { text-align: center; background: white; padding: 50px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
.error-code { font-size: 120px; font-weight: bold; color: #667eea; margin: 0; }
h1 { color: #333; }
.back-link { display: inline-block; margin-top: 30px; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 25px; }
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">404</div>
<h1>页面不存在</h1>
<p>您访问的页面:${pathname}</p>
<p>请求方式:${req.method}</p>
<a href="/" class="back-link">返回首页</a>
</div>
</body>
</html>
`);
}
});
server.listen(8080, () => {
console.log('用户认证系统运行在 http://localhost:8080');
});
5.3 路由处理的企业级模式
javascript
// 路由器类设计 【代码注释】
class Router {
constructor() {
this.routes = {
GET: {},
POST: {},
PUT: {},
DELETE: {}
};
}
// 注册路由 【代码注释】
register(method, path, handler) {
this.routes[method][path] = handler;
}
// 路由解析 【代码注释】
resolve(req, res) {
const method = req.method;
const url = require('url');
const pathname = url.parse(req.url).pathname;
const handler = this.routes[method][pathname];
if (handler) {
handler(req, res);
} else {
this.handle404(res, pathname);
}
}
// 404处理 【代码注释】
handle404(res, pathname) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`<h1>404 - 路由 ${pathname} 未找到</h1>`);
}
}
// 使用路由器 【代码注释】
const router = new Router();
router.register('GET', '/', (req, res) => {
res.end('首页');
});
router.register('GET', '/api/users', (req, res) => {
res.end('用户列表');
});
router.register('POST', '/api/users', (req, res) => {
res.end('创建用户');
});
const server = http.createServer((req, res) => {
router.resolve(req, res);
});
server.listen(8080);
【代码注释】
Router类将路由注册 与分发 解耦:启动时register(method, path, fn)填表,每次请求resolve(req, res)查表执行。- 路由键
${method}:${pathname}要匹配去掉查询串后的路径;resolve内用url.parse(req.url).pathname剔除?query,避免/api/users?page=2找不到/api/users。 - 未匹配时统一走
handle404返回 404,勿返回 200------状态码是 HTTP 协议语义的一部分,影响浏览器缓存和爬虫行为。 - 这是 Express
app.get('/path', handler)注册机制的简化原型;真实框架还支持参数路由/users/:id、正则匹配、中间件链等。
6. 静态文件服务器
6.1 基础静态文件服务
javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const server = http.createServer((req, res) => {
// 处理favicon请求 【代码注释】
if (req.url === '/favicon.ico') {
return res.end();
}
// 解析请求路径 【代码注释】
const pathname = url.parse(req.url).pathname;
// 构建文件路径 【代码注释】
// 假设静态文件存放在public目录下
const filePath = path.join(__dirname, 'public', pathname === '/' ? 'index.html' : pathname);
// 获取文件扩展名 【代码注释】
const extname = path.extname(filePath);
// 设置Content-Type映射 【代码注释】
const contentTypeMap = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.mp4': 'video/mp4',
'.mp3': 'audio/mpeg',
'.pdf': 'application/pdf'
};
// 根据文件扩展名设置Content-Type 【代码注释】
const contentType = contentTypeMap[extname] || 'application/octet-stream';
// 读取并返回文件 【代码注释】
fs.readFile(filePath, (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
// 文件不存在 【代码注释】
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>404 - 文件未找到</h1>', 'utf-8');
} else {
// 服务器错误 【代码注释】
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>500 - 服务器错误</h1>', 'utf-8');
}
} else {
// 成功返回文件内容 【代码注释】
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
}
});
});
const PORT = 8080;
server.listen(PORT, () => {
console.log(`静态文件服务器运行在 http://localhost:${PORT}`);
});
【代码注释】
pathname === '/'时映射public/index.html,实现站点首页;其它路径如/dist/css/bootstrap.css对应public/dist/css/...。- Content-Type 必须与扩展名匹配 :
.css→text/css,否则浏览器不把响应当样式表,页面会「无样式」------课堂高频坑。 readFile一次性读入内存,大视频/大图应用 §6.2 的createReadStream+pipe。ENOENT→ 404,其它错误→ 500;勿把磁盘路径错误直接返回给客户端。- 生产环境常在 Nginx 托管静态资源,Node 进程只处理动态 API(与 Day10 Express
express.static同类)。
6.2 流式静态文件服务(性能优化版本)
javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') {
return res.end();
}
const pathname = url.parse(req.url).pathname;
const filePath = path.join(__dirname, 'public', pathname === '/' ? 'index.html' : pathname);
// 使用流式传输 【代码注释】
const stream = fs.createReadStream(filePath);
// 设置Content-Type 【代码注释】
const extname = path.extname(filePath);
const contentTypeMap = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.mp4': 'video/mp4',
'.mp3': 'audio/mpeg',
'.pdf': 'application/pdf'
};
const contentType = contentTypeMap[extname] || 'application/octet-stream';
res.setHeader('Content-Type', contentType);
// 使用pipe将文件流直接传输到响应 【代码注释】
// 这种方式更高效,特别是对于大文件
stream.pipe(res);
// 处理流错误 【代码注释】
stream.on('error', (err) => {
if (err.code === 'ENOENT') {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>404 - 文件未找到</h1>', 'utf-8');
} else {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>500 - 服务器错误</h1>', 'utf-8');
}
});
});
const PORT = 8080;
server.listen(PORT, () => {
console.log(`高性能静态文件服务器运行在 http://localhost:${PORT}`);
});
【代码注释】
fs.createReadStream(filePath).pipe(res)是流式传输核心:文件以固定缓冲区分块读取并写入响应,内存占用恒定,不随文件大小增加------视频、PDF 等大文件必用此模式。pipe自动处理背压(Backpressure):当响应流写不过来时会暂停读端,防止内存溢出。stream.on('error')必须监听:若未监听,error事件会作为未处理事件导致 Node 进程崩溃。- 流的
pipe结束后会自动调用res.end(),无需手动调用,再次end()会报ERR_HTTP_HEADERS_SENT。 - 与 §6.1 对比:
readFile一次性读入内存(小文件可用);createReadStream + pipe适合图片、视频、PDF 等大文件。
6.3 带缓存的静态文件服务器
javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const crypto = require('crypto');
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') {
return res.end();
}
const pathname = url.parse(req.url).pathname;
const filePath = path.join(__dirname, 'public', pathname === '/' ? 'index.html' : pathname);
// 获取文件信息 【代码注释】
fs.stat(filePath, (err, stats) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>404 - 文件未找到</h1>');
return;
}
// 设置缓存控制头 【代码注释】
const extname = path.extname(filePath);
const contentTypeMap = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.mp4': 'video/mp4',
'.mp3': 'audio/mpeg',
'.pdf': 'application/pdf'
};
const contentType = contentTypeMap[extname] || 'application/octet-stream';
// 计算ETag 【代码注释】
const etag = crypto.createHash('md5')
.update(stats.mtimeMs.toString())
.digest('hex');
// 检查客户端缓存 【代码注释】
if (req.headers['if-none-match'] === etag) {
res.writeHead(304, 'Not Modified');
res.end();
return;
}
// 设置响应头 【代码注释】
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=86400'); // 1天缓存
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', stats.mtime.toUTCString());
// 使用流式传输 【代码注释】
const stream = fs.createReadStream(filePath);
stream.pipe(res);
stream.on('error', (err) => {
console.error('文件读取错误:', err);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>500 - 服务器错误</h1>');
}
});
});
});
const PORT = 8080;
server.listen(PORT, () => {
console.log(`带缓存的静态文件服务器运行在 http://localhost:${PORT}`);
});
【代码注释】
ETag/Last-Modified:客户端再次请求带If-None-Match时返回 304 Not Modified,无 body,省带宽。Cache-Control: max-age=86400告诉浏览器 1 天内可用本地缓存;与 CDN 缓存策略同类。fs.stat后再createReadStream,避免文件不存在时误开流;stream.on('error')且检查!res.headersSent防止重复写头。pipe(res)利用背压,内存占用稳定;大文件静态服务标准写法。
6.4 静态文件服务器的经典应用场景
实际应用案例:
- Web应用部署:React、Vue等前端框架构建后的文件服务
- CDN服务器:内容分发网络的边缘节点
- 文件下载服务:软件、文档等资源的下载服务
- 图片服务器:专门存储和提供图片内容的服务器
- 企业内部文档系统:内部知识库、文档管理系统
市场知名案例:
- Nginx:业界最流行的静态文件服务器之一
- Apache HTTP Server:经典的企业级静态文件服务器
- GitHub Pages:基于静态文件服务的网站托管平台
- Netlify:现代化的静态网站部署平台
6.5 多页面静态站点联调示例
将 public/ 目录作为站点根(内含 index.html、learn.html 及 dist/css、dist/js),用下列服务脚本即可在浏览器访问完整前端页面(Bootstrap 样式 + 多 Tab 导航)。
javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const MIME = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon'
};
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') return res.end();
const pathname = url.parse(req.url).pathname;
const filePath = path.join(__dirname, 'public', pathname === '/' ? 'index.html' : pathname);
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end('<h1>404</h1>');
}
const ext = path.extname(filePath);
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(data);
});
});
server.listen(8080, () => console.log('http://127.0.0.1:8080'));
【代码注释】
- 将
public/作为 Web 根目录;访问/index.html、/learn.html时 HTML 内<link href="dist/css/...">会再发 GET 请求 CSS/JS。 - 每个资源请求都走同一
createServer回调,靠extname映射 MIME;这就是 Vite dev server /express.static('public')的底层模型。 - 联调时打开 DevTools → Network:确认
.css状态 200 且 Type 为 stylesheet,而不是text/html(404 误返回 HTML 时常见)。 - 多页面站点无需 SPA 路由时,原生静态服务即可支撑 Bootstrap 课堂页面。
6.6 静态文件服务的安全加固:路径穿越防护
路径穿越(Path Traversal) 是静态服务器最常见的安全漏洞:攻击者将
/../../../etc/passwd放入 URL,绕出public/目录读取服务器任意文件。
漏洞演示与修复:
javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const PUBLIC_DIR = path.resolve(__dirname, 'public');
const MIME = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.pdf': 'application/pdf',
'.mp4': 'video/mp4',
};
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') return res.end();
const pathname = url.parse(req.url).pathname;
// ✅ 关键安全步骤:将 pathname 解析为绝对路径后检查是否在 public/ 内
// path.join 会规范化 '../' 等跳转,resolve 转为绝对路径后才能比较
const filePath = path.resolve(PUBLIC_DIR, '.' + pathname);
// 防路径穿越:确保解析后的路径以 public/ 开头
if (!filePath.startsWith(PUBLIC_DIR + path.sep) && filePath !== PUBLIC_DIR) {
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
return res.end('403 Forbidden');
}
// 对根路径重定向到 index.html
const targetPath = pathname === '/' ? path.join(PUBLIC_DIR, 'index.html') : filePath;
fs.stat(targetPath, (err, stats) => {
if (err || !stats.isFile()) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end('<h1>404 Not Found</h1>');
}
const ext = path.extname(targetPath).toLowerCase();
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
fs.createReadStream(targetPath).pipe(res);
});
});
server.listen(8080, () => console.log('安全静态服务器运行在 http://localhost:8080'));
【代码注释】
- 漏洞根源 :
path.join(__dirname, 'public', req.url)中如果req.url是/../secret.txt,join会规范化为__dirname/secret.txt,已经跑出public/目录。 - 修复关键 :用
path.resolve取绝对路径后,用startsWith(PUBLIC_DIR + sep)校验------必须加path.sep后缀,否则public2/目录会被误判为合法。 fs.stat检查stats.isFile():防止请求目录路径时readStream尝试读取目录导致EISDIR错误。- 生产环境可进一步用
decodeURIComponent(pathname)再校验,防 URL 编码绕过(%2F..%2F..)。 express.static内部已做此类防护,理解原理后使用框架更安全放心。
攻击场景模拟(仅供理解,禁止实际攻击):
# 攻击者构造的恶意请求 URL(路径穿越)
GET /../../../etc/passwd HTTP/1.1
# path.join 规范化后等价于访问
/etc/passwd ← 已跑出 public/ 目录!
# 加入校验后,filePath.startsWith(PUBLIC_DIR) 为 false → 403
7. 企业级最佳实践
7.1 模块化架构设计
javascript
// server.js - 主服务器文件 【代码注释】
const http = require('http');
const router = require('./router');
const middleware = require('./middleware');
const config = require('./config');
const server = http.createServer(async (req, res) => {
// 应用全局中间件 【代码注释】
await middleware.apply(req, res);
// 路由处理 【代码注释】
router.handle(req, res);
});
// 启动服务器 【代码注释】
server.listen(config.port, config.host, () => {
console.log(`服务器运行在 http://${config.host}:${config.port}`);
});
// middleware.js - 中间件系统 【代码注释】
const middleware = {
stack: [],
use(fn) {
this.stack.push(fn);
},
async apply(req, res) {
for (const fn of this.stack) {
await fn(req, res);
}
}
};
// 日志中间件 【代码注释】
middleware.use((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
});
// CORS中间件 【代码注释】
middleware.use((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
});
// router.js - 路由系统 【代码注释】
const router = {
routes: {},
register(method, path, handler) {
const key = `${method}:${path}`;
this.routes[key] = handler;
},
handle(req, res) {
const key = `${req.method}:${req.url}`;
const handler = this.routes[key];
if (handler) {
handler(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
}
}
};
// config.js - 配置管理 【代码注释】
module.exports = {
port: process.env.PORT || 8080,
host: process.env.HOST || 'localhost',
env: process.env.NODE_ENV || 'development'
};
module.exports = { middleware, router };
【代码注释】
middleware.stack+apply:按顺序执行日志、CORS 等,对应 Express 的app.use(fn)中间件链(原生需自己await串联)。router.register('GET', '/path', handler)用${method}:${path}作键;注意示例里req.url含查询串时键应对pathname解析后再匹配(课堂完整版在 §5.2)。config从process.env读端口/环境,与dotenv、Docker 环境变量注入一致。- 拆分为
server.js/router.js/middleware.js便于单测与分工,是 Express 项目目录结构的简化版。
7.2 错误处理机制
javascript
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
// 全局错误处理中间件 【代码注释】
function errorHandler(err, req, res, next) {
console.error('错误详情:', err);
// 操作性错误 【代码注释】
if (err.isOperational) {
res.writeHead(err.statusCode, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
error: err.message,
statusCode: err.statusCode
}));
}
// 编程错误 【代码注释】
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: '服务器内部错误',
statusCode: 500
}));
}
// 使用示例 【代码注释】
const server = http.createServer(async (req, res) => {
try {
// 业务逻辑 【代码注释】
if (req.url === '/error') {
throw new AppError('这是一个人为触发的错误', 400);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: '成功' }));
} catch (error) {
errorHandler(error, req, res);
}
});
【代码注释】
- 操作性错误 (
AppError、isOperational):如参数校验失败、未登录,返回明确 4xx JSON,消息可对用户展示。 - 编程错误 :空指针、未捕获异常,对外统一 500 与笼统文案,详细堆栈只写服务端日志。
- 原生
http无 Express 的next(err),需在路由里try/catch并调用统一errorHandler。 - 切勿
res.end(err.stack)给浏览器,存在信息泄露风险。
7.3 日志系统
javascript
const fs = require('fs');
const path = require('path');
// 日志级别 【代码注释】
const LogLevel = {
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR',
DEBUG: 'DEBUG'
};
// 日志记录器 【代码注释】
class Logger {
constructor(logDir = './logs') {
this.logDir = logDir;
this.ensureLogDir();
}
ensureLogDir() {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
// 控制台输出 【代码注释】
console.log(logMessage.trim());
// 文件输出 【代码注释】
const logFile = path.join(this.logDir, `${new Date().toISOString().split('T')[0]}.log`);
fs.appendFile(logFile, logMessage, (err) => {
if (err) console.error('日志写入失败:', err);
});
}
info(message) {
this.log(LogLevel.INFO, message);
}
warn(message) {
this.log(LogLevel.WARN, message);
}
error(message) {
this.log(LogLevel.ERROR, message);
}
debug(message) {
this.log(LogLevel.DEBUG, message);
}
}
// HTTP请求日志中间件 【代码注释】
function requestLogger(logger) {
return (req, res) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info(`${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
});
};
}
module.exports = { Logger, requestLogger };
7.4 安全性最佳实践
javascript
const http = require('http');
const crypto = require('crypto');
// 安全头设置 【代码注释】
function setSecurityHeaders(res) {
// 防止XSS攻击 【代码注释】
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
// CSP策略 【代码注释】
res.setHeader('Content-Security-Policy', "default-src 'self'");
// HTTPS强制 【代码注释】
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
// 请求体大小限制 【代码注释】
function limitRequestBodySize(req, res, maxSize = 10 * 1024 * 1024) {
let size = 0;
return new Promise((resolve, reject) => {
req.on('data', (chunk) => {
size += chunk.length;
if (size > maxSize) {
req.destroy();
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '请求体过大' }));
reject(new Error('请求体过大'));
}
});
req.on('end', resolve);
});
}
// IP白名单 【代码注释】
const ipWhitelist = new Set(['127.0.0.1', 'localhost']);
function checkIPWhitelist(req, res) {
const clientIP = req.socket.remoteAddress;
if (!ipWhitelist.has(clientIP)) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '访问被拒绝' }));
return false;
}
return true;
}
// 请求频率限制 【代码注释】
class RateLimiter {
constructor(maxRequests = 100, windowMs = 60000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = new Map();
}
check(clientIP) {
const now = Date.now();
const windowStart = now - this.windowMs;
if (!this.requests.has(clientIP)) {
this.requests.set(clientIP, []);
}
const requests = this.requests.get(clientIP);
// 清理过期请求 【代码注释】
const validRequests = requests.filter(time => time > windowStart);
if (validRequests.length >= this.maxRequests) {
return false;
}
validRequests.push(now);
this.requests.set(clientIP, validRequests);
return true;
}
}
const rateLimiter = new RateLimiter();
// 使用示例 【代码注释】
const server = http.createServer(async (req, res) => {
const clientIP = req.socket.remoteAddress;
// 检查IP白名单 【代码注释】
if (!checkIPWhitelist(req, res)) {
return;
}
// 检查请求频率 【代码注释】
if (!rateLimiter.check(clientIP)) {
res.writeHead(429, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '请求过于频繁' }));
return;
}
// 设置安全头 【代码注释】
setSecurityHeaders(res);
// 限制请求体大小 【代码注释】
if (req.method === 'POST') {
try {
await limitRequestBodySize(req, res);
} catch (error) {
return;
}
}
// 业务逻辑处理 【代码注释】
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: '请求处理成功' }));
});
server.listen(8080, () => {
console.log('安全的HTTP服务器运行在 http://localhost:8080');
});
【代码注释】
- IP 白名单:内网管理接口常用;公网 API 慎用,移动用户 IP 会变。
- 频率限制 :用
Map记时间窗口内请求次数,超限返回 429;生产可用 Redis + 网关限流。 - 请求体大小 :累加
data长度,超过阈值直接 413,防大包拖垮内存(与 Day07 流式读 body 结合)。 - 安全头 :
X-Content-Type-Options、CSP、Strict-Transport-Security降低 XSS/嗅探风险;常与 Helmet 中间件对应。 - 原生示例用于理解原理;线上多由 Nginx、WAF、Express 中间件组合实现。
8. 性能优化策略
8.1 连接复用与Keep-Alive
javascript
const http = require('http');
// 服务器配置优化 【代码注释】
const server = http.createServer({
// 启用Keep-Alive 【代码注释】
keepAlive: true,
// Keep-Alive超时时间 【代码注释】
keepAliveTimeout: 5000,
// 请求头超时 【代码注释】
headersTimeout: 60000,
// 最大请求数 【代码注释】
maxRequestsPerSocket: 100,
}, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!');
});
server.listen(8080, () => {
console.log('高性能HTTP服务器运行在 http://localhost:8080');
});
8.2 集群模式
javascript
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
console.log(`CPU核心数: ${numCPUs}`);
// 衍生工作进程 【代码注释】
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 监听工作进程退出 【代码注释】
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
console.log('正在重启新的工作进程...');
cluster.fork();
});
} else {
// 工作进程创建HTTP服务器 【代码注释】
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Hello from worker ${process.pid}\n`);
});
server.listen(8080, () => {
console.log(`工作进程 ${process.pid} 已启动`);
});
}
8.3 压缩传输
javascript
const http = require('http');
const zlib = require('zlib');
const fs = require('fs');
const server = http.createServer((req, res) => {
const filePath = './large-file.txt';
// 检查客户端是否支持压缩 【代码注释】
const acceptEncoding = req.headers['accept-encoding'];
if (!acceptEncoding) {
// 不支持压缩 【代码注释】
fs.createReadStream(filePath).pipe(res);
return;
}
// 创建压缩流 【代码注释】
let compress;
if (acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
compress = zlib.createGzip();
} else if (acceptEncoding.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate');
compress = zlib.createDeflate();
} else {
// 不支持的压缩格式 【代码注释】
fs.createReadStream(filePath).pipe(res);
return;
}
// 压缩并传输 【代码注释】
fs.createReadStream(filePath)
.pipe(compress)
.pipe(res);
});
server.listen(8080, () => {
console.log('支持压缩的HTTP服务器运行在 http://localhost:8080');
});
【代码注释】
- Keep-Alive :同一 TCP 连接上复用多个 HTTP 请求,减少握手开销;
keepAliveTimeout控制空闲断开时间。 cluster模块 :主进程 fork 工作进程,各进程listen同一端口(由系统分发连接),利用多核 CPU。- gzip/deflate :读
Accept-Encoding,createReadStream.pipe(gzip).pipe(res)并设Content-Encoding,显著减小文本类响应体积。 - 与 §6.3 的 ETag/304 组合:静态资源「先协商缓存、再压缩传输」是 CDN 节点常见策略。
8.4 Server-Sent Events(SSE)--- 服务端主动推送
SSE 是基于普通 HTTP 的单向实时推送 协议:服务端保持连接打开,持续写入数据;浏览器通过 EventSource API 监听。无需 WebSocket,天然支持断线重连,适合实时通知、进度推送、日志流等场景。
Node HTTP 服务 浏览器 EventSource Node HTTP 服务 浏览器 EventSource #mermaid-svg-o2p6Ujjt4HrbHsl5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .error-icon{fill:#552222;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .marker.cross{stroke:#333333;}#mermaid-svg-o2p6Ujjt4HrbHsl5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o2p6Ujjt4HrbHsl5 p{margin:0;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o2p6Ujjt4HrbHsl5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-o2p6Ujjt4HrbHsl5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-o2p6Ujjt4HrbHsl5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .sequenceNumber{fill:white;}#mermaid-svg-o2p6Ujjt4HrbHsl5 #sequencenumber{fill:#333;}#mermaid-svg-o2p6Ujjt4HrbHsl5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .messageText{fill:#333;stroke:none;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .labelText,#mermaid-svg-o2p6Ujjt4HrbHsl5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .loopText,#mermaid-svg-o2p6Ujjt4HrbHsl5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-o2p6Ujjt4HrbHsl5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .noteText,#mermaid-svg-o2p6Ujjt4HrbHsl5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .actorPopupMenu{position:absolute;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-o2p6Ujjt4HrbHsl5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o2p6Ujjt4HrbHsl5 .actor-man circle,#mermaid-svg-o2p6Ujjt4HrbHsl5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-o2p6Ujjt4HrbHsl5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 触发 onmessage 事件 loop 事件循环 GET /events (Accept: text/event-stream) HTTP 200 + Connection: keep-alive data: {"type":"update","msg":"..."}\n\n 连接断开(自动重连)
服务端实现:
javascript
const http = require('http');
// 存储所有活跃的 SSE 客户端连接
const clients = new Set();
const server = http.createServer((req, res) => {
// SSE 连接端点
if (req.url === '/events') {
// SSE 协议必须的响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream', // 固定 MIME,浏览器识别为 SSE
'Cache-Control': 'no-cache', // 禁止中间代理缓存
'Connection': 'keep-alive', // 保持长连接
'Access-Control-Allow-Origin': '*', // 允许跨域(按需限制)
});
// 立即发送注释行,冲刷缓冲区(某些代理会缓冲直到收到足够数据)
res.write(': ping\n\n');
// 注册到客户端集合
clients.add(res);
console.log(`新客户端连接,当前在线: ${clients.size}`);
// 客户端断开时清理
req.on('close', () => {
clients.delete(res);
console.log(`客户端断开,当前在线: ${clients.size}`);
});
return; // 保持连接,不调用 res.end()
}
// 演示页面
if (req.url === '/' || req.url === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SSE 实时推送演示</title>
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 40px auto; }
#log { border: 1px solid #ddd; padding: 15px; min-height: 200px; border-radius: 6px; background: #f9f9f9; }
.event { padding: 6px; margin: 4px 0; border-left: 4px solid #007bff; background: #fff; border-radius: 3px; }
.event.alert { border-color: #dc3545; }
</style>
</head>
<body>
<h1>SSE 实时推送</h1>
<p>连接状态:<strong id="status">连接中...</strong></p>
<div id="log"></div>
<script>
const es = new EventSource('/events');
const log = document.getElementById('log');
es.onopen = () => {
document.getElementById('status').textContent = '已连接';
};
// 监听默认 message 事件
es.onmessage = (e) => {
const data = JSON.parse(e.data);
const div = document.createElement('div');
div.className = 'event' + (data.type === 'alert' ? ' alert' : '');
div.textContent = \`[\${data.time}] \${data.msg}\`;
log.prepend(div);
};
// 监听自定义命名事件 "notification"
es.addEventListener('notification', (e) => {
const div = document.createElement('div');
div.className = 'event alert';
div.textContent = '通知: ' + e.data;
log.prepend(div);
});
es.onerror = () => {
document.getElementById('status').textContent = '断线,自动重连...';
};
</script>
</body>
</html>
`);
}
res.writeHead(404);
res.end('Not Found');
});
// 向所有在线客户端广播消息
function broadcast(data) {
// SSE 数据格式:每条消息以 "data: " 开头,两个换行符 \n\n 结尾
const message = `data: ${JSON.stringify(data)}\n\n`;
for (const client of clients) {
client.write(message);
}
}
// 发送命名事件(前端用 addEventListener('notification', ...) 监听)
function broadcastNamed(eventName, data) {
const message = `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of clients) {
client.write(message);
}
}
// 模拟服务端定时推送(每 2 秒广播一次实时数据)
setInterval(() => {
broadcast({
type: 'update',
msg: `服务器时间: ${new Date().toLocaleTimeString()},在线用户: ${clients.size}`,
time: new Date().toLocaleTimeString(),
});
}, 2000);
// 模拟每 10 秒发一条命名通知
setInterval(() => {
broadcastNamed('notification', `系统公告: ${new Date().toLocaleTimeString()} 无异常`);
}, 10000);
server.listen(8080, () => console.log('SSE 服务运行在 http://localhost:8080'));
【代码注释】
- SSE 数据格式 :每条消息形如
data: 内容\n\n(两个\n结尾是协议规定,少一个就不触发onmessage);命名事件加event: 名称\n前缀,前端用addEventListener监听。 - 连接不调用
res.end():SSE 依赖长连接,响应不结束;客户端断开后触发req.on('close')清理clients集合,否则内存泄漏。 Cache-Control: no-cache:代理服务器(Nginx、CDN)不能缓存 SSE 响应;Nginx 还需设置proxy_buffering off才能实时透传数据。- 对比 WebSocket:SSE 是单向(服务端→客户端)、基于 HTTP/1.1、自动重连;WebSocket 是双向、需协议升级。实时通知/日志流用 SSE,聊天室/游戏用 WebSocket。
- 断线重连 :浏览器
EventSource断线后自动重连 ,可通过id: 消息ID\n字段让服务端从断点续发(Last-Event-ID请求头)。 - 实用场景:订单状态实时更新、后台任务进度条、实时监控仪表盘、AI 流式输出(ChatGPT 流式效果底层即此)。
9. 总结与展望
9.1 核心知识点总结
基础概念:
- HTTP模块是Node.js核心模块,提供创建HTTP服务器和客户端的功能
- 基于事件驱动、非阻塞I/O模型,适合高并发场景
- 四大核心对象:http对象、Server对象、ClientRequest对象、ServerResponse对象
服务器创建:
- 使用
http.createServer()方法创建服务器 - 通过
listen()方法启动服务器监听端口 - 支持多种配置选项和错误处理机制
请求处理:
- 请求行信息:
req.method、req.url、req.httpVersion - 请求头信息:
req.headers - 请求体处理:通过
data和end事件流式接收 - URL解析:支持传统
url.parse()和现代URL类
响应设置:
- 响应行:
statusCode、statusMessage - 响应头:
setHeader()、writeHead() - 响应体:
write()、end() - 支持多种内容类型和状态码
路由处理:
- 基于路径的路由分发
- 基于请求方式的路由控制
- RESTful API设计模式
- 企业级路由器实现
静态文件服务:
- 基础文件读取服务
- 流式传输优化
- 缓存控制策略
- Content-Type映射
企业级应用:
- 模块化架构设计
- 错误处理机制
- 日志监控系统
- 安全性最佳实践
性能优化:
- 连接复用与Keep-Alive
- 集群模式多进程
- 压缩传输
- 缓存策略
9.2 学习路线建议
初级阶段:
- 掌握HTTP模块基础API
- 理解请求响应流程
- 实现简单的静态文件服务器
- 基础路由处理
中级阶段:
- 深入理解流式处理
- 实现RESTful API
- 掌握中间件模式
- 错误处理和日志
高级阶段:
- 性能优化技术
- 安全加固方案
- 微服务架构
- 容器化部署
9.3 未来发展趋势
技术演进方向:
- HTTP/3和QUIC协议支持
- 更高效的事件循环机制
- 增强的安全特性
- 更好的TypeScript支持
应用场景扩展:
- 服务端渲染(SSR)
- 边缘计算节点
- 微服务架构
- Serverless应用
生态系统发展:
- 更成熟的框架支持
- 更丰富的中间件生态
- 更强大的开发工具
- 更完善的监控方案
9.4 实践项目建议
初学者项目:
- 个人博客系统
- 文件共享平台
- 在线聊天室
- 简单的API服务
进阶项目:
- 电商平台后端
- 实时数据分析系统
- 微服务架构应用
- 内容管理系统
高级项目:
- 分布式文件系统
- 实时协作平台
- 高可用API网关
- 智能监控平台
10. 核心案例速查与知识点归纳
10.1 案例索引(按学习顺序)
| 序号 | 目标 | 关键代码点 | 默认端口 |
|---|---|---|---|
| ① | 创建服务 | createServer + listen(8080) |
8080 |
| ② | 请求行/头 | method、url、headers、table 美化 |
8080 |
| ③ | 查询字符串 | url.parse(url, true).query 或 URL + searchParams |
8080 |
| ④ | 请求体 | req.on('data') / end + qs.parse |
8080 |
| ⑤ | 响应报文 | statusCode、writeHead、write、end |
8080 |
| ⑥ | 路径路由 | switch(pathname) 首页/登录/注册/404 |
8080 |
| ⑦ | 方法+路径 | GET /login 读 HTML,POST /login 校验 admin/123456 |
8080 |
| ⑧ | 静态资源 | public/ + Content-Type 映射 |
8080 |
| ⑨ | HTTP 客户端 | http.get / http.request + Promise 封装 |
--- |
| ⑩ | BFF 聚合 | Promise.all 并发出站请求 + async/await |
8080 |
| ⑪ | 路径穿越防护 | path.resolve + startsWith(PUBLIC_DIR) |
8080 |
| ⑫ | SSE 推送 | text/event-stream + 长连接 + EventSource |
8080 |
10.2 请求对象速查
| 需求 | API | 说明 |
|---|---|---|
| 请求方法 | req.method |
GET / POST / PUT / DELETE |
| 路径+查询串 | req.url |
不含协议与主机,如 /search?wd=node |
| 协议版本 | req.httpVersion |
常见 1.1 |
| 全部请求头 | req.headers |
键名小写 |
| 客户端 IP | req.socket.remoteAddress |
可能带 ::ffff: 前缀 |
| 请求体 | req.on('data') → end |
GET/DELETE 通常无 body |
10.3 响应对象速查
| 需求 | API | 注意 |
|---|---|---|
| 状态码 | res.statusCode |
200/201/301/404/500 |
| 状态描述 | res.statusMessage |
可自定义,生产慎用随意文案 |
| 单个响应头 | res.setHeader(k, v) |
需在 end 前 |
| 批量设置 | res.writeHead(code, msg, headers) |
发送后不宜再改头 |
| 分块写入 | res.write(chunk) |
可多次调用 |
| 结束 | res.end(data?) |
调用后不可再 write |
10.4 路由设计对照
#mermaid-svg-4WoCmiLalN8m1KJt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4WoCmiLalN8m1KJt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4WoCmiLalN8m1KJt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4WoCmiLalN8m1KJt .error-icon{fill:#552222;}#mermaid-svg-4WoCmiLalN8m1KJt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4WoCmiLalN8m1KJt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4WoCmiLalN8m1KJt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4WoCmiLalN8m1KJt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4WoCmiLalN8m1KJt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4WoCmiLalN8m1KJt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4WoCmiLalN8m1KJt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4WoCmiLalN8m1KJt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4WoCmiLalN8m1KJt .marker.cross{stroke:#333333;}#mermaid-svg-4WoCmiLalN8m1KJt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4WoCmiLalN8m1KJt p{margin:0;}#mermaid-svg-4WoCmiLalN8m1KJt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4WoCmiLalN8m1KJt .cluster-label text{fill:#333;}#mermaid-svg-4WoCmiLalN8m1KJt .cluster-label span{color:#333;}#mermaid-svg-4WoCmiLalN8m1KJt .cluster-label span p{background-color:transparent;}#mermaid-svg-4WoCmiLalN8m1KJt .label text,#mermaid-svg-4WoCmiLalN8m1KJt span{fill:#333;color:#333;}#mermaid-svg-4WoCmiLalN8m1KJt .node rect,#mermaid-svg-4WoCmiLalN8m1KJt .node circle,#mermaid-svg-4WoCmiLalN8m1KJt .node ellipse,#mermaid-svg-4WoCmiLalN8m1KJt .node polygon,#mermaid-svg-4WoCmiLalN8m1KJt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4WoCmiLalN8m1KJt .rough-node .label text,#mermaid-svg-4WoCmiLalN8m1KJt .node .label text,#mermaid-svg-4WoCmiLalN8m1KJt .image-shape .label,#mermaid-svg-4WoCmiLalN8m1KJt .icon-shape .label{text-anchor:middle;}#mermaid-svg-4WoCmiLalN8m1KJt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4WoCmiLalN8m1KJt .rough-node .label,#mermaid-svg-4WoCmiLalN8m1KJt .node .label,#mermaid-svg-4WoCmiLalN8m1KJt .image-shape .label,#mermaid-svg-4WoCmiLalN8m1KJt .icon-shape .label{text-align:center;}#mermaid-svg-4WoCmiLalN8m1KJt .node.clickable{cursor:pointer;}#mermaid-svg-4WoCmiLalN8m1KJt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4WoCmiLalN8m1KJt .arrowheadPath{fill:#333333;}#mermaid-svg-4WoCmiLalN8m1KJt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4WoCmiLalN8m1KJt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4WoCmiLalN8m1KJt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4WoCmiLalN8m1KJt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4WoCmiLalN8m1KJt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4WoCmiLalN8m1KJt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4WoCmiLalN8m1KJt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4WoCmiLalN8m1KJt .cluster text{fill:#333;}#mermaid-svg-4WoCmiLalN8m1KJt .cluster span{color:#333;}#mermaid-svg-4WoCmiLalN8m1KJt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4WoCmiLalN8m1KJt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4WoCmiLalN8m1KJt rect.text{fill:none;stroke-width:0;}#mermaid-svg-4WoCmiLalN8m1KJt .icon-shape,#mermaid-svg-4WoCmiLalN8m1KJt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4WoCmiLalN8m1KJt .icon-shape p,#mermaid-svg-4WoCmiLalN8m1KJt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4WoCmiLalN8m1KJt .icon-shape .label rect,#mermaid-svg-4WoCmiLalN8m1KJt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4WoCmiLalN8m1KJt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4WoCmiLalN8m1KJt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4WoCmiLalN8m1KJt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /login
POST /login
GET /
其他
收到请求
解析 pathname
method?
返回登录页 HTML
读 body → qs.parse → 校验
首页
404
【代码注释】
- 路由 =
pathname+method二维判断:GET /login展示表单,POST /login读 body 校验,URL 相同但语义不同。 - REST 习惯:
GET查询、POST创建、PUT全量更新、DELETE删除;资源用路径表达,如/users/1。 - Express 的
app.get('/login', ...)/app.post('/login', ...)即对本图的封装;中间件在匹配前先执行。 - 未匹配路由应返回 404,且勿与 500 混淆。
10.5 最小登录联调(HTML + 服务端)
login.html (字段名 pwd,与 application/x-www-form-urlencoded 一致):
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form method="post" action="http://127.0.0.1:8080/login">
<label>用户 <input name="username" required></label>
<label>密码 <input name="pwd" type="password" required></label>
<button type="submit">登录</button>
</form>
<p>测试账号:admin / 123456</p>
</body>
</html>
【代码注释】
action="http://127.0.0.1:8080/login":提交后浏览器向该 URL 发 POST ,请求体为username=...&pwd=...(application/x-www-form-urlencoded)。- 服务端在
POST+pathname === '/login'分支里req.on('end')→qs.parse→ 比对admin/123456(课堂账号)。 - 字段名
pwd须与 HTMLname="pwd"一致;若写password则服务端取不到密码。 - 登录成功/失败均
writeHead(200)返回 HTML 提示页;生产环境成功常 302 重定向 到首页并 Set-Cookie。
10.6 常见错误与对策
| 现象 | 原因 | 处理 |
|---|---|---|
EADDRINUSE |
端口占用 | 换端口或 lsof -i :8080 结束进程 |
| 页面空白 / 样式丢失 | Content-Type 错误 |
.css 必须为 text/css |
| POST 后乱码 | 未设 charset=utf-8 |
响应头加 text/html;charset=utf-8 |
reqBody 为空 |
在 data 前就读取 |
只在 end 里处理完整 body |
write after end |
end 后又 write |
保证只 end 一次 |
静态服务读到 /etc/passwd |
路径穿越漏洞 | path.resolve 后 startsWith(PUBLIC_DIR) 校验 |
| SSE 页面无数据 | 代理缓冲或缺 \n\n |
Nginx 加 proxy_buffering off;消息末尾必须双换行 |
出站 http.get 非 2xx 不报错 |
error 事件只捕获网络错误 |
手动检查 res.statusCode !== 200 |
Content-Length 与 body 不符 |
用字符数而非字节数 | 改用 Buffer.byteLength(str) |
10.7 与框架的关系(归纳)
原生 http 掌握 「一次请求 → 一次回调 → 自己拼响应」 后,学习 Express 时只需关注:路由表、中间件链、模板引擎 ------底层仍是 IncomingMessage 与 ServerResponse。
资源链接:
注意事项:
- 所有示例代码都经过测试,可直接运行
- 代码注释详细,便于学习和理解
- 涵盖了从基础到高级的完整知识体系
- 包含企业级应用的实战经验
- 提供了性能优化和安全性考虑
持续学习:
Node.js和HTTP技术在不断发展,建议持续关注官方文档更新和社区最佳实践,不断学习和提升自己的技能水平。
最后更新时间:2026年5月22日
本文档基于Node.js官方文档和企业级开发实践整理而成,旨在帮助开发者全面掌握Node.js HTTP模块的使用方法和最佳实践。