网络知识

理解 HTTP 中的 multipart/form-data

背景


HTTP 是一种基于请求-响应模型的网络通信协议,主要用于 Web 中客户端和服务器之间通信的数据传输。事实上,如今的互联网就是构建在 HTTP 之上的。基于请求-响应模式的通信方式很简单,客户端向服务器发送请求,服务器处理请求并进行响应。
HTTP 其实并不关心我们想要传输的是什么类型的数据,因为它什么类型的数据都可以传输!对于客户端或者服务器来说,只需要设置好合适的 Content-Type,让另一端能够理解数据就可以了。在一个请求中,客户端发送给服务器的数据可以是文本,也可以是图片或者音视频等等。
如果客户端想在一个请求中给服务器发送多种类型的数据呢,该怎么办呢?例如,同时发送用户的基本信息和头像。方法很简单,就是使用 HTTP 的 multipart。

Multipart 消息结构



Multipart 允许客户端在一次 HTTP 请求中发送多个部分(part)数据,每部分数据之间的类型可以不同。Multipart 并不是一种专一的数据类型,它有很多子类型,例如:multipart/mixed,multipart/form-data 等。
通俗来讲,一个 multipart 消息就是一个大包裹,包裹里面有多个不同类型的消息,每一个消息就是一个 part,每个 part 都会声明自己的消息类型(Content-Type)。除了消息类型,part 还可以附加一些元数据。
Multipart 消息的基本语法结构可以在 RFC2046 中找到:

  • 每个 multipart 消息的 Content-Type 都必须包含一个叫做 boundary 的参数,boundary 声明了各个 part 之间的边界,记为 ${boundary}。实际上,完整的边界定义为:一行由两个 – 加上 ${boundary} 组成的字符串。假设我们在 Content-Type 里面指定的 boundary=example-part-boundary,那么按照协议规定,每个 part 之间的分隔行就是:–example-part-boundary。
  • 每个边界之后是一个 CRLF 加下一个 part 的头部信息。如果下一个 part 没有头部信息,边界之后就应该跟两个 CRLF,这样下一个 part 的消息类型就会被认为是 text/plain。
  • ${boundary} 不能出现在边界之间,并且长度不能超过 70 个字符。
  • 最后一个 part 之后的边界在末尾多了两个 -,表示后面不会再有其它的 part 了。这个边界的完整格式为:–${boundary}–,例如 –example-part-boundary–。

我们会在后面讲 multipart/form-data 给出一个具体的示例。

multipart/form-data



Multipart 的使用在 Web 应用程序中很常见。使用 multipart 频率最高的地方大概就是 Web 表单了,在表单提交时,文件的上传就是通过 Multipart 来实现的。
并不是所有的表单提交都会使用 multipart,如果表单只包含基于文本的输入组件(例如输入框、单选框等),浏览器会将这些数据以 key=value 的形式组织,使用一种被称为 application/x-www-form-urlencoded 的 Content-Type 传输。

application/x-www-urlencoded:数据被编码成以'&'分隔的键-值对, 同时以'='分隔键和值. 非字母或数字的字符会被 百分比编码(percent-encoding

如果表单中包含文件或图片等不能被编码成文本的元素,浏览器就会使用 multipart/form-data 向服务器传输数据。

下面是一个使用 multipart/form-data 传输用户信息的例子:

POST /profile HTTP/1.1
HOST: example.com
Content-Type: multipart/form-data; boundary=example-part-boundary
 
--example-part-boundary
Content-Disposition: form-data; name="username"
Content-Type: text/plain
 
Nicholas
--example-part-boundary
Content-Disposition: form-data; name="address"
Content-Type: application/json
 
{
    "country": "China",
    "city": "Beijing"
}
--example-part-boundary
Content-Disposition: form-data; name="avatar"; filename="my_avatar.jpeg"
Content-Type: image/jpeg
 
<binary-image data>
--example-part-boundary--


在上面这个请求中:

  • Content-Type: multipart/form-data; boundary=example-part-boundary 表示这个请求的的消息类型是 multipart-form-data,每个 part 之间的边界为 example-part-boundary
  • 这个请求总共包含三个 part:
第一个 part 的类型为 text/plain,它在表单上对应的 key 为 username,value 为 Nicholas。
第二个 part 的类型为 application/json,它在表单上对应的 key 为 address。
第三个 part 的数据类型为 image/jpeg,它在表单上对应的 key 为 avatar,并且 part 的头部还附加了文件名相关的元数据 filename="my_avatar.jpeg。
  • 最后面的 --example-part-boundary-- 表示整个 multipart 消息的结束。

multipart/form-data重要规范特征


  • 必须post方式发送数据;
  • Content-Type格式为multipart/form-data; boundary=${boundary}。满足条件的提交,浏览器会自动创建boundary。例如chrome和safari浏览器,自动创建的boundary格式像下面这个样子,其中boundary是长度为16的随机base64字符。
----WebKitFormBoundary${boundary}

例如:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary222BPd3etU0TLTOv
发送数据内容以------WebKitFormBoundary?${boundary}作为起始标记和分隔符、终结标记多了“--”;数据内容主要包括:Content-Disposition、Content-Type、数据内容等;其中数据内容前面有\n\r标记的空行;Content-Disposition是必选项,其它都是可选项;Content-Disposition 包含了 type 和 一个名字为 name 的 parameter,type 是 form-data,name 参数的值则为表单控件(username)的名字,如果是文件,那么还有一个 filename 参数,值就是文件名。

例如:

------WebKitFormBoundarywKADO5urZL0xelAF
Content-Disposition: form-data; name="name"


------WebKitFormBoundarywKADO5urZL0xelAF
Content-Disposition: form-data; name="time"

1586232882192
------WebKitFormBoundarywKADO5urZL0xelAF
Content-Disposition: form-data; name="f"

[object FileList]
------WebKitFormBoundarywKADO5urZL0xelAF--

补充: soapui 发送 multipart/form-data

  • (1)在图里1中,确认请求是否有参数;
  • (2)在图里2中,确认传输内容类型(注:2中后面有个选择框,如果选择,会将1中参数和值,直接拼接在url中进行请求;如果没有选择,则在body中传输,一般建议是选择:multipart/form-data,如图);
  • (3)切换到Attachments标签页,在图里3中,点击“+”上传附件,并找到列表中ContentID字段,这个id是对应”Content-Disposition”中的name参数值,需注意(注:自行去百度Content-Disposition的作用)

总结


Multipart 是一种常见的数据格式,常用于上传文件和发送包含多种数据类型的单个请求。正确地使用 multipart 可以方便地实现多种数据的传输,提高数据传输效率和用户的使用体验,减少服务器的请求次数。
但是,在使用 multipart 的时候,客户端必须正确地设置 Content-Type 请求头,包含 boundary 参数,并且 boundary 参数的内容不能和请求体内的内容重复。服务器收到请求后,需要根据 Content-Type 设置的 boundary 来解析请求体各个部分的内容。