Skip to content

HTTP 高级进阶

HTTP 所有版本

HTTP/0.9

HTTP/0.9 是 HTTP 协议的最初版本,于 1991 年发布。

特点:

  • 极其简单,只有 GET 方法
  • 没有请求头和响应头
  • 只能传输 HTML 格式文本
  • 服务器响应后立即关闭连接

请求示例:

GET /index.html

响应示例:

html
<html>
  <body>
    Hello World
  </body>
</html>

局限性:

  • 无法传输其他类型文件(如图片、视频)
  • 没有状态码,无法判断请求是否成功
  • 没有压缩机制,传输效率低
  • 每次请求都需要重新建立 TCP 连接

HTTP/1.0

HTTP/1.0 于 1996 年发布,引入了许多重要特性。

新增特性:

  • 增加了 POST、HEAD 方法
  • 引入请求头和响应头
  • 支持 Content-Type 传输多种数据类型
  • 引入状态码
  • 支持 Cache 缓存机制

请求示例:

http
GET /index.html HTTP/1.0
User-Agent: Mozilla/5.0
Accept: text/html

响应示例:

http
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 1234

<html>...</html>

局限性:

  • 默认使用短连接,每次请求都需建立新 TCP 连接
  • 连接建立开销大(三次握手 + 慢启动)
  • 队头阻塞问题:必须等待前一个请求完成
  • 无法复用连接,性能较差

Connection: keep-alive: 虽然 HTTP/1.0 默认短连接,但可以通过请求头开启长连接:

http
Connection: keep-alive

HTTP/1.1

HTTP/1.1 于 1997 年发布,是目前使用最广泛的版本。

核心改进:

1. 持久连接(Persistent Connection)

  • 默认开启 Connection: keep-alive
  • 一个 TCP 连接可发送多个请求
  • 减少连接建立开销

2. 管道化(Pipelining)

  • 允许同时发送多个请求
  • 但响应必须按顺序返回
  • 仍存在队头阻塞问题

3. 新增方法

  • PUT:更新资源
  • DELETE:删除资源
  • OPTIONS:查询支持的方法
  • PATCH:部分更新

4. Host 头字段

  • 支持虚拟主机
  • 同一 IP 可托管多个域名

5. 分块传输编码

http
Transfer-Encoding: chunked

5\r\n
Hello\r\n
6\r\n
 World\r\n
0\r\n
\r\n

6. 断点续传

http
Range: bytes=0-1023

请求示例:

http
GET /api/users HTTP/1.1
Host: example.com
Connection: keep-alive
Accept: application/json
User-Agent: Mozilla/5.0

队头阻塞问题:

请求1 ──────────────────> 响应1
      请求2 ──────────────────> 响应2
             请求3 ──────────────────> 响应3

即使请求2和请求3已发送,也必须等待响应1返回后才能返回响应2。


HTTP/2

HTTP/2 于 2015 年发布,带来革命性改进。

核心特性:

1. 二进制分帧

  • 将数据拆分为更小的帧
  • 帧类型:HEADERS、DATA、SETTINGS、PING 等
  • 更高效解析,更少错误
+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-------------------------------+
|R|         Stream Identifier (31)              |
+-------------------------------+
|                 Frame Payload (0...)        ...
+-----------------------------------------------+

2. 多路复用(Multiplexing)

  • 单个 TCP 连接并行多个请求/响应
  • 解决队头阻塞问题
  • Stream(流)概念:一个连接包含多个流
连接
├── Stream 1 (请求A)
│   ├── Frame 1 (HEADERS)
│   └── Frame 2 (DATA)
├── Stream 3 (请求B)
│   └── Frame 1 (HEADERS)
└── Stream 5 (请求C)
    └── Frame 1 (HEADERS)

3. 头部压缩(HPACK)

  • 使用 Huffman 编码压缩
  • 维护头部表,避免重复传输
  • 压缩率可达 85% 以上

静态表示例:

IndexNameValue
2:methodGET
4:path/
6:schemehttp

4. 服务器推送

  • 服务器主动推送资源
  • 减少请求延迟
javascript
// 服务器推送示例
<link rel="stylesheet" href="/style.css">
// 服务器主动推送 style.css,无需客户端再次请求

5. 请求优先级

  • 设置流的依赖关系和权重
  • 关键资源优先加载
http
Priority: u=1, i  // 紧急优先级
Priority: u=4     // 普通优先级

6. 流量控制

  • 基于流的流量控制
  • 防止发送方淹没接收方

性能对比:

特性HTTP/1.1HTTP/2
连接复用有限(6个)完全复用
队头阻塞存在应用层解决
头部压缩HPACK
二进制协议
服务器推送不支持支持

HTTP/3

HTTP/3 于 2022 年正式发布,基于 QUIC 协议。

核心改进:

1. 基于 QUIC 协议

  • 使用 UDP 替代 TCP
  • 避免内核层 TCP 队头阻塞
  • 更快的连接建立

连接建立对比:

TCP + TLS 1.3:
客户端 ──SYN──> 服务器
客户端 <──SYN+ACK── 服务器
客户端 ──ACK──> 服务器
客户端 ──ClientHello──> 服务器
客户端 <──ServerHello+Certificate── 服务器
...(2-3 RTT)

QUIC:
客户端 ──Initial+Handshake──> 服务器
客户端 <──Handshake+1-RTT Data── 服务器
(0-1 RTT)

2. 解决队头阻塞

  • TCP 层队头阻塞彻底解决
  • 单个丢包不影响其他流
HTTP/2 over TCP:
Stream 1: [Frame 1] [Frame 2] [Frame 3(丢失)] [等待...]
Stream 2: [Frame 1] [等待...] [等待...] [等待...]

HTTP/3 over QUIC:
Stream 1: [Frame 1] [Frame 2] [Frame 3(丢失)] [重传]
Stream 2: [Frame 1] [Frame 2] [Frame 3] [继续]

3. 连接迁移

  • 使用 Connection ID 标识连接
  • 网络切换不断开连接
  • WiFi 切换 4G 无缝衔接
javascript
// 场景:用户从 WiFi 切换到 4G
// HTTP/1.1 & HTTP/2:连接断开,需要重新建立
// HTTP/3:连接保持,继续传输

4. 快速迭代

  • QUIC 在用户态实现
  • 不依赖内核更新
  • 协议升级更容易

5. 内置 TLS 1.3

  • 加密是强制的
  • 更安全的传输

协议栈对比:

HTTP/1.1 & HTTP/2:
┌─────────────┐
│    HTTP     │
├─────────────┤
│    TLS      │
├─────────────┤
│    TCP      │
├─────────────┤
│     IP      │
└─────────────┘

HTTP/3:
┌─────────────┐
│    HTTP     │
├─────────────┤
│    QUIC     │
│   (含TLS)   │
├─────────────┤
│    UDP      │
├─────────────┤
│     IP      │
└─────────────┘

性能对比:

特性HTTP/2HTTP/3
传输层TCPUDP
队头阻塞TCP 层存在完全解决
连接建立2-3 RTT0-1 RTT
连接迁移不支持支持
加密可选强制
丢包恢复

浏览器支持:

javascript
// 检测 HTTP/3 支持
const supportsHTTP3 =
  'connection' in navigator && navigator.connection.effectiveType === '4g'

性能优化具体方案

连接优化

1. 连接复用

HTTP/1.1 Keep-Alive:

nginx
# Nginx 配置
keepalive_timeout 65;
keepalive_requests 100;

连接池配置:

javascript
// Node.js HTTP Agent
const http = require('http')

const agent = new http.Agent({
  keepAlive: true,
  maxSockets: 50,
  maxFreeSockets: 10,
  timeout: 30000,
  freeSocketTimeout: 15000
})

const req = http.request({
  hostname: 'example.com',
  agent: agent
})

浏览器连接限制:

浏览器HTTP/1.1 每域名连接数
Chrome6
Firefox6
Safari6
Edge6

域名分片(Domain Sharding):

html
<!-- 突破浏览器连接限制 -->
<link rel="stylesheet" href="https://static1.example.com/style.css" />
<link rel="stylesheet" href="https://static2.example.com/theme.css" />
<script src="https://static3.example.com/app.js"></script>

2. HTTP/2 多路复用优化

启用 HTTP/2:

nginx
# Nginx 配置
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # HTTP/2 推送
    http2_push /style.css;
    http2_push /app.js;
}

最佳实践:

javascript
// 合并请求 vs 多路复用
// HTTP/1.1:合并资源减少请求数
// HTTP/2:保持资源独立,利用缓存

// ❌ HTTP/2 不推荐
<script src="bundle.js"></script> // 包含所有代码

// ✅ HTTP/2 推荐
<script src="core.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>

3. HTTP/3 升级策略

Alt-Svc 头:

nginx
# 告知客户端支持 HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';

渐进式升级:

javascript
// 客户端自动选择最优协议
fetch('https://example.com/api/data').then(response => {
  console.log('Protocol:', response.headers.get('protocol'))
})

缓存优化

1. 强缓存策略

Cache-Control 配置:

nginx
# 静态资源(长期缓存)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# HTML 文件(不缓存或短期缓存)
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}

# API 接口
location /api/ {
    add_header Cache-Control "no-cache, private";
}

Cache-Control 指令详解:

指令说明
public可被任何缓存存储
private只能被浏览器缓存
no-cache使用前需验证
no-store不缓存任何内容
max-age=秒缓存有效期
immutable资源永不变化
must-revalidate过期后必须验证

版本化资源:

html
<!-- 使用内容哈希作为版本号 -->
<link rel="stylesheet" href="/style.a1b2c3d4.css" />
<script src="/app.e5f6g7h8.js"></script>

<!-- 配合长期缓存 -->
<!-- Cache-Control: max-age=31536000, immutable -->

2. 协商缓存策略

ETag 配置:

nginx
# 启用 ETag
etag on;

# 精确的文件修改时间
if_modified_since exact;

Last-Modified vs ETag:

http
// 首次请求
GET /style.css HTTP/1.1

// 首次响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
ETag: "abc123"

// 再次请求(Last-Modified)
GET /style.css HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

// 再次请求(ETag)
GET /style.css HTTP/1.1
If-None-Match: "abc123"

// 未修改响应
HTTP/1.1 304 Not Modified

协商缓存流程:

客户端请求


检查强缓存 ──有效──> 直接使用缓存

   无效


发送协商缓存头 ──资源未变──> 304 Not Modified

   已变


返回新资源 + 200 OK

3. Service Worker 缓存

注册 Service Worker:

javascript
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js')
      console.log('SW registered:', registration.scope)
    } catch (error) {
      console.error('SW registration failed:', error)
    }
  })
}

缓存策略实现:

