多部分(multipart)把好几个「段」塞进一个报文,段之间用一段随机字符串 boundary 隔开。 上传文件全靠它。
上一节的分块是把一个内容切开发,这一节的多部分是把多个内容装一起发。 两者名字像,作用完全不同。
一、三种 multipart,用途各不同
| 类型 | 用途 | 常见度 |
|---|---|---|
multipart/form-data |
表单提交,含文件 | 极常见 |
multipart/byteranges |
范围请求回多段 | 很少 |
multipart/mixed |
混合内容(邮件附件) | 邮件里常见 |
书里三个都列了。日常写后端只会遇到第一个——凡是有 <input type='file'> 的表单,提交时一定是它。 |
二、格式:boundary 是唯一的分隔依据
请求首部写 Content-Type: multipart/form-data; boundary=XXX,正文里每段以 --XXX 开头,
段与段之间也是 --XXX,结束是 --XXX--。
每段内部有自己的小首部,最常见的只有一行 Content-Disposition: form-data; name="字段名";
文件段再加 ; filename="文件名",还可以带自己的 Content-Type。
Demo 里三段一共只装了 43 字节内容,封装却花了 331 字节——开销 88.5%。
因为 boundary 本身就有 37 字节,出现四次。这就是为什么上传小文件特别不划算。
三、为什么 boundary 必须够长够随机
解析器只认 boundary。
如果内容里恰好出现 --boundary 这段字节,解析就会在那里提前断段——文件被截断,或者多出一个假字段。
所以标准要求 boundary 足够长(实践里 30 字节以上)、足够随机。
浏览器生成的通常是 ----WebKitFormBoundary 加 16 位随机串。
理论上一段文本里可能出现任何字节序列,这是 multipart 天生不严谨的地方——它没有转义机制,
只靠「撞上的概率极低」。所以长度就是安全性。
四、动手 Demo:手工拼一个三段的上传请求
自己拼 boundary、拼三段(一个中文城市名、一段中文说明、一个 CSV 文件),发给服务端, 再原样拿回来逐段解析。
boundary = u'----WebKitFormBoundary7MA4YWxkTrZu0gW'
body = b''
for disp, data in parts:
body += (u'--%s\r\n' % boundary).encode()
body += (u'Content-Disposition: %s\r\n\r\n' % disp).encode()
body += data + b'\r\n'
body += (u'--%s--\r\n' % boundary).encode()
实跑输出:
--------------------------------------------------------------------
多部分对象集合:一次发好几样东西
--------------------------------------------------------------------
整段请求主体 374 字节
服务端收到的 374 字节(一致)
boundary 常量 ----WebKitFormBoundary7MA4YWxkTrZu0gW
boundary 长度 37 字节
分隔符出现次数 4(3 段 + 1 个结束符)
真正的内容合计 43 字节
封装开销 331 字节(88.5%)
第 1 段 Content-Disposition: form-data; name="city" → 6 字节
第 2 段 Content-Disposition: form-data; name="note" → 21 字节
第 3 段 Content-Disposition: form-data; name="file"; filename="a.txt" → 14 字节
这就是上传文件的全部原理:一个 HTTP 请求里塞好几个「段」,
每段有自己的小首部,段之间用 boundary 分隔。**没有魔法,纯拼字节**。
boundary 必须足够长、足够随机,否则可能和内容撞上——撞上就断错段。
整段请求主体 374 字节、真正的内容合计 43 字节、
封装开销 331 字节(88.5%)——封装是内容的七倍多。
分隔符出现次数 4(3 段 + 1 个结束符)。
这个数是可以数出来验的:三段各一个开头分隔,最后再来一个带两个短横的结束符。
逐段解析那三行最有说服力:name="city" 6 字节(宜昌 的 UTF-8)、name="note" 21 字节、
filename="a.txt" 14 字节。文件上传就是把字节原样塞进一段,没有任何编码。
30 秒实验
- 把 boundary 改成一个短串(比如
--x),看还能不能正确解析(能, 但一旦内容里出现--x就会断错)。 - 在文件内容里故意写进
----WebKitFormBoundary7MA4YWxkTrZu0gW,重跑,看解析出几段。 - 算一算:上传一个 10 MB 的文件,封装开销还值不值得在意。
常见误区
- 「multipart 是编码」——它不做任何编码,只是拼接。文件是原样字节。
- 「表单只可能是 multipart」——普通表单用
application/x-www-form-urlencoded, 有文件才切到 multipart。 - 「boundary 随便写个短串就行」——长度就是安全性,短串容易被内容撞上。
自测三问(点开对答案)
1. 文件上传用的 `Content-Type` 是什么? —— `multipart/form-data; boundary=...`。 2. boundary 为什么要够长? —— 它没有转义机制,只靠「内容里不会恰好出现这段字节」,长度越长撞上概率越低。 3. 结束符和普通分隔符有什么区别? —— 结束符是 `--boundary--`,末尾多两个短横,表示后面没有段了。多种数据能一起发了。下一节看「只要其中一段」怎么写。
- ☐ (本节完,下一节 3.5 获取部分内容的范围请求)