你真的知道网页上传文件背后的原理吗()

今年第三季度工作上完成了一个比较有意思的项目,类似于外包的性质,主要任务就是提供一大堆API,其中一个API是上传附件,完成开发后,对方的程序员问我,这个API怎么调用,当时我就愣住了,因为自己也没想过这个问题,一般情况下,我就是用 Curl 命令行或 Postman 测试API的。
【你真的知道网页上传文件背后的原理吗()】针对文件上传,我使用 Curl 测试,比如:

# 使用@引用一个文件 $ curl -F"param=value" -F"file=@/path/file.png" http://localhost/api.php

如果使用Postman测试,如下图:
你真的知道网页上传文件背后的原理吗()
文章图片
图1 注意观察form-data和File标签。
看上去是不是很简单,现在换个角度,你想以代码的方式上传文件API,怎么办?也非常简单,很多开发语言有很多现成的库,比如PHP通过Curl库上传文件非常容易。再深入想一想,如果不使用这些库,怎么上传文件?可能会难倒很多人,所以这篇文章简单讲讲文件上传的原理,其实就是根据HTTP协议的定义,封装一个HTTP消息体。
MIME
首先必须先讲下MIME(Multipurpose Internet Mail Extensions),它并不是HTTP协议的一部分,就像我们每个人都是独一无二的,有自己的属性,互联网上每个资源也有属性,比如有些资源是图片,有些是视频,有些是HTML页面,MIME规定了每种资源的类型,这个类型不是随便定义的,由IANA负责登记和维护。
说的有点难理解,比如你看到一个URL地址,http://localhost/image.png,我们其实并不是通过.png后缀判断资源类型的,而是通过MIME来获知该资源类型的,这个图片的MIME可能就是image/png(至于客户端如何知晓资源的MIME类型,后面会讲),现在是不是有了点感性的认识了。
MIME类型结构如下:
type/subtype

type相当于某些类型的集合,而subtype相当于子类型。以image/png为例,image表示图片类型集合,png表示某种类型图片。
让我们看几个比较重要的MIME类型:
text/plain text/html application/octet-stream multipart/form-data

其实本篇文章的主角就是multipart/form-data,再等一等,先别着急,再一次说说MIME,从它的英文全称来看,它和mail有关系,是由mail应用定义而来的,一封邮件由多种资源组成,为了将不同类型的资源组成在邮件中,MIME产生了。随着互联网Web的发展,MIME的作用越来越多,扩展也越来越多,MIME概念也逐步移到了Web。
Content Type
现在我们定义了每种资源的MIME类型,那么客户端如何知晓每种资源的MIME类型呢?这时候就要使用Content-Type HTTP Header 头了,比如我们请求一个资源,Web服务器在发送资源的时候,发送了“content-type:image/png” Header 头,这样客户端就知道该资源是一个png图片了。
如果客户端发送了一个 “Content-Type: multipart/form-data; ”,代表客户端要上传一个附件。
也就是说 Content-Type 后面的值就是一个 MIME 类型,聪明的同学也猜到了,上传附件和 multipart/form-data MIME 类型有关,确实是!
multipart/form-data
multipart/form-data 这个MIME类型并不是标准的MIME类型,而是因为Web的需要扩展而来的,我们在开发网页的时候为了上传一个文件,会输入以下的HTML标签:
Select image to upload:

关于HTML表单上传可以参考 https://www.w3.org/TR/html5/sec-forms.html#multipart-form-data 或 RFC 1867(Form-based File Upload in HTML,该RFC已经废弃了)。
那么multipart/form-data表示什么呢?multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件,具体的定义可以参考RFC 7578。
multipart/form-data结构
说了那么多,从HTTP协议的角度,最后看下文件上传的HTTP消息体,使用Postman也容易看出,如下:
POST /api.php HTTP/1.1 Host: localhst Cache-Control: no-cache Content-Type: multipart/form-data; boundary=----FormBoundary------FormBoundary Content-Disposition: form-data; name="file"; filename="file.png" Content-Type: image/png<图片二进制内容> ------FormBoundary Content-Disposition: form-data; name="param1"value1 ------FormBoundary Content-Disposition: form-data; name="param2"value2 ------FormBoundary--

消息体什么意思呢,如果你自行想使用代码实现文件上传,要根据定义自行封装HTTP消息,接下去我们简单描述一下。
Content-Type: multipart/form-data; boundary=----FormBoundary 表示要上传附件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由------FormBoundary开始,以------FormBoundary结尾。每一个表单项又由Content-Type和Content-Disposition组成。
------FormBoundary Content-Disposition: form-data; name="param1"value1 ------FormBoundary

表示普通的一个表单元素,最重要的是理解 Content-Disposition HTTP 消息头,其中第一个参数总是固定不变的form-data,name表示表单元素属性名,回车换行符后面的内容就是元素的值。
接下去重点描述和文件有关的:
------FormBoundary Content-Disposition: form-data; name="file"; filename="file.png" Content-Type: image/png<图片二进制内容> ------FormBoundary

其中多了一个filename参数,表示文件名,Content-Type 告诉服务器这是一个图片,内容就是图片的二进制数据。
其实Content-Disposition这个HTTP header头用途也很广泛,在本文就不重点描述了。
其实要自行封装文件上传,最好的办法就是用自己熟悉的开发语言实现一下,这样印象才更深刻,希望这篇文章对你有用。
我最近写了一本新书《深入浅出HTTPS:从原理到实战》,本书github地址是 https://github.com/ywdblog/httpsbook,大家可以一起讨论本书。本书豆瓣地址 https://book.douban.com/subject/30250772/,如果你读了本书,还请在豆瓣写个评论。或者关注我的公众号(ID:yudadanwx,虞大胆的叽叽喳喳),我会分享一些原创文章(这篇文章有彩蛋哦,可能只有我自己才能知道了:))。

    推荐阅读