javascript
// sw.js
const CACHE_NAME = 'v1'
const STATIC_ASSETS = ['/', '/style.css', '/app.js', '/offline.html']

// 安装阶段:预缓存静态资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then(cache => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting())
  )
})

// 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches
      .keys()
      .then(keys =>
        Promise.all(
          keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
        )
      )
      .then(() => self.clients.claim())
  )
})

// 请求拦截:缓存优先策略
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached

      return fetch(event.request)
        .then(response => {
          // 缓存新请求
          if (response.status === 200) {
            const responseClone = response.clone()
            caches
              .open(CACHE_NAME)
              .then(cache => cache.put(event.request, responseClone))
          }
          return response
        })
        .catch(() => caches.match('/offline.html'))
    })
  )
})

多种缓存策略:

javascript
// 1. Cache First(缓存优先)
async function cacheFirst(request) {
  const cached = await caches.match(request)
  if (cached) return cached
  return fetch(request)
}

// 2. Network First(网络优先)
async function networkFirst(request) {
  try {
    const response = await fetch(request)
    const cache = await caches.open(CACHE_NAME)
    cache.put(request, response.clone())
    return response
  } catch {
    return caches.match(request)
  }
}

// 3. Stale While Revalidate(后台更新)
async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME)
  const cached = await cache.match(request)

  const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone())
    return response
  })

  return cached || fetchPromise
}

// 4. Network Only(仅网络)
async function networkOnly(request) {
  return fetch(request)
}

// 5. Cache Only(仅缓存)
async function cacheOnly(request) {
  return caches.match(request)
}

压缩优化

1. Gzip/Brotli 压缩

Nginx 配置:

nginx
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types text/plain text/css text/xml application/json application/javascript
           application/xml application/xml+rss text/javascript application/x-javascript;

# Brotli 压缩(优先)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css text/xml application/json application/javascript
             application/xml application/xml+rss text/javascript;

# 根据客户端支持选择压缩方式
map $http_accept_encoding $compress_type {
    default gzip;
    ~br br;
    ~gzip gzip;
}

压缩效果对比:

文件类型原始大小GzipBrotli
HTML100KB25KB20KB
CSS100KB20KB16KB
JavaScript100KB30KB24KB
JSON100KB15KB12KB

压缩级别选择:

nginx
# CPU vs 压缩率权衡
gzip_comp_level 6;    # 推荐:平衡 CPU 和压缩率
brotli_comp_level 6;  # 推荐:Brotli 压缩率更高

# 不同级别对比
# Level 1: 快速压缩,压缩率低
# Level 6: 平衡选择
# Level 9: 慢速压缩,压缩率高

2. 代码压缩

JavaScript 压缩:

javascript
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log']
          },
          format: {
            comments: false
          }
        }
      })
    ]
  }
}

CSS 压缩:

javascript
// webpack.config.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
        minimizerOptions: {
          preset: [
            'default',
            {
              discardComments: { removeAll: true },
              normalizeWhitespace: false
            }
          ]
        }
      })
    ]
  }
}

HTML 压缩:

javascript
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true
      }
    }
  }
})

3. 图片优化

WebP 格式:

nginx
# Nginx 自动选择 WebP
map $http_accept $webp_suffix {
    default   "";
    "~*webp"  ".webp";
}

server {
    location ~* ^/images/(.+)\.(png|jpg|jpeg)$ {
        add_header Vary Accept;
        try_files /images/$1$webp_suffix $uri =404;
    }
}

响应式图片:

html
<picture>
  <source
    type="image/webp"
    srcset="
      image-small.webp   400w,
      image-medium.webp  800w,
      image-large.webp  1200w
    "
    sizes="(max-width: 600px) 400px,
           (max-width: 1000px) 800px,
           1200px"
  />
  <img
    src="image-fallback.jpg"
    srcset="image-small.jpg 400w, image-medium.jpg 800w, image-large.jpg 1200w"
    sizes="(max-width: 600px) 400px,
           (max-width: 1000px) 800px,
           1200px"
    alt="Responsive image"
    loading="lazy"
  />
</picture>

图片懒加载:

html
<!-- 原生懒加载 -->
<img src="image.jpg" loading="lazy" alt="Lazy loaded image" />

<!-- Intersection Observer API -->
<script>
  const images = document.querySelectorAll('img[data-src]')

  const imageObserver = new IntersectionObserver(
    (entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target
          img.src = img.dataset.src
          img.removeAttribute('data-src')
          observer.unobserve(img)
        }
      })
    },
    {
      rootMargin: '50px 0px',
      threshold: 0.01
    }
  )

  images.forEach(img => imageObserver.observe(img))
</script>

请求优化

1. 减少 HTTP 请求

内联关键 CSS:

html
<head>
  <style>
    /* 关键 CSS 内联 */
    .header {
      background: #fff;
    }
    .hero {
      min-height: 100vh;
    }
  </style>
  <link
    rel="preload"
    href="/style.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'"
  />
</head>

SVG 内联:

html
<!-- 内联 SVG -->
<svg width="24" height="24" viewBox="0 0 24 24">
  <path d="M12 2L2 7l10 5 10-5-10-5z" />
</svg>

<!-- 或使用 SVG Sprite -->
<svg class="icon">
  <use xlink:href="/sprite.svg#icon-name"></use>
</svg>

2. 资源预加载

Preload(预加载):

html
<!-- 预加载关键资源 -->
<link rel="preload" href="/critical.css" as="style" />
<link rel="preload" href="/app.js" as="script" />
<link
  rel="preload"
  href="/font.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>
