HTTP协议基础
什么是HTTP?
HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。HTTP是一个基于TCP/IP通信协议来传递数据的协议。
HTTP请求方法
在文件传输中,最常用的是以下两种请求方法:
| 方法 | 描述 | 应用场景 |
|---|---|---|
| GET | 请求获取指定资源 | 下载文件、获取图片 |
| POST | 向指定资源提交数据 | 上传文件、表单提交 |
| PUT | 替换指定资源 | 完整替换文件 |
| PATCH | 部分修改指定资源 | 断点续传、分片上传 |
HTTP消息结构
小文件传输(图片、文档等)
原理:直接传输
对于小文件(通常小于几MB),HTTP可以直接将文件内容放在请求体或响应体中传输。
示例1:上传小文件(POST请求)
// 客户端发送POST请求上传文件 POST /upload HTTP/1.1 Host: example.com Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Length: 12345 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="file"; filename="image.jpg" Content-Type: image/jpeg [二进制文件数据...] ------WebKitFormBoundary7MA4YWxkTrZu0gW--
示例2:下载小文件(GET请求)
// 客户端发送GET请求下载文件 GET /download/image.jpg HTTP/1.1 Host: example.com // 服务器响应 HTTP/1.1 200 OK Content-Type: image/jpeg Content-Length: 12345 Content-Disposition: inline; filename="image.jpg" [二进制文件数据...]
关键技术:multipart/form-data
当使用HTML表单上传文件时,必须使用multipart/form-data编码类型。它会将表单数据和文件数据分成多个部分(parts),每个部分都有自己的HTTP头部。
大文件传输(视频、压缩包等)
解决方案1:分片上传(Chunked Upload)
将大文件分割成多个小块(chunks),分别上传,最后在服务器端合并。
步骤1:文件分片
客户端将文件分割成固定大小的小块(如每块5MB)。
const chunkSize = 5 * 1024 * 1024; // 5MB const chunks = []; for (let i = 0; i < file.size; i += chunkSize) { chunks.push(file.slice(i, i + chunkSize)); }
步骤2:逐个上传分片
为每个分片添加索引信息,按顺序或并发上传。
for (let i = 0; i < chunks.length; i++) { const formData = new FormData(); formData.append('chunk', chunks[i]); formData.append('index', i); formData.append('total', chunks.length); formData.append('fileId', fileId); await fetch('/upload-chunk', { method: 'POST', body: formData }); }
步骤3:服务器合并分片
所有分片上传完成后,服务器按索引顺序合并文件。
// 服务器端(Node.js示例) app.post('/merge', (req, res) => { const { fileId, totalChunks } = req.body; const writeStream = fs.createWriteStream(`./uploads/${fileId}.mp4`); for (let i = 0; i < totalChunks; i++) { const chunkPath = `./temp/${fileId}_${i}.chunk`; const chunkData = fs.readFileSync(chunkPath); writeStream.write(chunkData); fs.unlinkSync(chunkPath); // 删除临时分片 } writeStream.end(); res.json({ success: true }); });
解决方案2:断点续传(Resumable Upload)
断点续传允许在传输中断后,从中断的地方继续传输,而不需要重新传输整个文件。
HTTP Range请求
// 客户端请求文件的部分内容(从字节1000开始) GET /large-file.zip HTTP/1.1 Host: example.com Range: bytes=1000- // 服务器响应 HTTP/1.1 206 Partial Content Content-Range: bytes 1000-9999999/10000000 Content-Length: 9999000 Content-Type: application/zip [从字节1000开始的文件数据...]
上传断点续传
对于上传,客户端需要记录已上传的分片,中断后只上传剩余部分。
// 客户端检查已上传的分片 async function checkUploadedChunks(fileId) { const response = await fetch(`/check?fileId=${fileId}`); const { uploadedChunks } = await response.json(); return uploadedChunks; // 如 [0, 1, 2, 3] } // 只上传未上传的分片 const uploaded = await checkUploadedChunks(fileId); for (let i = 0; i < chunks.length; i++) { if (!uploaded.includes(i)) { await uploadChunk(chunks[i], i); } }
解决方案3:Chunked Transfer Encoding
HTTP/1.1引入了分块传输编码,允许服务器将响应数据分块传输,而不需要预先知道内容长度。
// 服务器使用分块传输 HTTP/1.1 200 OK Transfer-Encoding: chunked Content-Type: video/mp4 5\r\n Hello\r\n 6\r\n World\r\n 0\r\n \r\n
图片传输的特殊处理
图片压缩
在传输图片前,通常需要进行压缩以减少文件大小,加快传输速度。
有损压缩
- JPEG格式
- 压缩率高,文件小
- 适合照片、复杂图像
- 会损失一些画质
无损压缩
- PNG、GIF、WebP
- 压缩率较低
- 适合图标、文字图像
- 保持原始画质
图片懒加载(Lazy Loading)
只加载可视区域内的图片,当用户滚动时再加载其他图片。
<!-- HTML5原生懒加载 --> <img src="image.jpg" loading="lazy" alt="描述"> // JavaScript实现懒加载 const images = document.querySelectorAll('img[data-src]'); const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; imageObserver.unobserve(img); } }); }); images.forEach(img => imageObserver.observe(img));
图片CDN加速
使用内容分发网络(CDN)将图片缓存到离用户更近的服务器,减少传输延迟。
响应式图片
根据设备屏幕大小和网络状况,传输不同尺寸的图片。
<!-- 使用srcset提供多个尺寸 --> <img src="small.jpg" srcset="small.jpg 300w, medium.jpg 600w, large.jpg 1200w" sizes="(max-width: 600px) 300px, (max-width: 1200px) 600px, 1200px" alt="响应式图片" >
关键HTTP头部字段
| 头部字段 | 作用 | 示例 |
|---|---|---|
| Content-Type | 指示资源的MIME类型 | image/jpeg, application/pdf |
| Content-Length | 指示响应体的长度(字节) | 12345 |
| Content-Disposition | 指示内容是内联显示还是下载 | attachment; filename="file.zip" |
| Content-Range | 指示部分内容的位置 | bytes 1000-9999/10000 |
| Range | 请求部分内容 | bytes=0-999 |
| Accept-Ranges | 服务器支持的范围类型 | bytes |
| Transfer-Encoding | 传输编码方式 | chunked |
| Cache-Control | 缓存控制 | max-age=3600, public |
| ETag | 资源版本标识 | "686897696a7c876b7e" |
性能优化策略
1. 使用HTTP/2或HTTP/3
HTTP/2支持多路复用,可以在一个TCP连接上并行传输多个文件,减少延迟。
HTTP/1.1
- 每个请求需要单独的连接
- 队头阻塞问题
- 头部冗余传输
HTTP/2
- 多路复用
- 头部压缩
- 服务器推送
2. 启用Gzip/Brotli压缩
对文本文件(HTML、CSS、JS)进行压缩,减少传输大小。
# Nginx配置示例 http { gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1024; brotli on; brotli_types text/plain text/css application/json; }
3. 使用HTTP缓存
合理设置缓存策略,减少重复下载。
// 强缓存:Cache-Control HTTP/1.1 200 OK Cache-Control: max-age=31536000 // 缓存1年 // 协商缓存:ETag/Last-Modified GET /image.jpg HTTP/1.1 If-None-Match: "686897696a7c876b7e" HTTP/1.1 304 Not Modified // 服务器返回304,客户端使用缓存
4. 并行下载
将大文件分成多个部分,同时从多个服务器或连接下载。
// 使用多个Range请求并行下载 const fileSize = 10000000; // 10MB const chunkSize = fileSize / 4; // 分成4个部分 const promises = []; for (let i = 0; i < 4; i++) { const start = i * chunkSize; const end = (i + 1) * chunkSize - 1; promises.push( fetch('/large-file.zip', { headers: { 'Range': `bytes=${start}-${end}` } }).then(res => res.arrayBuffer()) ); } const chunks = await Promise.all(promises); // 合并chunks...
安全考虑
1. 使用HTTPS
HTTPS通过TLS/SSL加密HTTP通信,防止文件在传输过程中被窃听或篡改。
2. 文件类型和大小限制
在服务器端限制上传文件的类型和大小,防止恶意文件上传和DoS攻击。
// Node.js Express示例 const multer = require('multer'); const upload = multer({ limits: { fileSize: 10 * 1024 * 1024 // 限制10MB }, fileFilter: (req, file, cb) => { const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('不支持的文件类型')); } } });
3. 防止文件名冲突和路径遍历
使用UUID或哈希值重命名上传的文件,防止恶意路径遍历攻击。
const path = require('path'); const crypto = require('crypto'); function safeFilename(originalname) { const ext = path.extname(originalname); const safeName = crypto.randomBytes(16).toString('hex'); return safeName + ext; } // 不要直接使用用户提供的文件名 // 危险:../../etc/passwd // 安全:a1b2c3d4e5f6...jpg
4. 病毒扫描
对上传的文件进行病毒扫描,特别是允许用户上传可执行文件的情况。
完整示例:大文件上传与断点续传
客户端代码
class ResumableUpload { constructor(file, options = {}) { this.file = file; this.chunkSize = options.chunkSize || 5 * 1024 * 1024; // 5MB this.fileId = options.fileId || this.generateFileId(); this.uploadedChunks = []; } generateFileId() { return crypto.randomUUID(); } async checkUploadedChunks() { const response = await fetch(`/upload/check?fileId=${this.fileId}`); const data = await response.json(); this.uploadedChunks = data.uploadedChunks || []; } async upload(onProgress) { await this.checkUploadedChunks(); const totalChunks = Math.ceil(this.file.size / this.chunkSize); for (let i = 0; i < totalChunks; i++) { if (this.uploadedChunks.includes(i)) { continue; // 跳过已上传的分片 } const start = i * this.chunkSize; const end = Math.min(start + this.chunkSize, this.file.size); const chunk = this.file.slice(start, end); const formData = new FormData(); formData.append('chunk', chunk); formData.append('index', i); formData.append('total', totalChunks); formData.append('fileId', this.fileId); formData.append('filename', this.file.name); await fetch('/upload/chunk', { method: 'POST', body: formData }); this.uploadedChunks.push(i); if (onProgress) { const progress = (this.uploadedChunks.length / totalChunks) * 100; onProgress(progress); } } // 所有分片上传完成,请求合并 const response = await fetch('/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileId: this.fileId, filename: this.file.name, totalChunks: totalChunks }) }); return await response.json(); } } // 使用示例 const fileInput = document.getElementById('fileInput'); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; const uploader = new ResumableUpload(file); try { const result = await uploader.upload((progress) => { console.log(`上传进度: ${progress.toFixed(2)}%`); }); console.log('上传成功:', result); } catch (error) { console.error('上传失败:', error); } });
服务器端代码(Node.js + Express)
const express = require('express'); const multer = require('multer'); const fs = require('fs'); const path = require('path'); const app = express(); app.use(express.json()); const upload = multer({ dest: './temp/' }); // 检查已上传的分片 app.get('/upload/check', (req, res) => { const { fileId } = req.query; const uploadedChunks = []; // 检查哪些分片已经上传 for (let i = 0; i < 10000; i++) { // 假设最多10000个分片 const chunkPath = path.join('./temp', `${fileId}_${i}.chunk`); if (fs.existsSync(chunkPath)) { uploadedChunks.push(i); } else if (i > 0) { break; } } res.json({ uploadedChunks }); }); // 上传分片 app.post('/upload/chunk', upload.single('chunk'), (req, res) => { const { fileId, index } = req.body; const chunkPath = path.join('./temp', `${fileId}_${index}.chunk`); fs.renameSync(req.file.path, chunkPath); res.json({ success: true, index: index }); }); // 合并分片 app.post('/upload/merge', (req, res) => { const { fileId, filename, totalChunks } = req.body; const outputPath = path.join('./uploads', filename); const writeStream = fs.createWriteStream(outputPath); for (let i = 0; i < totalChunks; i++) { const chunkPath = path.join('./temp', `${fileId}_${i}.chunk`); const chunkData = fs.readFileSync(chunkPath); writeStream.write(chunkData); fs.unlinkSync(chunkPath); // 删除临时分片 } writeStream.end(() => { res.json({ success: true, filePath: outputPath, fileId: fileId }); }); }); app.listen(3000, () => { console.log('服务器运行在 http://localhost:3000'); });
总结
小文件传输
- 使用
multipart/form-data编码上传 - 直接在响应体中返回文件内容
- 适合图片、文档等小文件
大文件传输
- 分片上传:将文件分成多个小块分别上传
- 断点续传:使用Range和Content-Range实现
- Chunked Transfer Encoding:分块传输编码
- 需要在客户端和服务器端都实现相应逻辑
图片传输优化
- 图片压缩(有损/无损)
- 懒加载(Lazy Loading)
- CDN加速
- 响应式图片(srcset)
性能优化
- 使用HTTP/2或HTTP/3
- 启用Gzip/Brotli压缩
- 合理使用HTTP缓存
- 并行下载
安全考虑
- 使用HTTPS加密传输
- 限制文件类型和大小
- 防止文件名冲突和路径遍历
- 对上传文件进行病毒扫描