首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 文件上傳源碼深度解析:RFC 1867 標準與內核實現流程

     編輯:PHP 相關知識

前言

HTTP 文件上傳遵循 RFC 1867 (Form-based File Upload in HTML) 標準。PHP 內核在 SAPI 層接收到 `multipart/form-data` 請求後,通過 `rfc1867_post_handler` 解析邊界、分割字段、處理文件臨時存儲,最終填充 `$_FILES` 超全局變量。本文從協議格式、SAPI 接收、內核解析、臨時文件管理、安全檢查五個維度還原完整流程。

一、RFC 1867 請求格式回顧

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:隨機字串,分隔各部分,結尾加 `--` 表示結束。
  • 每部分包含 **頭部**(Content-Disposition、Content-Type 等)與 **體**(文本或二進制)。
  • 文件字段同時具備 `name` 與 `filename`;普通字段僅有 `name`。

二、SAPI 層接收與分發

以 Apache 2 Handler (`mod_php`) 為例:

  1. Apache 讀取請求頭,識別 `Content-Type: multipart/form-data`。
  2. 調用 `php_apache_sapi_read_post_data()` 將請求體讀入內存/臨時文件。
  3. SAPI 啟動階段 (`sapi_activate`) 檢測 `SG(request_info).content_type` 以 `multipart/form-data` 開頭,註冊 `rfc1867_post_handler` 為 POST 數據處理器。
  4. PHP 內核調用 `sapi_read_post_data()` → `rfc1867_post_handler()` 開始解析。

三、內核解析核心:main/rfc1867.c

3.1 主循環邏輯


// 簡化流程
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);
    }
}

3.2 關鍵結構體


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;

3.3 文件處理流程 handle_file_upload()

  1. 觸發 `MULTIPART_EVENT_FILE_START`:允許擴展(如 `uploadprogress`)介入。
  2. 創建臨時文件: ```c fd = php_open_temporary_fd_ex(PG(upload_tmp_dir), "php", &temp_filename, 1); ``` - 目錄由 `upload_tmp_dir` 指定(預設系統臨時目錄)。 - 文件名格式 `phpXXXXXX`(mkstemp 保證唯一)。 - 返回文件描述符 `fd` 與路徑 `temp_filename`。
  3. 循環讀取 Body 寫入臨時文件: ```c while ((blen = multipart_buffer_read(mbuff, buff, sizeof(buff))) > 0) { wlen = write(fd, buff, blen); if (wlen == -1) { cancel_upload = UPLOAD_ERROR_F; break; } } ```
  4. 關閉文件,觸發 `MULTIPART_EVENT_FILE_END`:記錄最終大小、臨時路徑。
  5. 註冊到 `SG(rfc1867_uploaded_files)`: ```c zend_hash_add(SG(rfc1867_uploaded_files), temp_filename, strlen(temp_filename)+1, &temp_filename, sizeof(char*), NULL); ``` 供 `move_uploaded_file()`/`is_uploaded_file()` 驗證使用。
  6. 填充 `$_FILES`: ```c // 有名上傳 $_FILES['userfile'] = [ 'name' => original_filename, 'type' => mime_type, 'tmp_name' => temp_filename, 'error' => UPLOAD_ERR_OK, 'size' => file_size, ]; // 無名上傳(無 name 屬性)鍵為 tmp_name ```

四、關鍵配置與安全檢查

配置項默認值作用
`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 上傳進度

  • PECL uploadprogress:掛鉤 `MULTIPART_EVENT_FILE_DATA` 記錄已接收字節到共享內存,前端輪詢獲取進度。
  • PHP 5.4+ Session Upload Progress:啟用 `session.upload_progress.enabled`,表單添加 ``,`$_SESSION['upload_progress_...']` 實時更新。

七、總結

  1. RFC 1867 定義了 `multipart/form-data` 邊界分割格式。
  2. PHP SAPI 識別 Content-Type → 註冊 `rfc1867_post_handler`。
  3. 內核邊讀邊解析:頭部提取 `name/filename` → 判斷類型 → 文件創建臨時文件流式寫入 → 註冊到 `SG(rfc1867_uploaded_files)` → 填充 `$_FILES`。
  4. 大小限制雙層:`upload_max_filesize`(PHP)+ `post_max_size`(PHP)+ `client_max_body_size`(Nginx)。
  5. 臨時文件請求結束自動清理,**必須用 `move_uploaded_file()` 持久化**。

理解內核流程有助於排查疑難上傳問題、編寫高性能上傳擴展、設計大文件分片上傳架構。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved