Node.js HTTP 模块深度解析与实战指南

一篇面向实战的后端入门博客:从 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.createServerserver.listen 本地可访问的 Web 入口
读请求 req.methodreq.urlreq.headers、流式 data/end 打日志、解析表单、REST 路由
写响应 statusCodesetHeaderwriteHeadwrite/end 返回 HTML/JSON/文件流
路由 pathname + method 多页面、登录 POST、404
静态站 fs + path + Content-Type 托管前端构建产物
HTTP 客户端 http.gethttp.requesthttps 调用外部 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'
服务器实例
请求对象
响应对象

对象详解:

  1. http对象 :通过require('http')导入的内置模块,提供创建服务器和客户端的方法
  2. http.Server对象http.createServer()方法的返回值,代表一个HTTP服务器实例
  3. http.ClientRequest对象:请求对象,包含客户端发送的请求信息
  4. 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 installcreateServer 返回 Server 实例,每个 HTTP 请求触发一次回调。
  • 回调参数:reqIncomingMessage,只读请求)、resServerResponse,写响应);必须对每个 请求调用 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 服务器启动注意事项

常见问题处理:

  1. 端口占用问题
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
  1. 代码热重载
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)timeoutkeepAliveTimeout 控制连接与 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=2query.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 与浏览器 URLSearchParams API 一致,便于前后端共用同一套参数名。
  • 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 无协议与主机,必须补全基址才能构造 WHATWG URL 对象。
  • 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/jsonJSON.parse
  • Content-Type: application/x-www-form-urlencoded 时字段名为 nameemail 等,与 HTML <input name="..."> 一一对应。
  • GET/HEAD/DELETE 通常没有 请求体,不要对 GET 注册 data 监听还期待有数据。
  • 写盘前 mkdirSync(..., { recursive: true });响应用 end 返回 HTML,成功/失败分支都要 setHeaderend,且每个请求只 end 一次。
  • 大表单或文件上传应改用 multipart/form-data + 专用解析(或 Express multer),本示例适合课堂「联系人表单」场景。

配套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&... 格式,服务端在 endqs.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-fetchaxiosgot 等库替代手写 Promise;但理解底层有助于调试连接超时、重定向、证书等问题。
  • https.gethttp.get API 完全一样,切换只需换模块名------底层多了 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 服务端错。
  • 仅改 statusCodewriteHead 时,仍须在 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 联调)。
  • 示例中先 setHeaderwriteHead 仅为演示,实际项目选一种方式即可,避免重复或覆盖。

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(整个字符串) 一次写完小页面;静态小文件 readFileend(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/infoContent-Type: application/json + JSON.stringify,是 REST API 最简形态;Access-Control-Allow-Origin: * 便于浏览器跨域读 JSON(生产应限制域名)。
  • /downloadContent-Disposition: attachment 触发下载;Content-Length 与 body 字节数一致可避免客户端截断。
  • /redirect301 + 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 必须与扩展名匹配.csstext/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 静态文件服务器的经典应用场景

实际应用案例:

  1. Web应用部署:React、Vue等前端框架构建后的文件服务
  2. CDN服务器:内容分发网络的边缘节点
  3. 文件下载服务:软件、文档等资源的下载服务
  4. 图片服务器:专门存储和提供图片内容的服务器
  5. 企业内部文档系统:内部知识库、文档管理系统

市场知名案例:

  • Nginx:业界最流行的静态文件服务器之一
  • Apache HTTP Server:经典的企业级静态文件服务器
  • GitHub Pages:基于静态文件服务的网站托管平台
  • Netlify:现代化的静态网站部署平台

6.5 多页面静态站点联调示例

public/ 目录作为站点根(内含 index.htmllearn.htmldist/cssdist/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.txtjoin 会规范化为 __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)。
  • configprocess.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);
    }
});

【代码注释】

  • 操作性错误AppErrorisOperational):如参数校验失败、未登录,返回明确 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-OptionsCSPStrict-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-EncodingcreateReadStream.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.methodreq.urlreq.httpVersion
  • 请求头信息:req.headers
  • 请求体处理:通过dataend事件流式接收
  • URL解析:支持传统url.parse()和现代URL

响应设置:

  • 响应行:statusCodestatusMessage
  • 响应头:setHeader()writeHead()
  • 响应体:write()end()
  • 支持多种内容类型和状态码

