?HTTP 文件上傳是 Web 開發中的常見需求,涉及到特殊的請求格式和處理機制。
一、HTTP 文件上傳的核心協議
1. 兩種主要方式
multipart/form-data
(主流)
支持二進制文件和表單字段混合傳輸,由?Content-Type
?頭部標識。application/x-www-form-urlencoded
(傳統表單)
僅支持文本數據,文件會被編碼為 Base64(體積增大 33%),已逐漸淘汰。
2. 關鍵請求頭
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 12345
boundary
:分隔符,用于標記不同表單字段的邊界。Content-Length
:請求體總長度(字節)。
3.請求體
請求體包含了實際要上傳的數據。對于文件上傳,數據被分割成多個部分,每部分由兩部分組成:一部分是頭部,描述了該部分的內容(如字段名和文件名),另一部分是實際的文件內容。每個部分都以--boundary開始,并以--boundary--結束
關鍵規則:
-
字段頭部與內容之間必須有空行
空行由?\r\n\r\n
(CRLF CRLF)組成,是協議的硬性規定。 -
分隔符與字段頭部之間
分隔符后可以緊跟字段頭部(無需空行),但實際請求中可能存在一個換行符(取決于客戶端實現)。 -
結束標記
最后一個分隔符必須以?--
?結尾(如?-----------------------------1234567890--
)。
二、請求報文結構詳解
1. 基礎格式
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=---------------------------1234567890-----------------------------1234567890
Content-Disposition: form-data; name="textField"Hello, World!
-----------------------------1234567890
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plainThis is the file content.
-----------------------------1234567890--
2. 核心組成部分
- 分隔符(Boundary)
由?Content-Type
?中的?boundary
?指定,用于分隔不同字段。 - 字段頭部(Headers)
?Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain
name
:字段名(對應表單中的?name
?屬性)。filename
:文件名(可選,僅文件字段需要)。Content-Type
:文件 MIME 類型(默認?application/octet-stream
)。
- 字段內容(Body)
文件的二進制數據或文本值。
?完整示例
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
Content-Length: 12345------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"JohnDoe
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plainHello, this is the content of test.txt.
------WebKitFormBoundaryABC123--
三、服務器處理流程
1. 解析步驟
- 讀取?
Content-Type
?中的?boundary
。 - 按分隔符分割請求體。
- 解析每個字段的頭部和內容。
2. Node.js 示例(原生實現.學習使用 該案例未做安全防護,未做文件分割,大文件會導致內存溢出.)
const http = require('http');
const fs = require('fs');
const path = require('path');const server = http.createServer((req, res) => {if (req.method === 'POST') {// 獲取 boundaryconst contentType = req.headers['content-type'];const boundary = `--${contentType.split('boundary=')[1]}`;const boundaryBuffer = Buffer.from(boundary);// 存儲完整請求體(二進制形式)let requestBuffer = Buffer.from('');// 收集所有數據塊(二進制形式)req.on('data', (chunk) => {requestBuffer = Buffer.concat([requestBuffer, chunk]);});req.on('end', () => {// 按 boundary 分割(使用二進制操作)const parts = splitBuffer(requestBuffer, boundaryBuffer);parts.forEach(part => {// 分離頭部和內容(二進制形式)const headerEnd = part.indexOf('\r\n\r\n');if (headerEnd === -1) return;const headersBuffer = part.slice(0, headerEnd);const contentBuffer = part.slice(headerEnd + 4); // +4 跳過 \r\n\r\n// 解析頭部(轉換為字符串)const headers = parseHeaders(headersBuffer.toString());// 如果是文件,保存到磁盤if (headers.filename) {// 移除內容末尾的 \r\n--const endIndex = contentBuffer.indexOf('\r\n--');const fileContent = endIndex !== -1 ? contentBuffer.slice(0, endIndex) : contentBuffer;// 生成安全的文件名(添加時間戳)const ext = path.extname(headers.filename);const safeFilename = `${Date.now()}_${Math.random().toString(36).substring(2, 10)}${ext}`;const savePath = path.join(__dirname, 'uploads', safeFilename);// 直接寫入二進制數據fs.writeFile(savePath, fileContent, (err) => {if (err) {console.error('保存文件失敗:', err);res.statusCode = 500;res.end('服務器錯誤');}});console.log(`文件已保存: ${savePath}`);}});res.end('上傳完成');});} else {//設置為utf-8編碼res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });res.end(`<form method="post" enctype="multipart/form-data"><input type="file" name="file"><button type="submit">上傳</button></form>`);}
});// 按 boundary 分割 Buffer
function splitBuffer(buffer, boundary) {const parts = [];let startIndex = 0;while (true) {const index = buffer.indexOf(boundary, startIndex);if (index === -1) break;if (startIndex > 0) {parts.push(buffer.slice(startIndex, index));}startIndex = index + boundary.length;// 檢查是否到達末尾if (buffer.slice(index + boundary.length, index + boundary.length + 2).toString() === '--') {break;}}return parts;
}// 解析頭部信息
function parseHeaders(headerText) {const headers = {};const lines = headerText.split('\r\n');lines.forEach(line => {if (!line) return;const [key, value] = line.split(': ');if (key === 'Content-Disposition') {const params = value.split('; ');params.forEach(param => {const [name, val] = param.split('=');if (val) headers[name] = val.replace(/"/g, '');});} else {headers[key] = value;}});return headers;
}// 創建上傳目錄
fs.mkdirSync(path.join(__dirname, 'uploads'), { recursive: true });server.listen(3000, () => {console.log('服務器運行在 http://localhost:3000');
});
四、?前端實現
- 原生表單:
<form action="/upload" method="post" enctype="multipart/form-data"><input type="file" name="file"><input type="submit"> </form>
- AJAX 上傳(使用?
FormData
):const formData = new FormData(); formData.append('file', fileInput.files[0]);fetch('/upload', {method: 'POST',body: formData });
五 總結
- 協議核心:
multipart/form-data
?格式通過分隔符實現多字段傳輸。 - 安全要點:該案例不適合生產使用. 生產使用建議使用第三方庫formidable?