文件上传流从哪里获取内容?

7

我有一个与文件上传相关的问题,这更关注于它是如何工作而不是代码问题。我在互联网上搜索了一下,但没有找到合适的答案。

我有一个运行在Tomcat上的Web应用程序,它通过Servlet来处理文件上传。假设我现在想要上传大文件(> 1 Gb)。我的理解是一旦整个文件实际传输完成后,HTTP请求的多部分内容就会在我的servlet中可用。

我的问题是请求的内容实际上存储在哪里?当调用HttpServletRequest.getParts()时,Part对象上有一个InputStream可用。然而,流从哪里读取?Tomcat是否把它存储在某个地方?

我想这可能不够清楚,如果有的话,我会根据您的评论更新帖子。

谢谢


1
这不是直接的答案,但对于那么大的文件,HTTP并不是最好的协议。Base 64编码进一步增加了文件大小,并且不支持恢复中断的上传。对于快速网络上的企业间交流来说还不错,但家庭用户上传1GB文件,如果在几小时的上传过程中出现短暂的网络中断,会感到非常不满意。 - Tim B
5个回答

9

Tomcat将Part存储在“X:\some\path\Tomcat 7.0\temp”(/some/path/apache-tomcat-7.0.x/temp)目录中。

当解析多部分请求时,如果单个部分的大小超过阈值,则会为该部分创建临时文件。

当所有部分的传输完成后,您的servlet/jsp将被调用。

当请求被销毁时,所有临时文件也将被删除。

如果您对多部分解析阶段感兴趣,请查看apache commons-fileupload(特别是ServletFileUpload.parseRequest()),Tomcat基于其变体。

更新

您可以将其配置为Java参数,例如在Windows中:

enter image description here


我找不到路径可以在哪里配置。你知道吗? - benjamin.d
1
在web.xml文件中或使用@MultiPartConfig注释,使用<multipart-config>元素并设置“location”会更加容易。这使您能够基于每个servlet设置目录路径,而不是更改整个服务器的配置,从而写入文件。 - Christopher Schultz
当请求被销毁时,所有临时文件也将被删除。这是否有记录? - Greg Brown
@GregBrow 我不知道这是否有记录,我认为它不是规范的一部分,而是由实现决定。对于Tomcat,有两种不同的“清理”机制:Request.recycle() 和 FileCleaningTracker(请参见下一个评论,太长了)。 - Michele Mariotti
@GregBrown FileCleaningTrackerDiskFileItemFactory.createItem() 使用。 - Michele Mariotti
谢谢,Michele - 这很有道理。我希望 API 文档能更清楚地说明应该如何处理这种情况。这似乎不是应该特定于实现的东西。 - Greg Brown

4
InputStream通常会从临时文件中读取,该临时文件是在请求期间由多部分框架创建的。这个临时文件通常存储在应用程序服务器的临时区域中,由servlet上下文属性javax.servlet.context.tempdir指定。在Tomcat中,这个位置在$CATALINA_HOME/work下。一旦请求完成,该文件将被删除。
对于小文件大小,多部分框架可能会将整个上传保存在内存中,此时InputStream将直接从内存中读取。
如果您正在使用Spring的CommonsMultipartResolver,则可以通过maxInMemorySize属性设置允许在内存中上传的最大大小。如果上传的文件大于此大小,则会将其存储为临时文件。

感谢您的回答。我更感兴趣的是“框架”之前的部分。我猜它们都依赖底层的servlet api方法(至少现在是这样)。当您从流中读取时,数据必须“在servlet api实现的某个地方”。那么Tomcat是否提供了一个流到文件的功能,在框架启动之前将其存储起来?希望这有意义。 - benjamin.d
我猜Tomcat只是将整个HttpServletRequest传递给多部分框架,然后框架依赖于HttpServletRequest.getInputStream(),该方法会读取请求的正文(可能从低级套接字流式传输)。但是,我不确定。需要深入挖掘源代码才能确定。 - Will Keeling
但如果内容是流式传输的,为什么浏览器要等待整个文件上传完毕呢? - benjamin.d
@benjamin.d - 可能浏览器正在等待服务器返回响应?应用程序接收到图像后会怎样处理呢?如果涉及耗时处理,则需要在单独的线程中处理,以便服务器可以立即返回“已收到文件”的响应。 - SergeyB
@benjamin.d 你应该了解一下MultipartConfig(或<multipart-config>)。你可以控制文件是写入磁盘还是仅在内存中缓冲。 - Christopher Schultz

