📘 第三章 HTTP报文内的HTTP信息
📄 一句话概括

多部分(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 秒实验

  1. 把 boundary 改成一个短串(比如 --x),看还能不能正确解析(能, 但一旦内容里出现 --x 就会断错)。
  2. 在文件内容里故意写进 ----WebKitFormBoundary7MA4YWxkTrZu0gW,重跑,看解析出几段。
  3. 算一算:上传一个 10 MB 的文件,封装开销还值不值得在意。

常见误区

  1. 「multipart 是编码」——它不做任何编码,只是拼接。文件是原样字节。
  2. 「表单只可能是 multipart」——普通表单用 application/x-www-form-urlencoded, 有文件才切到 multipart。
  3. 「boundary 随便写个短串就行」——长度就是安全性,短串容易被内容撞上。
自测三问(点开对答案) 1. 文件上传用的 `Content-Type` 是什么? —— `multipart/form-data; boundary=...`。 2. boundary 为什么要够长? —— 它没有转义机制,只靠「内容里不会恰好出现这段字节」,长度越长撞上概率越低。 3. 结束符和普通分隔符有什么区别? —— 结束符是 `--boundary--`,末尾多两个短横,表示后面没有段了。

多种数据能一起发了。下一节看「只要其中一段」怎么写。

相关笔记