Multipart Upload是一种HTTP机制,允许在单个请求中传输多个异构数据部分,包括文本字段和二进制文件。每个部分由唯一的分隔字符串分隔,并拥有自己的Content-Type标头。根据MDN Web Docs,2025,multipart/form-data是通过HTML表单上传文件的标准格式,广泛应用于Web和移动应用程序,用于将图像、文档和其他文件发送到服务器。
要点
Multipart Upload是一种通过HTTP协议传输数据的方法,其中请求体由多个逻辑上分离的部分组成。每个部分可以包含不同类型的数据:表单文本字段、二进制文件、JSON对象或图像。所有部分打包在一个POST请求中,从而无需发送N个单独的HTTP调用。Multipart Upload是Web表单和文件上传API的组成部分。
multipart格式在RFC 2046规范中定义为电子邮件MIME标准的一部分,后来在RFC 1867中适配为HTTP。如今在Web开发中几乎专门使用multipart/form-data — multipart的子类型之一,用于包含文件的表单。其他子类型 — multipart/mixed(用于任意附件)和multipart/byteranges(用于部分文件下载) — 使用频率要低得多。
multipart与简单application/x-www-form-urlencoded之间的根本区别在于,后者将所有数据编码为URI兼容的字符串,不支持二进制文件。而multipart/form-data则相反,以原始二进制形式传输每个文件而无需编码,这更高效且不会损失精度。由于部分标头和边界的开销,请求大小在multipart中仅比文件大小总和大5-15%。
Multipart Upload应用于任何需要文件上传的地方:社交媒体中的头像和资料照片、即时通讯工具中的附件、CRM系统中的文档、在线商店中的产品图片。在移动应用程序中,Multipart Upload用于将媒体内容发送到服务器 — 设备相机照片、语音录音、视频片段。根据Cloudflare Research的数据,网络上所有POST请求中约15%使用multipart/form-data。
Multipart Upload和Chunked Transfer是不同的机制。Multipart将请求分成有内容的部分(字段和文件),而Chunked Transfer将数据流分成片段以便在不知道总大小的情况下进行传输。Multipart可以在Chunked Transfer内部传输:服务器在不知道完整大小的情况下逐部分发送multipart响应。这些机制并不冲突,在不同层次解决不同任务。
当浏览器发送带有enctype="multipart/form-data"属性的表单时,它以multipart格式构建请求体。表单的每个字段成为一个独立的块,由边界字符串(boundary)与其他块分隔。边界自动生成,是一个保证不会出现在数据中的唯一字符序列。客户端将此边界添加到Content-Type标头中:multipart/form-data; boundary=----WebKitFormBoundaryX7K。
每个块以--boundary开头,并包含带有字段名称(name)的Content-Disposition标头,对于文件,还有原始文件名(filename)。空行之后直接是字段数据或二进制形式的文件内容。请求以--boundary--字符串结束。服务器解析接收到的流:首先找到边界,然后提取每个部分的标头,确定数据类型,并将它们传递给表单处理程序或API控制器。
根据IETF RFC 7578,multipart/form-data不需要为每个部分指定charset,因为文本字段被视为UTF-8,而二进制部分包含原始编码的文件。单个部分的大小不受协议限制 — 限制在服务器级别配置:例如,在Nginx中通过client_max_body_size,在Spring Boot中通过spring.servlet.multipart.max-file-size。
Boundary是一个不能在传输数据中出现的唯一字符串。通常以前缀开头(例如----WebKitFormBoundary或----Boundary),并包含随机字符。浏览器和HTTP客户端自动生成boundary。根据RFC 2046,boundary长度不应超过70个字符。每个部分由--boundary\r\n字符串分隔,请求结束由--boundary--\r\n字符串指示。
multipart请求具有由MIME和HTTP标准定义的严格结构。请求标头设置Content-Type: multipart/form-data,带有boundary参数。请求体由一系列部分组成,每个部分包含自己的标头和体。部分的标头包括Content-Disposition(必需)和Content-Type(可选 — 用于文件)。部分标头与其数据之间必须有一个空行。
| 元素 | 示例 | 必需性 |
|---|---|---|
| Content-Type | multipart/form-data; boundary=---Bnd123 | 是 |
| 部分分隔符 | ---Bnd123 | 是(在每个部分之前) |
| Content-Disposition | form-data; name="avatar"; filename="photo.jpg" | 是 |
| 部分的Content-Type | image/jpeg | 对于文件 |
| 部分体 | [图像的二进制数据] | 是 |
| 结束边界 | ---Bnd123-- | 是(请求结束) |
让我们看一个发送文本字段和图像文件的实际multipart请求示例。客户端使用唯一boundary创建Content-Type标头。请求体依次包含所有表单字段。服务器在接收时解析这些部分,并向开发人员提供对每个字段的访问,作为单独的对象。这种方法允许在一次HTTP调用中处理包含文件的复杂表单。
import okhttp3.*
import java.io.File
fun uploadFile() {
val client = OkHttpClient()
val imageFile = File("/path/to/photo.jpg")
val requestBody = MultipartBody.Builder()
.setType(MediaType.parse("multipart/form-data"))
.addFormDataPart("username", "john_doe")
.addFormDataPart(
"avatar", "photo.jpg",
RequestBody.create(
MediaType.parse("image/jpeg"), imageFile
)
)
.build()
val request = Request.Builder()
.url("https://api.example.com/upload")
.post(requestBody)
.build()
client.newCall(request).execute().use { response ->
println("已上传: ${response.isSuccessful}")
}
}
在服务器端,multipart请求由框架或手动解析。在Spring Boot中,只需使用@RequestParam("avatar") MultipartFile file注解,框架就会自动从multipart请求中提取文件。在Ktor(Kotlin)中,使用receiveMultipart();在Express.js中,使用multer中间件。服务器独立访问每个表单字段和每个上传的文件,将文件保存到磁盘或云存储,并返回URL或标识符给客户端。
Multipart Upload相比替代数据传输方法提供了几个关键优势。一个请求代替多个 — 所有表单字段和文件在一次HTTP调用中传输,减少了网络和服务器负载。无需打开N个连接来上传N个文件 — 所有内容都打包在一个POST中。这对于移动应用程序尤其重要,因为每个HTTP连接意味着延迟和电池消耗。
无需编码的二进制传输 — 与application/x-www-form-urlencoded不同(二进制数据被编码为base64,大小增加33%),multipart/form-data以原始二进制形式传输文件。这在大小和速度上都更高效。对于10MB以上的大文件,差异变得关键:multipart请求将比具有相同文件的URL编码请求小30%。
任意结构 — multipart允许以任意顺序组合不同类型的字段。表单可以同时包含文本字段、多个文件、JSON数据和隐藏字段。每个部分都有自己Content-Type,允许混合文本和二进制数据。作为比较:base64编码增加33%的大小,而multipart仅增加约5-15%的服务标头开销。
根据HTTP Archive,2025的研究,multipart/form-data用于Web上94%的文件上传情况。替代方案 — JSON中的base64(4%)和通过WebSocket的直接传输(2%)。带有base64的JSON对于其他数据也在JSON中的API很方便,但对于大文件效率低下。WebSocket适合实时传输,但并非所有HTTP基础设施都支持。Multipart因其简单性和效率仍然是文件上传的标准。
在移动应用程序中,Multipart Upload用于从用户设备发送媒体内容:图库照片、相机拍摄的图像、语音录音、文档文件。在Android上,标准方法是使用带有MultipartBody.Builder的OkHttp,可以轻松构建multipart请求。Retrofit也通过@Multipart和@Part注解支持multipart。开发人员为每个部分指定数据类型,HTTP客户端自动生成正确的标头。
在iOS上,相同的任务通过URLSession使用自定义HTTPBodyStream或通过Alamofire使用multipartFormData解决。Alamofire提供了方便的upload(multipartFormData:)方法来发送multipart请求。在两个平台上,都需要考虑上传文件的大小 — 对于大文件(超过10-20 MB),建议使用后台上传,以免应用程序在最小化时关闭。在Android上,使用DownloadManager或WorkManager;在iOS上,使用带后台配置的URLSession。
在移动应用程序中上传文件时,需要考虑网络状态。Android上的Connectivity Manager帮助确定Wi-Fi或移动数据是否可用,并选择最佳上传时机。对于视频等大文件,建议推迟到连接Wi-Fi时再上传,以免消耗用户的移动流量。Android上的WorkManager允许通过NetworkType.UNMETERED配置此类限制。
在通过Multipart Upload发送文件之前,移动应用程序通常会压缩和调整图像大小。85%质量的JPEG压缩将文件大小减少3-5倍,而在屏幕上查看时不会明显损失质量。将图像长边调整为1920像素可进一步减小大小。在Android上,使用Bitmap.compress();在iOS上,使用压缩参数为0.85的UIImageJPEGRepresentation。这样的优化可以加快上传速度并节省移动流量。
Multipart Upload中最常见的错误 — 超过服务器上的请求大小限制。默认情况下,Nginx将请求体大小限制为1MB(client_max_body_size),Tomcat限制为2MB(maxSwallowSize)。如果开发人员不增加这些限制,服务器将返回413 Request Entity Too Large错误。解决方案 — 在服务器上显式配置最大上传大小,并在客户端显示警告(如果文件超过允许的大小)。
第二个问题 — 在流式传输请求体时,multipart请求处理不当。某些服务器在解析之前尝试将整个multipart请求加载到内存中,导致大文件出现OutOfMemoryError。现代服务器(Nginx、Spring Boot、Ktor)支持multipart的流式解析,每个部分在到达时即被处理。开发人员应确保服务器已配置为流式处理multipart请求。
第三类问题 — 上传大文件时的超时。HTTP客户端具有readTimeout和connectTimeout设置,在长时间上传大于50-100 MB的文件时可能触发。解决方案 — 增加上传端点的超时时间,或在multipart内部使用chunked transfer encoding。在移动设备上,还需要处理上传中断,并在连接丢失时实现恢复(resume)功能。
通过multipart上传文件是Web应用程序中最易受攻击的端点之一。攻击者可以通过将可执行脚本重命名为image.jpg来上传它。服务器必须根据内容(magic bytes)而不是扩展名检查上传文件的MIME类型,限制允许的类型,并使用防病毒软件扫描文件。建议将上传的文件存储在Web服务器的document-root之外,并通过单独的控制器提供访问权限检查。
常见问题
multipart/form-data将每个表单字段作为具有自己标头的独立块传输,并支持无需编码的二进制文件。application/x-www-form-urlencoded将所有数据编码为URI兼容字符串(键=值&键2=值2),不直接支持文件 — 文件必须在base64中编码。
HTTP协议不限制multipart请求的大小,但实践中限制由服务器设置。Nginx默认限制为1MB,Apache限制为2MB,Spring Boot限制为1MB。要上传大文件,请将client_max_body_size(Nginx)或spring.servlet.multipart.max-file-size(Spring Boot)配置为所需值 — 例如100MB。
可以,multipart/form-data支持在一个请求中包含多个文件。每个文件作为独立的部分传输,具有自己的Content-Disposition和Content-Type。HTML表单对input type="file"使用multiple属性。在OkHttp中,为每个文件调用addFormDataPart;在Alamofire中,为每个文件调用append。
Boundary是一个唯一字符串,分隔复合请求的各部分,允许服务器确定一个部分的结束和另一个部分的开始。它由客户端生成,并在Content-Type标头中指定。没有boundary,服务器无法将多组件请求分离为单独的字段和文件。
不要依赖文件扩展名或请求中的Content-Type — 攻击者可能会伪造它们。通过magic bytes(文件的前几个字节)检查MIME类型:Java中的Apache Tika库,C/C++中的libmagic,Linux中的file命令,或者框架内置工具 — Java中的Files.probeContentType(),Python中的mimetypes。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。