HTTP 文件上傳遵循 RFC 1867 (Form-based File Upload in HTML) 標準。PHP 內核在 SAPI 層接收到 `multipart/form-data` 請求後,通過 `rfc1867_post_handler` 解析邊界、分割字段、處理文件臨時存儲,最終填充 `$_FILES` 超全局變量。本文從協議格式、SAPI 接收、內核解析、臨時文件管理、安全檢查五個維度還原完整流程。
POST /upload.php HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 12345
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"
用戶輸入的描述文字
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="userfile"; filename="test.png"
Content-Type: image/png
二進制文件內容...
------WebKitFormBoundary7MA4YWxkTrZu0gW--
boundary:隨機字串,分隔各部分,結尾加 `--` 表示結束。以 Apache 2 Handler (`mod_php`) 為例:
main/rfc1867.c// 簡化流程 while (multipart_buffer_read(mbuff, &buf, &len)) { // 1. 解析頭部 if (parse_headers(buf, len, &headers)) continue; // 2. 從 Content-Disposition 提取 name / filename cd = get_header_value(headers, "Content-Disposition"); parse_cd(cd, &name, &filename); // 3. 判斷類型 if (filename) { // 文件上傳 handle_file_upload(name, filename, headers, mbuff); } elseif (name) { // 普通字段 handle_normal_field(name, mbuff); } }
typedef struct _multipart_event_file_start { const char *name; // 表單字段名 const char *filename; // 原始文件名 } multipart_event_file_start; typedef struct _multipart_event_file_data { const char *data; size_t length; off_t offset; } multipart_event_file_data; typedef struct _multipart_event_file_end { const char *temp_filename; // 臨時文件路徑 int cancel_upload; // 是否取消 } multipart_event_file_end;
handle_file_upload()| 配置項 | 默認值 | 作用 |
|---|---|---|
| `file_uploads` | On | 總開關 |
| `upload_tmp_dir` | 系統臨時目錄 | 臨時文件存放路徑,需可寫 |
| `upload_max_filesize` | 2M | 單文件最大體積(PHP 層檢查) |
| `post_max_size` | 8M | 整個 POST 體最大體積,必須 ≥ upload_max_filesize |
| `max_file_uploads` | 20 | 單次請求最大文件數 |
| `max_input_time` | 60s | 解析 POST 數據最長耗時 |
rfc1867.c)/* 1. PHP 硬性限制 */ if (PG(upload_max_filesize) > 0 && total_bytes > PG(upload_max_filesize)) { cancel_upload = UPLOAD_ERROR_A; // UPLOAD_ERR_INI_SIZE } /* 2. 表單隱藏字段 MAX_FILE_SIZE(客戶端可繞過,僅提示) */ else if (max_file_size && total_bytes > max_file_size) { cancel_upload = UPLOAD_ERROR_B; // UPLOAD_ERR_FORM_SIZE }
結論:`` **不可信**,僅作用戶體驗提示,真正限制靠 `upload_max_filesize` / `post_max_size` / Web Server (`client_max_body_size`)。
| 現象 | 內核原因 | 解決 |
|---|---|---|
| `$_FILES` 為空 | `post_max_size` < 實際 POST 體,SAPI 直接丟棄 | 調大 `post_max_size`、Nginx `client_max_body_size` |
上傳大文件報 UPLOAD_ERR_PARTIAL | 網絡中斷導致實際接收字節 < `Content-Length` | 檢查網絡/代理超時、增大 `max_input_time` |
| 臨時文件消失 | 請求結束 `sapi_deactivate` 會 `unlink` 所有 `rfc1867_uploaded_files` | 必須在腳本執行期間調用 `move_uploaded_file()` |
| 文件名亂碼 | 瀏覽器發送的 `filename` 編碼不一(RFC 5987 `filename*` vs 老式) | 用 `rawurldecode()` 或 `mb_convert_encoding()` 兼容處理 |
| 並發上傳臨時文件衝突 | 極罕見,`mkstemp` 保證唯一 | 無需處理 |
uploadprogress 與 Session 上傳進度理解內核流程有助於排查疑難上傳問題、編寫高性能上傳擴展、設計大文件分片上傳架構。