<link rel="preload" href="/hero.webp" as="image" />

Prefetch(预获取):

html
<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/next-page.js" as="script" />
<link rel="prefetch" href="/next-page.html" as="document" />

Preconnect(预连接):

html
<!-- 提前建立连接 -->
<link rel="preconnect" href="https://cdn.example.com" />
<link rel="preconnect" href="https://api.example.com" crossorigin />

DNS-Prefetch(DNS 预解析):

html
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com" />

Modulepreload(模块预加载):

html
<!-- ES Module 预加载 -->
<link rel="modulepreload" href="/utils.js" />
<link rel="modulepreload" href="/components.js" />
<script type="module" src="/app.js"></script>

3. 资源优先级

Fetch Priority API:

html
<!-- 高优先级 -->
<img src="hero.jpg" fetchpriority="high" />

<!-- 低优先级 -->
<img src="below-fold.jpg" fetchpriority="low" />

<!-- 脚本优先级 -->
<script src="critical.js" fetchpriority="high"></script>
<script src="analytics.js" fetchpriority="low"></script>

优先级对比:

html
<!-- 默认优先级 -->
<link rel="stylesheet" href="/style.css" />
<!-- 高 -->
<script src="/app.js"></script>
<!-- 高 -->
<img src="/image.jpg" />
<!-- 中 -->

<!-- 调整优先级 -->
<link rel="stylesheet" href="/style.css" fetchpriority="high" />
<script src="/app.js" fetchpriority="high"></script>
<img src="/image.jpg" fetchpriority="low" />

CDN 加速

1. CDN 配置

静态资源 CDN:

javascript
// webpack.config.js
const CDN_URL = 'https://cdn.example.com'

module.exports = {
  output: {
    publicPath: process.env.NODE_ENV === 'production' ? CDN_URL + '/' : '/'
  }
}

多 CDN 域名:

html
<!-- 分散请求,突破浏览器连接限制 -->
<link rel="stylesheet" href="https://cdn1.example.com/style.css" />
<script src="https://cdn2.example.com/app.js"></script>
<img src="https://cdn3.example.com/image.jpg" />

2. 缓存策略

CDN 缓存规则:

nginx
# CDN 边缘节点缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header CDN-Cache-Control "max-age=31536000";
}

# 动态内容短缓存
location /api/ {
    add_header Cache-Control "public, max-age=60";
    add_header CDN-Cache-Control "max-age=60";
}

缓存清除:

javascript
// 版本化 URL
const version = 'v1.2.3'
const assetUrl = `https://cdn.example.com/app.${version}.js`

// 或使用缓存标签
// CDN-Cache-Tag: app,v1.2.3
// 通过 API 清除特定标签的缓存

高级使用场景案例

场景一:大文件分片上传

前端实现:

javascript
class FileUploader {
  constructor(options = {}) {
    this.chunkSize = options.chunkSize || 5 * 1024 * 1024 // 5MB
    this.concurrency = options.concurrency || 3
    this.retries = options.retries || 3
  }

  async upload(file) {
    const fileId = this.generateFileId(file)
    const chunks = this.createChunks(file)
    const uploadedChunks = await this.getUploadedChunks(fileId)

    const chunksToUpload = chunks.filter(
      (_, index) => !uploadedChunks.includes(index)
    )

    await this.uploadChunks(file, chunksToUpload, fileId)
    await this.mergeChunks(fileId, file.name, chunks.length)
  }

  createChunks(file) {
    const chunks = []
    let start = 0

    while (start < file.size) {
      const end = Math.min(start + this.chunkSize, file.size)
      chunks.push({
        start,
        end,
        index: chunks.length
      })
      start = end
    }

    return chunks
  }

  async uploadChunks(file, chunks, fileId) {
    const queue = new ConcurrentQueue(this.concurrency)

    const tasks = chunks.map(chunk => async () => {
      const blob = file.slice(chunk.start, chunk.end)

      for (let attempt = 0; attempt < this.retries; attempt++) {
        try {
          await this.uploadChunk(blob, fileId, chunk.index, chunks.length)
          return
        } catch (error) {
          if (attempt === this.retries - 1) throw error
          await this.delay(Math.pow(2, attempt) * 1000)
        }
      }
    })

    await queue.addAll(tasks)
  }

  async uploadChunk(blob, fileId, chunkIndex, totalChunks) {
    const formData = new FormData()
    formData.append('file', blob)
    formData.append('fileId', fileId)
    formData.append('chunkIndex', chunkIndex)
    formData.append('totalChunks', totalChunks)

    const response = await fetch('/api/upload/chunk', {
      method: 'POST',
      body: formData
    })

    if (!response.ok) {
      throw new Error(`Upload failed: ${response.status}`)
    }

    return response.json()
  }

  async getUploadedChunks(fileId) {
    const response = await fetch(`/api/upload/chunks?fileId=${fileId}`)
    const data = await response.json()
    return data.chunks || []
  }

  async mergeChunks(fileId, fileName, totalChunks) {
    const response = await fetch('/api/upload/merge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ fileId, fileName, totalChunks })
    })

    return response.json()
  }

  generateFileId(file) {
    return `${file.name}-${file.size}-${file.lastModified}`
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

// 并发队列
class ConcurrentQueue {
  constructor(concurrency) {
    this.concurrency = concurrency
    this.running = 0
    this.queue = []
  }

  async add(task) {
    if (this.running >= this.concurrency) {
      await new Promise(resolve => this.queue.push(resolve))
    }

    this.running++
    try {
      await task()
    } finally {
      this.running--
      const next = this.queue.shift()
      if (next) next()
    }
  }

  async addAll(tasks) {
    return Promise.all(tasks.map(task => this.add(task)))
  }
}

后端实现:

javascript
const express = require('express')
const multer = require('multer')
const fs = require('fs').promises
const path = require('path')

const app = express()
const upload = multer({ dest: 'temp/' })

const UPLOAD_DIR = 'uploads'
const CHUNK_DIR = 'chunks'

app.post('/api/upload/chunk', upload.single('file'), async (req, res) => {
  const { fileId, chunkIndex, totalChunks } = req.body
  const chunkDir = path.join(CHUNK_DIR, fileId)

  await fs.mkdir(chunkDir, { recursive: true })
  await fs.rename(req.file.path, path.join(chunkDir, `${chunkIndex}`))

  res.json({ success: true, chunkIndex })
})

app.get('/api/upload/chunks', async (req, res) => {
  const { fileId } = req.query
  const chunkDir = path.join(CHUNK_DIR, fileId)

  try {
    const files = await fs.readdir(chunkDir)
    const chunks = files.map(f => parseInt(f))
    res.json({ chunks })
  } catch {
    res.json({ chunks: [] })
  }
})

app.post('/api/upload/merge', async (req, res) => {
  const { fileId, fileName, totalChunks } = req.body
  const chunkDir = path.join(CHUNK_DIR, fileId)
  const outputPath = path.join(UPLOAD_DIR, fileName)

  await fs.mkdir(UPLOAD_DIR, { recursive: true })

  const writeStream = fs.createWriteStream(outputPath)

  for (let i = 0; i < totalChunks; i++) {
    const chunkPath = path.join(chunkDir, `${i}`)
    const data = await fs.readFile(chunkPath)
    writeStream.write(data)
    await fs.unlink(chunkPath)
  }

  writeStream.end()
  await fs.rmdir(chunkDir)

  res.json({ success: true, path: outputPath })
})

场景二:实时数据推送

Server-Sent Events (SSE):

服务端:

javascript
const express = require('express')
const app = express()

app.get('/api/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')
  res.setHeader('Connection', 'keep-alive')
  res.setHeader('X-Accel-Buffering', 'no')

  const sendEvent = data => {
    res.write(`data: ${JSON.stringify(data)}\n\n`)
  }

  const interval = setInterval(() => {
    sendEvent({
      timestamp: Date.now(),
      message: 'Heartbeat'
    })
  }, 30000)

  const eventEmitter = getEventEmitter()

  eventEmitter.on('update', data => {
    sendEvent(data)
  })

  req.on('close', () => {
    clearInterval(interval)
    eventEmitter.removeAllListeners('update')
    res.end()
  })
})

// 发送自定义事件
app.post('/api/broadcast', express.json(), (req, res) => {
  const { event, data } = req.body

  getEventEmitter().emit('update', {
    event,
    data,
    timestamp: Date.now()
  })

  res.json({ success: true })
})

客户端:

javascript
class SSEClient {
  constructor(url, options = {}) {
    this.url = url
    this.reconnectInterval = options.reconnectInterval || 1000
    this.maxReconnectInterval = options.maxReconnectInterval || 30000
    this.eventSource = null
    this.reconnectAttempts = 0
    this.listeners = new Map()
  }

  connect() {
    this.eventSource = new EventSource(this.url)

    this.eventSource.onopen = () => {
      console.log('SSE connected')
      this.reconnectAttempts = 0
      this.reconnectInterval = 1000
    }

    this.eventSource.onmessage = event => {
      const data = JSON.parse(event.data)
      this.emit('message', data)
    }

    this.eventSource.onerror = error => {
      console.error('SSE error:', error)
      this.eventSource.close()
      this.reconnect()
    }
  }

  reconnect() {
    this.reconnectAttempts++
    const delay = Math.min(
      this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
      this.maxReconnectInterval
    )

    console.log(`Reconnecting in ${delay}ms...`)

    setTimeout(() => {
      this.connect()
    }, delay)
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set())
    }
    this.listeners.get(event).add(callback)
  }

  off(event, callback) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).delete(callback)
    }
  }

  emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach(callback => callback(data))
    }
  }

  close() {
    if (this.eventSource) {
      this.eventSource.close()
      this.eventSource = null
    }
  }
}

// 使用示例
const sse = new SSEClient('/api/events')

sse.on('message', data => {
  console.log('Received:', data)
})

sse.connect()

WebSocket 实时通信:

服务端:

javascript
const WebSocket = require('ws')
const http = require('http')

const server = http.createServer()
const wss = new WebSocket.Server({ server })

const rooms = new Map()