路由处理:

  • 基于路径的路由分发
  • 基于请求方式的路由控制
  • RESTful API设计模式
  • 企业级路由器实现

静态文件服务:

  • 基础文件读取服务
  • 流式传输优化
  • 缓存控制策略
  • Content-Type映射

企业级应用:

  • 模块化架构设计
  • 错误处理机制
  • 日志监控系统
  • 安全性最佳实践

性能优化:

  • 连接复用与Keep-Alive
  • 集群模式多进程
  • 压缩传输
  • 缓存策略

9.2 学习路线建议

初级阶段:

  1. 掌握HTTP模块基础API
  2. 理解请求响应流程
  3. 实现简单的静态文件服务器
  4. 基础路由处理

中级阶段:

  1. 深入理解流式处理
  2. 实现RESTful API
  3. 掌握中间件模式
  4. 错误处理和日志

高级阶段:

  1. 性能优化技术
  2. 安全加固方案
  3. 微服务架构
  4. 容器化部署

9.3 未来发展趋势

技术演进方向:

  • HTTP/3和QUIC协议支持
  • 更高效的事件循环机制
  • 增强的安全特性
  • 更好的TypeScript支持

应用场景扩展:

  • 服务端渲染(SSR)
  • 边缘计算节点
  • 微服务架构
  • Serverless应用

生态系统发展:

  • 更成熟的框架支持
  • 更丰富的中间件生态
  • 更强大的开发工具
  • 更完善的监控方案

9.4 实践项目建议

初学者项目:

  1. 个人博客系统
  2. 文件共享平台
  3. 在线聊天室
  4. 简单的API服务

进阶项目:

  1. 电商平台后端
  2. 实时数据分析系统
  3. 微服务架构应用
  4. 内容管理系统

高级项目:

  1. 分布式文件系统
  2. 实时协作平台
  3. 高可用API网关
  4. 智能监控平台

10. 核心案例速查与知识点归纳

10.1 案例索引(按学习顺序)

序号 目标 关键代码点 默认端口
创建服务 createServer + listen(8080) 8080
请求行/头 methodurlheaderstable 美化 8080
查询字符串 url.parse(url, true).queryURL + searchParams 8080
请求体 req.on('data') / end + qs.parse 8080
响应报文 statusCodewriteHeadwriteend 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 须与 HTML name="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.resolvestartsWith(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 时只需关注:路由表、中间件链、模板引擎 ------底层仍是 IncomingMessageServerResponse


资源链接:


注意事项:

  1. 所有示例代码都经过测试,可直接运行
  2. 代码注释详细,便于学习和理解
  3. 涵盖了从基础到高级的完整知识体系
  4. 包含企业级应用的实战经验
  5. 提供了性能优化和安全性考虑

持续学习:

Node.js和HTTP技术在不断发展,建议持续关注官方文档更新和社区最佳实践,不断学习和提升自己的技能水平。


最后更新时间:2026年5月22日

本文档基于Node.js官方文档和企业级开发实践整理而成,旨在帮助开发者全面掌握Node.js HTTP模块的使用方法和最佳实践。

相关推荐
带娃的IT创业者1 小时前
深度解析 Bun:重新定义 JavaScript 运行时的性能边界
开发语言·javascript·node.js·ecmascript·bun·运行时
潜创微科技2 小时前
ITE IT920X 4K60 HDMI+USB over IP 远距离传输与视频墙单芯片方案
网络协议·tcp/ip·音视频
Zhan8611242 小时前
深夜调试法国行情数据API接口的教训:法国CAC40指数WebSocket接入复盘
websocket·网络协议·php
带土12 小时前
12. UDP协议概述
网络·网络协议·udp
AIFQuant2 小时前
全球行情自动更新、多品种展示、性能优化实战指南
python·性能优化·金融·node.js·restful
kebidaixu3 小时前
Modbus TCP 协议详解
网络·网络协议·tcp/ip
VidDown3 小时前
VidDown 工具站:免费视频处理与开发者工具箱
网络协议·编辑器·音视频·视频编解码·视频
jike88ai3 小时前
Claude Code完整安装+API配置教程(Windows系统)
windows·gpt·node.js·claude·api中转·claude code·88api
ZengLiangYi17 小时前
测试策略:单元测试 + 集成测试怎么写
javascript·typescript·node.js