HTTP 文件傳輸協議解析:上傳與下載
這份文檔會用最簡單的方式,帶你了解 HTTP 協議是如何處理文件下載和上傳的。我們會專注于協議本身,看看客戶端(比如你的瀏覽器)和服務端(網站服務器)之間到底在“聊”些什么。
1. 文件下載:從服務器“拉”數據
文件下載相對簡單,核心就是一個 GET
請求。你可以把它想象成你對服務器說:“嘿,請把那個文件給我”。
過程解析
-
客戶端發起請求: 你的瀏覽器向服務器發送一個
GET
請求,請求的 URL 就是那個文件的地址。 -
服務器響應: 服務器收到請求后,如果文件存在且你有權限訪問,它就會返回一個 HTTP 響應。這個響應的“身體”(Response Body)里就裝著文件的完整二進制數據。
關鍵的 HTTP 報文(Headers)
在下載過程中,服務器的響應報文(Response Headers)非常重要,它們會告訴瀏覽器如何處理接收到的數據。
-
Content-Type
: 這個字段告訴瀏覽器這是個什么類型的文件。-
image/jpeg
: 一個 JPEG 圖片 -
application/pdf
: 一個 PDF 文件 -
text/plain
: 純文本 -
text/markdown; charset=utf-8
: 一個 Markdown 文本文件,使用 UTF-8 編碼。 -
application/octet-stream
: 這是個通用的二進制文件類型,瀏覽器通常不知道如何直接打開它,于是會提示用戶下載。
-
-
Content-Length
: 這個字段告訴瀏覽器文件有多大(單位是字節)。這樣瀏覽器就可以顯示下載進度條了。 -
Content-Disposition
: 這是個非常有用的字段。-
如果它的值是
inline
,瀏覽器會嘗試直接在頁面里顯示這個文件(比如圖片、PDF)。 -
如果它的值是
attachment; filename="your-file-name.zip"
,瀏覽器會立即彈出下載對話框,并使用filename
指定的文件名作為默認保存名。
-
-
Last-Modified
: 文件在服務器上最后一次被修改的時間。這個信息可以被瀏覽器用來做緩存判斷,避免重復下載沒有變化的文件。 -
ETag
(Entity Tag): 文件的“指紋”或版本號。這是一個唯一的字符串,只要文件內容有變動,ETag
就會改變。它也是用來做緩存控制的,比Last-Modified
更精確。 -
Accept-Ranges
: 告訴客戶端,服務器是否支持“范圍請求”(即只請求文件的一部分)。值為bytes
表示支持,這對于實現斷點續傳功能至關重要。 -
Server
: 響應這個請求的服務器軟件名稱和版本,例如Apache
,Nginx
,uvicorn
。 -
Date
: 服務器生成并發送此響應的時間。
下載示例(協議層面)
示例一:簡單示例
# 1. 客戶端(瀏覽器)的請求
GET /files/document.pdf HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...# 2. 服務器的響應
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 102400
Content-Disposition: attachment; filename="report.pdf"# (這里是 102400 字節的 PDF 文件二進制數據)
...
示例二:包含更多信息的詳細示例
HTTP/1.1 200 OK
Date: Thu, 04 Sep 2025 08:16:55 GMT
Server: uvicorn
Content-Type: text/markdown; charset=utf-8
Content-Length: 12004
Content-Disposition: attachment; filename="parsed_6983e807-f1f9-4416-77d52c295a80.md"
Last-Modified: Thu, 04 Sep 2025 07:44:51 GMT
ETag: "13b5d596f8229a520b7ccd9f889b290c"
Accept-Ranges: bytes# (這里是 12004 字節的 Markdown 文件數據)
...
2. 文件上傳:向服務器“推”數據
文件上傳比下載要復雜一些,核心是使用 POST
或 PUT
請求。你可以把它想象成你要給服務器寄一個包裹。
文件是如何被發送的?(一個比喻)
上傳時,發送的并不是文件的路徑,而是文件內容的完整二進制數據。瀏覽器會打開這個文件,讀取里面所有的數據,然后把這些原始的二進制數據流直接放進 HTTP 請求的 Body(請求體)里。
和普通 POST 表單提交有何不同?
文件上傳和普通的 POST 表單提交不一樣,關鍵區別在于 Content-Type
和請求體的數據結構。
-
普通 POST 表單提交:
Content-Type
通常是application/x-www-form-urlencoded
,請求體是key=value&key2=value2
這樣的字符串,只適合文本。 -
文件上傳 POST:
Content-Type
必須是multipart/form-data
,請求體被分割成多個部分,可以同時容納文本和二進制文件。
問題一:文件是如何被打包成一個整體的?
你的理解有一點點偏差,這是一個非常常見的誤區。
-
誤區:把一個大文件的二進制數據,用分割符切成若干個小參數來發送。
-
正確理解:分割符(boundary)的作用是分隔不同的表單項,而不是用來切割單個文件。
你可以把整個請求體(Request Body)想象成一個大包裹。
-
包裹里有不同的“隔間”,
boundary
就是這些隔間的“分隔板”。 -
一個隔間放你的用戶名(一個
part
)。 -
另一個隔間放你上傳的文件(另一個
part
)。 -
整個文件的完整二進制數據,是作為一個整體,被原封不動地放進它自己所屬的那個“隔間”里的。它并沒有被
boundary
切割。
所以,請求體看起來像這樣:
[分隔板]
-> [用戶名字段]
-> [分隔板]
-> [一整個文件的完整二進制數據]
-> [帶結尾的分隔板]
問題二:這塊打包好的數據如何傳輸?會很大嗎?
這里依次回答你的問題:
-
這塊數據怎么傳輸呢?是和普通的post請求一樣通過一個鍵值對的值發送嗎?
- 不,它不是通過一個鍵值對的值發送的。這一點是
multipart/form-data
和application/x-www-form-urlencoded
的根本區別。這一整塊被boundary
分隔的數據,它本身就是 HTTP 請求的 Body(請求體)的全部內容。HTTP 請求頭中的Content-Type: multipart/form-data; ...
就像是這個 Body 的“說明書”,告訴服務器:“接下來我發送的 Body 是一個多部分格式的數據,請你用這個boundary
字符串來解析它”,而不是一個簡單的key=value
字符串。
- 不,它不是通過一個鍵值對的值發送的。這一點是
-
這一塊數據會不會特別大?
- 會的。請求體的大小約等于所有文件的大小 + 所有文本字段的大小 + 分隔符和頭部信息的額外開銷。如果你上傳一個 100MB 的視頻,那么這個 HTTP POST 請求的 Body 大小就會超過 100MB。這也就是為什么服務器通常會設置一個“請求體大小限制”(比如 200MB),以防止惡意用戶或程序通過上傳超大文件耗盡服務器資源。
-
抓包的話是不是會看到這個一整塊的二進制數據?
- 完全正確。如果你使用像 Wireshark 這樣的網絡抓包工具,并捕獲了這次上傳的流量,你將能夠清晰地看到整個 HTTP POST 請求的原始報文。你會先看到請求頭(Request Headers),緊接著就是原封不動的請求體(Request Body),其中包含了所有的
boundary
分隔符、每個部分的Content-Disposition
,以及那個文件的、未經加密的、完整的二進制數據流。
- 完全正確。如果你使用像 Wireshark 這樣的網絡抓包工具,并捕獲了這次上傳的流量,你將能夠清晰地看到整個 HTTP POST 請求的原始報文。你會先看到請求頭(Request Headers),緊接著就是原封不動的請求體(Request Body),其中包含了所有的
問題三:服務器如何接收并還原文件?
服務器收到數據后,會進行一系列“拆包”和“還原”操作。
-
a. 識別包裹類型: 服務器首先檢查請求頭的
Content-Type
,看到是multipart/form-data
,并且獲取到了那個獨一無二的boundary
字符串。 -
b. 按分隔板拆包: 服務器的應用程序(比如 PHP, Python, Java 的 Web 框架)會使用這個
boundary
字符串作為標記,去掃描整個請求體。每當遇到一個boundary
,它就知道這是一個新部分的開始。 -
c. 解析每個部分:
-
對于文本部分,服務器直接讀取
name
和它的值(比如username
和John
)。 -
對于文件部分,服務器會讀取它的
name
(比如userfile
)和filename
(比如avatar.png
),然后將這個部分包含的、連續的、完整的二進制數據流讀取出來。
-
-
d. 還原文件: 服務器通常會把這個讀取出來的二進制數據流暫存到服務器磁盤上的一個臨時文件中,或者直接在內存中處理。至此,你在本地電腦上的那個文件,就以一個臨時文件的形式,在服務器上被完整地“復制”或“還原”了出來。后續程序就可以對這個還原后的文件進行永久保存、病毒掃描或其他處理了。
一個簡單的上傳示例(協議層面)
假設我們要上傳一個名為 avatar.png
的圖片,并且附帶一個用戶名字段 username
,值為 John
。
# 客戶端(瀏覽器)的請求
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 437# (下面是請求體 Request Body 的內容)
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"John
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="userfile"; filename="avatar.png"
Content-Type: image/png# (這里是 avatar.png 文件的完整二進制數據,沒有被切割)
...
------WebKitFormBoundary7MA4YWxkTrZu0gW--
總結
操作 | HTTP 方法 | 數據位置 | 關鍵 Content-Type |
---|---|---|---|
下載 | GET | 響應體 (Response Body) | application/octet-stream , image/jpeg 等 |
上傳 | POST / PUT | 請求體 (Request Body) | multipart/form-data |
簡單來說:
-
下載 就是客戶端發一個簡單的
GET
請求,服務器把文件放在響應體里發回來。 -
上傳 就是客戶端發一個結構化的
POST
請求,用multipart/form-data
格式把文件和其他信息打包放在請求體里發過去。