wss.on('connection', (ws, req) => {
  const url = new URL(req.url, `http://${req.headers.host}`)
  const roomId = url.searchParams.get('room') || 'default'

  ws.roomId = roomId
  ws.userId = generateUserId()

  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set())
  }
  rooms.get(roomId).add(ws)

  ws.on('message', data => {
    const message = JSON.parse(data)

    switch (message.type) {
      case 'chat':
        broadcast(
          roomId,
          {
            type: 'chat',
            userId: ws.userId,
            content: message.content,
            timestamp: Date.now()
          },
          ws
        )
        break

      case 'typing':
        broadcast(
          roomId,
          {
            type: 'typing',
            userId: ws.userId
          },
          ws
        )
        break

      case 'ping':
        ws.send(JSON.stringify({ type: 'pong' }))
        break
    }
  })

  ws.on('close', () => {
    rooms.get(roomId).delete(ws)
    if (rooms.get(roomId).size === 0) {
      rooms.delete(roomId)
    }
    broadcast(roomId, {
      type: 'leave',
      userId: ws.userId
    })
  })

  ws.send(
    JSON.stringify({
      type: 'welcome',
      userId: ws.userId,
      users: Array.from(rooms.get(roomId)).map(w => w.userId)
    })
  )

  broadcast(
    roomId,
    {
      type: 'join',
      userId: ws.userId
    },
    ws
  )
})

function broadcast(roomId, message, excludeWs = null) {
  const room = rooms.get(roomId)
  if (!room) return

  const data = JSON.stringify(message)
  room.forEach(ws => {
    if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) {
      ws.send(data)
    }
  })
}

function generateUserId() {
  return Math.random().toString(36).substring(2, 15)
}

server.listen(8080)

客户端:

javascript
class WebSocketClient {
  constructor(url, options = {}) {
    this.url = url
    this.reconnectInterval = options.reconnectInterval || 1000
    this.maxReconnectAttempts = options.maxReconnectAttempts || 10
    this.heartbeatInterval = options.heartbeatInterval || 30000

    this.ws = null
    this.reconnectAttempts = 0
    this.heartbeatTimer = null
    this.listeners = new Map()
    this.messageQueue = []
  }

  connect() {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(this.url)

      this.ws.onopen = () => {
        console.log('WebSocket connected')
        this.reconnectAttempts = 0
        this.startHeartbeat()
        this.flushMessageQueue()
        resolve()
      }

      this.ws.onmessage = event => {
        const data = JSON.parse(event.data)
        this.emit(data.type, data)
      }

      this.ws.onerror = error => {
        console.error('WebSocket error:', error)
        reject(error)
      }

      this.ws.onclose = () => {
        console.log('WebSocket closed')
        this.stopHeartbeat()
        this.reconnect()
      }
    })
  }

  reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnect attempts reached')
      this.emit('error', { message: 'Connection lost' })
      return
    }

    this.reconnectAttempts++
    const delay =
      this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1)

    console.log(`Reconnecting in ${delay}ms...`)

    setTimeout(() => {
      this.connect().catch(() => {})
    }, delay)
  }

  send(type, data) {
    const message = JSON.stringify({ type, ...data })

    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(message)
    } else {
      this.messageQueue.push(message)
    }
  }

  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift()
      this.ws.send(message)
    }
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.send('ping')
    }, this.heartbeatInterval)
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set())
    }
    this.listeners.get(event).add(callback)
  }

  off(event, callback) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).delete(callback)
    }
  }

  emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach(callback => callback(data))
    }
  }

  close() {
    this.stopHeartbeat()
    if (this.ws) {
      this.ws.close()
      this.ws = null
    }
  }
}

// 使用示例
const ws = new WebSocketClient('ws://localhost:8080?room=chat1')

ws.on('welcome', data => {
  console.log('Welcome!', data)
})

ws.on('chat', data => {
  console.log(`${data.userId}: ${data.content}`)
})

ws.on('join', data => {
  console.log(`${data.userId} joined`)
})

await ws.connect()

ws.send('chat', { content: 'Hello!' })

场景三:请求重试与熔断

请求重试机制:

javascript
class RetryableFetch {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3
    this.retryDelay = options.retryDelay || 1000
    this.retryMultiplier = options.retryMultiplier || 2
    this.retryableStatusCodes = options.retryableStatusCodes || [
      408, 429, 500, 502, 503, 504
    ]
  }

  async fetch(url, options = {}) {
    let lastError
    let delay = this.retryDelay

    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        const response = await fetch(url, {
          ...options,
          signal: this.createTimeoutSignal(options.timeout)
        })

        if (response.ok) {
          return response
        }

        if (!this.retryableStatusCodes.includes(response.status)) {
          return response
        }

        const retryAfter = response.headers.get('Retry-After')
        if (retryAfter) {
          delay = parseInt(retryAfter) * 1000
        }

        lastError = new Error(`HTTP ${response.status}`)
      } catch (error) {
        lastError = error
      }

      if (attempt < this.maxRetries) {
        console.log(
          `Retry attempt ${attempt + 1}/${this.maxRetries} after ${delay}ms`
        )
        await this.sleep(delay)
        delay *= this.retryMultiplier
      }
    }

    throw lastError
  }

  createTimeoutSignal(timeout = 30000) {
    const controller = new AbortController()
    setTimeout(() => controller.abort(), timeout)
    return controller.signal
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

// 使用示例
const retryFetch = new RetryableFetch({
  maxRetries: 3,
  retryDelay: 1000,
  retryMultiplier: 2
})

try {
  const response = await retryFetch.fetch('/api/data', {
    method: 'GET',
    timeout: 10000
  })
  const data = await response.json()
} catch (error) {
  console.error('All retries failed:', error)
}

熔断器模式:

javascript
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5
    this.successThreshold = options.successThreshold || 3
    this.timeout = options.timeout || 60000

    this.state = 'CLOSED'
    this.failureCount = 0
    this.successCount = 0
    this.lastFailureTime = null
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      if (this.shouldAttemptReset()) {
        this.state = 'HALF_OPEN'
      } else {
        throw new Error('Circuit breaker is OPEN')
      }
    }

    try {
      const result = await fn()
      this.onSuccess()
      return result
    } catch (error) {
      this.onFailure()
      throw error
    }
  }

  onSuccess() {
    this.failureCount = 0

    if (this.state === 'HALF_OPEN') {
      this.successCount++
      if (this.successCount >= this.successThreshold) {
        this.state = 'CLOSED'
        this.successCount = 0
      }
    }
  }

  onFailure() {
    this.failureCount++
    this.lastFailureTime = Date.now()

    if (this.state === 'HALF_OPEN') {
      this.state = 'OPEN'
      this.successCount = 0
    } else if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN'
    }
  }

  shouldAttemptReset() {
    return Date.now() - this.lastFailureTime >= this.timeout
  }

  getState() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      successCount: this.successCount,
      lastFailureTime: this.lastFailureTime
    }
  }
}

