tomcat如何处理文件上传的
上周同事有个需求,有个web页面支持升级的逻辑的,但升级包的大小比较大(接近2G)。由此引申出来上传前需要判断磁盘是否足够的问题。此时就产生了如下几个问题
tomcat如何处理文件上传的是否使用磁盘文件,猜测必然使用磁盘文件,不然文件大时内存不足的。
那么磁盘文件是如何创建,如何删除,系统异常重启后是否会删除?
文件上传协议
协议基础
查看rfc文档(http://www.faqs.org/rfcs/rfc1867.html),该文档描述了带文件的form表单提交的要求。该提案对HTML进行了两项更改:
为INPUT的TYPE属性添加FILE选项。
允许INPUT标签使用ACCEPT属性,该属性是输入所允许的媒体类型或类型模式
此外,它定义了新的MIME媒体类型multipart / form-data,并指定了HTML用户代理在使用ENCTYPE =“ multipart / form-data”和/或<INPUT type =“ file”>解释表单时的行为标签。
协议格式
该文档侧重于tomcat如何处理文件上传请求的,对协议的详细信息不做过多介绍,有想了解的参考rfc文档。更多侧重于协议content内容的格式介绍
简介http数据格式
http数据包中包含3个部分:
请求行
格式为:method url version \r\n (GET /manager/images/asf-logo.svg HTTP/1.1\r\n)
method:http请求类型,常见的有GET, POST, PUT,DELETE, OPTION 等
url:指定要访问的网络资源,如 /manager/images/asf-logo.svg
version:http协议的版本
请求头
格式为:Key: value \r\n (Content-Type: text/plain\r\n)
请求头中包含很多有关客户端的环境和请求正文的有用信息,例如常见的Content-Type,Content-Length等
请求头中包含多行请求信息
请求体
代表请求中包含的信息,如form表单提交的时候携带的信息,本文描述的文件上传的文件信息等
根据上面的介绍我们看个请求的样例:
Content-Length为45,由于请求体为:deployPath=123&deployConfig=123&deployWar=123 长度就是45。
Content-Type为 application/x-www-form-urlencoded 这个请求是一个普通的form表单的提交。
form-data的content格式
上面介绍了通用的http请求的数据格式, 我们看看form-data的数据请求格式。从rfc文档中可以看到,form-data的请求可以归结为如下几点:
请求头中Content-Type格式为 Content-type: multipart/form-data, boundary=AaB03x
boundary要求需要在任何一部分的数据内容中都不能包含
请求体中可以包含多部分,每部分的格式如下:
1
2
3
4boundary (http请求头中的boundary)
header (当前请求部分的头信息,与http头格式一致)
\r\n
data (实际的数据信息)
从上述介绍引申出如下几个问题:
tomcat是如何解析,请求的content信息的
如果每一部分的请求数据中包含了boundary的请求下会出现什么问题
tomcat解析流程
核心类简介
MultipartConfigElement, 上传文件的配置信息。通常如果是处理文件上传的servlet中我们会配置一些参数,最终会解析为一个MultipartConfigElement对象。配置如下:
1
2
3
4
5
6<multipart-config>
<location></location>
<max-file-size>10737418240</max-file-size>
<max-request-size>10737418240</max-request-size>
<file-size-threshold>1048576</file-size-threshold>
</multipart-config>
FileItem, 由于一个请求中可以包含多份数据,在tomcat中就将每一部分数据定义个一个FileItem对象
DiskFileItemFactory, 顾名思义,该类为了生成一份份FileItem对象
ServletFileUpload, 该类为解析content内容的入口,尤其基类FileUploadBase进行实际的业务处理
FileItemIterator,使用迭代器的方式,将content中的每一部分组装为一个迭代器,依次遍历下来
查看tomcat源码
1、tomcat源码中,全局搜索multipart/form-data,可以在Request中搜索到Request类中有个方法parseParameters,顾名思义是解析参数的。中间有一段判断,如果contentType为multipart/form-data的情况下会调用parseParts方法,解析请求中的每一部分。
1 |
if ("multipart/form-data".equals(contentType)) { |
2、接着我们看看parseParts方法中实现,主要看核心的部分,总结来说该方法的主要步骤包括如下几步
从请求上下文中获取的MultipartConfigElement信息
创建DiskFileItemFactory对象
创建ServletFileUpload对象
调用ServletFileUpload的parseRequest对请求进行解析,获取到请求内容
1 |
// 获取配置信息 |
3、关注ServletFileUpload的parseRequest方法内的实现,依旧关注核心代码的实现,总结来说该方法的实现包括如下几步。
通过request信息获取到FileItemIterator的一个迭代器的对象
遍历迭代器,将迭代器的header信息组装为一个个FileItem对象
将每一部分的数据copy到fileItem的数据中
1 |
//创建FileItemIterator对象 |
3、关注迭代器的实现, 一般来说迭代器的会包含hasNext以及next方法,我们关注下迭代器的创建以及hasNext和next的方法,首先看看迭代器的创建的实现, 为了简化代码省略了部分异常的处理。可以把其整理为如下几步
检查请求体的长度是否符合要求,此处的sizeMax是最初从MultipartConfigElement中取得的
将请求上下文的inputstream转化为MultipartStream对象
调用findNextItem方法
1 |
FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException { |
4、创建迭代器的关键就到了findNextItem的实现逻辑了, 简化该方法的实现,其逻辑包括如下几个流程。
读取Boundary,即将输入流中的boundary读取指针调整到header中
解析当前部分的header信息内容
将header信息组装为一个FileItemStreamImpl对象
1 |
//读取Boundary |
5、接着我们需要考虑下读取Boundary的逻辑,skipPreamble和readBoundary的差别不大,差别主要在于是否需要先处理掉Boundary之前的数据。正常来说,调用到此处之前已经把boundary之前的数据消费了。skipPreamble中同样调用的有readBoundary数据, 我们就侧重于关注下skipPreamble的实现,核心分为函数computeBoundaryTable, discardBodyData, 和 readBoundary。按照我们协议介绍的,不同的数据段之间是使用boundary进行分割的,那么实现逻辑自然就是从数据流中查找与boundary相匹配的字符串段,字符串匹配的算法有很多,tomcat选择的是KMP算法,具体的逻辑可以简述为:
生成kmp算法的next数组
读取到boundary的位置
读取boundary以及换行符
1 |
computeBoundaryTable() |
6、从上述介绍中我们可以大致了解到数据包中的每一部分的解析逻辑,简单介绍为按照boundary的值对数据流进行分割,分割为一段一段的数据处理, 引题的问题,就涉及到了我们对每一部分数据的处理逻辑。那么这片的逻辑如何实现的, 从第3点我们看到的其中逻辑最后一步是将每一部分的数据copy到fileItem的数据中, 那么我们就需要看fileItem中是如何处理数据流的, copy到数据流中,我们就需要看每一部分的read的函数实现以及FileItem的数据流的write的方法的实现:
read的实现,相当于将socket中的数据读取到数组中,是否读取完成是以数据是否读取到boundary为止
write的实现,先check数据是否达到了Threshold(逻辑),如果达到了上限那么将output由内存转换为FileOutputstream
Threshold 实际读取源码可以看到DeferredFileOutputStream的sizeThreshold,最终还是通过MultipartConfigElement中获取的。
1 |
// read的实现 |
7、总结下实现流程
解析到http协议的数据内容时,将content解析为一个个数据部分
每个部分的分割符为header中的boundary信息(数据报文中可能会调整boundary),boundary的匹配是通过KMP算法进行查询的
读取数据的时候优先存储在内存中,如果数据超过MultipartConfigElement的sizeThreshold配置时在读取后将数据存储在磁盘中。
引题问题
1、是否使用磁盘文件
答案:会使用磁盘文件,磁盘文件根据配置的location,如果未配置取默认配置,默认在tomcat工作目录下的work/Catalina下对应host下的,项目目录中。如E:\java\catalina-home\work\Catalina\localhost\manager。其中E:\java\catalina-home是tomcat的启动目录中
2、如何创建,如何删除,系统异常重启后是否会删除
答案: 创建在生成FIleItem的时候会同步生成文件,文件名的是格式为String.format(“upload_%s_%s.tmp”, UID, getUniqueId())。
删除时机:整个service处理完成后会进行删除,Request类中有个recycle的方法,负责资源的释放。
异常重启:此种逻辑tomcat原生的逻辑确实没有进行处理,那么就需要我们根据实际的情况,如果需要处理此种情况的话,需要我们额外的处理进行相应的额外
3、tomcat是如何解析,请求的content信息的
答案:tomcat原生的解析逻辑就是以boundary做为分割符进行分割的数据段的,使用的方法是KMP算法。
4、如果每一部分的请求数据中包含了boundary的请求下会出现什么问题
答案:从源码层面来看,如果数据中包含boundary的情况下确实会影响的数据的解析的,因此说在rfc文档中对于这种情况也有描述,尽可能选择不会出现在文件中的boundary
5、默认实现的思考
答案:上传数据时会遍历全部的数据信息,以此进行相应的算法匹配,效率确实一般。如果时每一部分中包含数据包的长度的情况下会不会更好点,解析的时候就不需要对每一部分的数据进行遍历匹配?