2
我认为我们应该暂停一下,考虑一下网络基础设施。首先,HTTP传输文本数据,因此将二进制信息编码为base 64,以避免数据混乱。这最终导致大量的数据,从而产生了多部分表单,它将数据拆分为带有特殊标记的编码文本部分,使服务器可以将所有内容组装在一起。但是要使用这些数据,我们必须首先对其进行解码,并且我必须使用表单的多个部分才能完成此操作。
[让我们休息一下]
接着,浏览器需要发送大量数据(如您在示例中提到的1GB),该数据使用base64进行编码,然后分成多个片段(多部分表单)并带有其标记,然后浏览器开始将这些片段发送到服务器,但是服务器只有在接收和处理HTTP请求后才返回HTTP响应(或者如果超时,则在浏览器屏幕上出现错误)。
这里可以假设的是,Tomcat可以(我没有检查内部)开始解码已经到达的每个多部分的一部分(从临时文件或内存中),将输入流传递给用户,因为输入流读取是阻塞操作,服务器会等待下一个数据片段传递给Tomcat,然后Tomcat会将其传递给处理数据的程序。

一旦所有数据都到达服务器,程序将准备好响应,Tomcat将返回给浏览器完成HTTP请求-响应循环并关闭连接(因为HTTP是无连接协议)。

希望这有所帮助 :)


2
Tomcat遵循Servlet 3.0规范,允许您指定一些内容,例如在存储(临时)在磁盘上之前multipart“part”的大小限制、将写入哪些临时文件、文件的最大大小以及整个请求的最大大小。您可以在此处找到有关配置multipart上传(在Tomcat或任何其他符合规范3.0的服务器中)的各种良好信息这里这里
Tomcat的实现细节并不是非常重要:它遵循规范。如果要上传的文件小于设置的阈值,则应该能够从内存中读取文件的字节(即没有涉及磁盘)。如果文件较大,则会首先(完全)写入磁盘,然后您可以从容器获取字节。
因此,如果您想接收1GiB文件并且没有那种可用的内存(我不建议允许客户端为每个上传填充堆积的1GiB数据...如果您只开始几个同时进行的1GiB上传,则易受DoS攻击,而且您就无法使用),那么Tomcat(或您正在使用的任何容器)将把该文件(再次完全)读入磁盘,当您的servlet获得控制权时,您可以从该文件中读取字节。
请注意,在任何代码真正运行之前,容器必须处理整个multipart请求。这是为了防止您通过部分读取请求的InputStream或类似方法来破坏任何内容。处理multipart请求是非常棘手的,并且很容易出问题。
如果您想能够流式处理大型文件(例如可以串行处理的巨大XML文件),则需要自己处理multipart解析。这样,您就不需要大量堆积缓冲文件,也不需要在开始处理文件之前将文件存储在磁盘上。(如果这是您的用例,则建议使用HTTP PUT或HTTP POST而不是使用multipart请求。)
(值得一提的是,甚至在任何multipart处理规范中都没有提到base64编码。有些人在这里提到了base64,但我从未见过标准Web客户端使用base64上传使用multipart/form-data的文件。HTTP很好地处理二进制上传,谢谢。)

0

这是它

  1. 用户的浏览器组成http多部分请求
  2. 用户操作系统的Tcp/ip堆栈将它们切片成数据包
  3. 互联网上的路由器将这些数据包传递到您的服务器
  4. 您的服务器操作系统的Tcp/ip堆栈获取有效载荷并将其传递给Tcp端口侦听器
  5. Tomcat http连接器从Tcp数据中解码http post请求(源代码位于https://github.com/apache/tomcat/tree/trunk/java/org/apache/coyote
  6. Tomcat http连接器包装一个Http请求,并最终转发到您的servlet(https://github.com/apache/tomcat/blob/trunk/java/org/apache/catalina/connector/Request.java
  7. 在您的代码读取Http请求内容之前和期间,Tomcat将在内部缓冲Http请求正文
  8. 在调用request.getParts()(https://github.com/apache/tomcat/blob/trunk/java/org/apache/catalina/connector/Request.java#L2561)之前,Tomcat不会解析多部分正文,因此不会为调用之前的部分创建临时文件。
  9. Tomcat将上传的文件存储在您的servlet代码中@MultipartConfig注释指向的位置,除非您的代码没有提供它并且设置了allowCasualMultipartParsing(http://tomcat.apache.org/tomcat-7.0-doc/config/context.html#Common_Attributes
  10. 考虑到默认情况下allowCasualMultipartParsing为false,您不必担心Tomcat存储文件的位置,尽管很容易挖掘出来。
我提到1~5是因为理解request.getInputStream()返回的流非常重要,这是Servlet 3.x request.getParts()功能之前所必需的。通常,Tomcat会很快将请求传递给Web应用程序,不需要等待客户端完成上传,因此Tomcat不需要缓冲大量数据。在JSR-000315获批之前,我已经离开了Java服务器端好几年了 :-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接