// 使用示例
const circuitBreaker = new CircuitBreaker({
  failureThreshold: 5,
  successThreshold: 3,
  timeout: 60000
})

async function fetchWithCircuitBreaker(url) {
  return circuitBreaker.execute(async () => {
    const response = await fetch(url)
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
    return response.json()
  })
}

// 监控熔断器状态
setInterval(() => {
  console.log('Circuit Breaker State:', circuitBreaker.getState())
}, 5000)

完整的请求管理器:

javascript
class RequestManager {
  constructor(options = {}) {
    this.retryOptions = options.retry || {}
    this.circuitBreakerOptions = options.circuitBreaker || {}
    this.cache = new Map()
    this.pendingRequests = new Map()

    this.circuitBreakers = new Map()
    this.retryableFetch = new RetryableFetch(this.retryOptions)
  }

  async request(url, options = {}) {
    const cacheKey = this.getCacheKey(url, options)

    if (options.cache && this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey)
      if (Date.now() < cached.expiry) {
        return cached.data
      }
      this.cache.delete(cacheKey)
    }

    if (this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey)
    }

    const circuitBreaker = this.getCircuitBreaker(url)

    const promise = circuitBreaker.execute(async () => {
      const response = await this.retryableFetch.fetch(url, options)
      const data = await response.json()

      if (options.cache) {
        this.cache.set(cacheKey, {
          data,
          expiry: Date.now() + (options.cacheTTL || 60000)
        })
      }

      return data
    })

    this.pendingRequests.set(cacheKey, promise)

    try {
      const result = await promise
      return result
    } finally {
      this.pendingRequests.delete(cacheKey)
    }
  }

  getCircuitBreaker(url) {
    const host = new URL(url).host

    if (!this.circuitBreakers.has(host)) {
      this.circuitBreakers.set(
        host,
        new CircuitBreaker(this.circuitBreakerOptions)
      )
    }

    return this.circuitBreakers.get(host)
  }

  getCacheKey(url, options) {
    return `${options.method || 'GET'}:${url}:${JSON.stringify(options.body || '')}`
  }

  clearCache() {
    this.cache.clear()
  }

  getStats() {
    const stats = {
      cacheSize: this.cache.size,
      pendingRequests: this.pendingRequests.size,
      circuitBreakers: {}
    }

    this.circuitBreakers.forEach((cb, host) => {
      stats.circuitBreakers[host] = cb.getState()
    })

    return stats
  }
}

// 使用示例
const requestManager = new RequestManager({
  retry: {
    maxRetries: 3,
    retryDelay: 1000
  },
  circuitBreaker: {
    failureThreshold: 5,
    timeout: 60000
  }
})

const data = await requestManager.request('/api/users', {
  method: 'GET',
  cache: true,
  cacheTTL: 300000
})

console.log('Stats:', requestManager.getStats())

场景四:请求取消与竞态处理

AbortController 请求取消:

javascript
class CancellableRequest {
  constructor() {
    this.abortController = null
  }

  async fetch(url, options = {}) {
    this.abortController = new AbortController()

    try {
      const response = await fetch(url, {
        ...options,
        signal: this.abortController.signal
      })

      return await response.json()
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Request cancelled')
      }
      throw error
    }
  }

  cancel() {
    if (this.abortController) {
      this.abortController.abort()
      this.abortController = null
    }
  }
}

// 使用示例
const request = new CancellableRequest()

document.getElementById('search').addEventListener('input', async e => {
  request.cancel()

  try {
    const results = await request.fetch(`/api/search?q=${e.target.value}`)
    renderResults(results)
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error(error)
    }
  }
})

竞态条件处理:

javascript
class RaceConditionHandler {
  constructor() {
    this.latestRequestId = 0
    this.abortController = null
  }

  async fetch(url, options = {}) {
    const requestId = ++this.latestRequestId

    if (this.abortController) {
      this.abortController.abort()
    }

    this.abortController = new AbortController()

    try {
      const response = await fetch(url, {
        ...options,
        signal: this.abortController.signal
      })

      if (requestId !== this.latestRequestId) {
        return null
      }

      return await response.json()
    } catch (error) {
      if (error.name === 'AbortError') {
        return null
      }
      throw error
    }
  }
}

// React Hook 示例
function useSearch() {
  const [results, setResults] = React.useState([])
  const [loading, setLoading] = React.useState(false)
  const requestIdRef = React.useRef(0)
  const abortControllerRef = React.useRef(null)

  const search = React.useCallback(async query => {
    const requestId = ++requestIdRef.current

    if (abortControllerRef.current) {
      abortControllerRef.current.abort()
    }

    abortControllerRef.current = new AbortController()
    setLoading(true)

    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: abortControllerRef.current.signal
      })

      if (requestId !== requestIdRef.current) {
        return
      }

      const data = await response.json()
      setResults(data)
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error(error)
      }
    } finally {
      if (requestId === requestIdRef.current) {
        setLoading(false)
      }
    }
  }, [])

  React.useEffect(() => {
    return () => {
      if (abortControllerRef.current) {
        abortControllerRef.current.abort()
      }
    }
  }, [])

  return { results, loading, search }
}

请求去重:

javascript
class RequestDeduplicator {
  constructor() {
    this.pendingRequests = new Map()
  }

  async fetch(url, options = {}) {
    const key = this.getRequestKey(url, options)

    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key)
    }

    const promise = fetch(url, options)
      .then(response => response.json())
      .finally(() => {
        this.pendingRequests.delete(key)
      })

    this.pendingRequests.set(key, promise)

    return promise
  }

  getRequestKey(url, options) {
    return `${options.method || 'GET'}:${url}:${JSON.stringify(options.body || '')}`
  }

  clear() {
    this.pendingRequests.clear()
  }
}

// 使用示例
const deduplicator = new RequestDeduplicator()

async function getUser(id) {
  return deduplicator.fetch(`/api/users/${id}`)
}

getUser(1)
getUser(1)
getUser(1)

场景五:HTTP/2 服务器推送

Nginx 配置:

nginx
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    root /var/www/html;

    location / {
        # 推送关键资源
        http2_push /style.css;
        http2_push /app.js;
    }

    location /api/ {
        proxy_pass http://backend;
    }
}

动态推送:

nginx
# 根据请求动态推送
location / {
    http2_push_preload on;

    # 后端返回 Link 头触发推送
    # Link: </style.css>; rel=preload; as=style
    proxy_pass http://backend;
}

Node.js 实现:

javascript
const http2 = require('http2')
const fs = require('fs')

const server = http2.createSecureServer({
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
})

server.on('stream', (stream, headers) => {
  const path = headers[':path']

  if (path === '/') {
    stream.respond({
      'content-type': 'text/html',
      ':status': 200
    })
    stream.end('<html>...</html>')

    stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
      if (err) throw err

      pushStream.respond({
        'content-type': 'text/css',
        ':status': 200
      })
      pushStream.end('body { margin: 0; }')
    })

    stream.pushStream({ ':path': '/app.js' }, (err, pushStream) => {
      if (err) throw err

      pushStream.respond({
        'content-type': 'application/javascript',
        ':status': 200
      })
      pushStream.end('console.log("Hello");')
    })
  }
})

server.listen(8443)

Link 预加载头:

javascript
const express = require('express')
const app = express()

app.get('/', (req, res) => {
  res.setHeader(
    'Link',
    [
      '</style.css>; rel=preload; as=style',
      '</app.js>; rel=preload; as=script',
      '</font.woff2>; rel=preload; as=font; type=font/woff2; crossorigin'
    ].join(', ')
  )

  res.send('<html>...</html>')
})

总结

HTTP 版本演进总结

版本年份核心改进主要问题
HTTP/0.91991简单文本传输功能极其有限
HTTP/1.01996头部、状态码、多方法短连接开销大
HTTP/1.11997持久连接、管道化、Host队头阻塞
HTTP/22015多路复用、头部压缩、推送TCP 层队头阻塞
HTTP/32022QUIC、无队头阻塞、快速连接部署成本高

性能优化核心要点

连接层优化:

  • 使用 HTTP/2 或 HTTP/3
  • 启用 Keep-Alive 长连接
  • 合理使用 CDN 加速
  • 预连接关键域名

缓存策略:

  • 静态资源:长期缓存 + 内容哈希
  • 动态内容:协商缓存
  • Service Worker:离线缓存
  • CDN 边缘缓存

传输优化:

  • 启用 Gzip/Brotli 压缩
  • 代码压缩和 Tree Shaking
  • 图片优化
  • 资源预加载

请求优化:

  • 减少不必要的请求
  • 合并请求(HTTP/1.1)
  • 请求去重
  • 请求取消

最佳实践建议

1. 协议选择:

优先级:HTTP/3 > HTTP/2 > HTTP/1.1
条件:服务器支持、CDN 支持、客户端兼容

2. 缓存策略:

静态资源:Cache-Control: max-age=31536000, immutable
动态资源:Cache-Control: no-cache + ETag
敏感数据:Cache-Control: no-store

3. 压缩策略:

优先:Brotli (br)
备选:Gzip
排除:已压缩文件(图片、视频、PDF)

4. 预加载策略:

关键资源:<link rel="preload">
下一页:<link rel="prefetch">
第三方:<link rel="preconnect">

5. 错误处理:

重试:网络错误、5xx 错误
熔断:连续失败后快速失败
降级:备份数据源、缓存数据

未来趋势

1. HTTP/3 普及:

  • 浏览器支持度提升
  • CDN 服务商支持
  • 性能优势明显

2. 边缘计算:

  • 计算下沉到边缘节点
  • 降低延迟
  • 提升用户体验

3. 安全增强:

  • TLS 1.3 普及
  • 证书透明度
  • 安全头部标准化

4. 新特性探索:

  • WebTransport
  • WebCodecs
  • 更高效的传输协议

基于 VitePress 的本地知识库