Appearance
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-aliveHTTP/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\n6. 断点续传
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% 以上
静态表示例:
| Index | Name | Value |
|---|---|---|
| 2 | :method | GET |
| 4 | :path | / |
| 6 | :scheme | http |
4. 服务器推送
- 服务器主动推送资源
- 减少请求延迟
javascript
// 服务器推送示例
<link rel="stylesheet" href="/style.css">
// 服务器主动推送 style.css,无需客户端再次请求5. 请求优先级
- 设置流的依赖关系和权重
- 关键资源优先加载
http
Priority: u=1, i // 紧急优先级
Priority: u=4 // 普通优先级6. 流量控制
- 基于流的流量控制
- 防止发送方淹没接收方
性能对比:
| 特性 | HTTP/1.1 | HTTP/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/2 | HTTP/3 |
|---|---|---|
| 传输层 | TCP | UDP |
| 队头阻塞 | TCP 层存在 | 完全解决 |
| 连接建立 | 2-3 RTT | 0-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 每域名连接数 |
|---|---|
| Chrome | 6 |
| Firefox | 6 |
| Safari | 6 |
| Edge | 6 |
域名分片(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 OK3. 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;
}压缩效果对比:
| 文件类型 | 原始大小 | Gzip | Brotli |
|---|---|---|---|
| HTML | 100KB | 25KB | 20KB |
| CSS | 100KB | 20KB | 16KB |
| JavaScript | 100KB | 30KB | 24KB |
| JSON | 100KB | 15KB | 12KB |
压缩级别选择:
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.9 | 1991 | 简单文本传输 | 功能极其有限 |
| HTTP/1.0 | 1996 | 头部、状态码、多方法 | 短连接开销大 |
| HTTP/1.1 | 1997 | 持久连接、管道化、Host | 队头阻塞 |
| HTTP/2 | 2015 | 多路复用、头部压缩、推送 | TCP 层队头阻塞 |
| HTTP/3 | 2022 | QUIC、无队头阻塞、快速连接 | 部署成本高 |
性能优化核心要点
连接层优化:
- 使用 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-store3. 压缩策略:
优先: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
- 更高效的传输协议