Tomcat 中是怎么处理文件上传的?
Posted 开发者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat 中是怎么处理文件上传的?相关的知识,希望对你有一定的参考价值。
开发者(KaiFaX)
面向全栈工程师的开发者
专注于前端、Java/Python/Go/php的技术社区
来源链接:https://juejin.cn/post/6955841741349978143
前言
这两天在另一个社区看到了一个关于 Tomcat 的提问,还挺有意思。正好自己之前也没思考过这个问题,今天就结合 Tomcat 机制来聊聊这个“为什么”。 本文对 HTTP 协议中的文件上传标准和 Tomcat 机制的分析内容较多,比较基础,不需要的大佬门可以直接跳到文末。
HTTP 协议中的文件上传
众所周知,HTTP 是一个文本协议,那文本协议如何传输文件呢?
multipart/form-data 方式
HTTP 协议中规定了一种基于表单的文件上传方式(Form-based File Upload)。在 form 中定义一个 ENCTYPE 属性,值为 multipart/form-data,然后增加一个 type 为 file 的 <input>
标签。
<FORM ENCTYPE="multipart/form-data" ACTION="_URL_" METHOD=POST>
File to process: <INPUT NAME="userfile1" TYPE="file">
<INPUT TYPE="submit" VALUE="Send File">
</FORM>
&
符号拼接,并且对key/value 都进行 urlencode 编码
虽然 x-www-form-urlencoded 增加了一步编码的过程,但不会给每个字段增加header,也没有 boundary,报文体积相对 multipart 方式来说小了很多。
binary payload 方式
除了 multipart/form-data之外,还有一种 binary payload 的上传方式。这个 binary payload 是我自己起的名字……因为在 HTTP 协议中并没有找到这种方式的说明(如果有找到的大佬评论区贴个连接),不过很多 HTTP 客户端都支持。
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("image/png");
RequestBody body = RequestBody.create(mediaType, "<file contents here>");
Request request = new Request.Builder()
.url("localhost:8098/upload")
.method("POST", body)
.addHeader("Content-Type", "image/png")
.build();
Response response = client.newCall(request).execute();
这种方式非常简单,就是将整个 payload 部分,都用来存放文件数据。 如下图所示,整个 payload 部分都是文件内容: 这种方式虽然简单,客户端实现也简单,但……服务端没有很好的支持。 比如 Tomcat 中,并不会将这种 binary file 的形式作为文件处理,而是当做普通的报文处理。
Tomcat 处理机制分析
Tomcat 在处理文本形式的报文时,会先读取前面的 Header 部分,解析 Content-Length 来划分报文边界,剩下的 Payload 部分并不会一次性读取,而是包装了一个 InputStream ,在内部调用 Socket read 进行读取 RCV_BUF 的数据(完整报文大小大于 readBuf Size时)
org.apache.tomcat.util.http.fileupload.disk.DiskFileItem
(这个 DiskFileItem 不区分是文件还是文本数据)。DiskFileItem 内又分为 Header 部分和 Content 部分。Content 中一部分存储在内存,剩下的存储至磁盘,通过一个 sizeThreshold 进行分割;
不过这个值默认为0,也就是说默认会把内容部分全部存储至磁盘。
//org.apache.catalina.connector.Request#parseParts
if (part.getSubmittedFileName() == null) {
String name = part.getName();
String value = null;
try {
value = part.getString(charset.name());
} catch (UnsupportedEncodingException uee) {
// Not possible
}
......
parameters.addParameter(name, value);
}
要知道这个 getParameter 是只能获取表单参数(FormParam)和查询参数(QueryString)的,不过 multipart 也是 form,能获取参数好像也没啥毛病……
一个简单的小结
Tomcat 对不同类型的请求处理方式:
-
如果参数是 GET queryString方式(url上拼参数),那么所有参数都在报文头中,会一次性全部读取至内存 -
如果是 POST 类型的报文,Tomcat 只会对读取 Header 部分,Payload 部分不会主动读取,而是将 Socket 包装成一个 InputStream 供应用层 read -
x-www-form-urlencoded 这种类型的报文,虽然不会主动读取,但很多 Web 框架(比如 SpringMVC)会调用 getParameter,还是会出发 InputStream 的read,对 RCV_BUF 进行读取 -
上面提到的 binary payload也是一样,Tomcat 并不会主动发起 read 操作,需要应用层调用 ServletRequest#InputStream 进行 read操作读取 RCV_BUF 的数据 -
multipart 类型的报文,一样不会主动读取,调用HttpServletRequest#getParts 才会触发解析/读取;同样的,很多 Web 框架会调用 getParts,所以会触发解析
为什么要先写入临时文件,直接包装 InputStream 交给应用层读取不行吗?
如果应用层不(及时)读取 RCV_BUF,那么当收到的数据写满 RCV_BUF 时,就不会再返回 ACK 了,客户端的的数据也会存储在 SND_BUF 中,无法继续发送数据,当 SND_BUF 被应用层写满时,这条连接就被阻塞了。
-
接收到完整文件数据,存储至内存中,然后调用对象存储的SDK -
用流的方式,一边 read ServletRequest#InputStream,一边 write 到 SDK 的 OutputStream 中
参考
-
Apache Tomcat (http://tomcat.apache.org/)
-
Form-based File Upload in html - IETF( https://tools.ietf.org/html/rfc1867)
-
《Tomcat 架构解析》 - 刘光瑞 著
以上是关于Tomcat 中是怎么处理文件上传的?的主要内容,如果未能解决你的问题,请参考以下文章
图片怎么上传到tomcat服务器,tomcat服务器如何构建
jsp页面被tomcat引擎运行的时候组装成java片段,但是这些java片段怎么没有main方法作为程序的入口啊?
在Tomcat的安装目录下conf目录下的server.xml文件中增加一个xml代码片段,该代码片段中每个属性的含义与用途