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,以及那个文件的、未经加密的、完整的二进制数据流。

问题三:服务器如何接收并还原文件?

服务器收到数据后,会进行一系列“拆包”和“还原”操作。

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 格式把文件和其他信息打包放在请求体里发过去。