一、基本原理
上传文件的基本流程如下图所示。浏览器端提供了一个表单,在用户提交请求后,将文件数据和其他表单信息 编码并上传至服务器端,服务器端将上传的内容进行解码了,提取出 HTML 表单中的信息,将文件数据存入磁盘或数据库。
二、各过程详解
浏览器编码
复制代码
在向服务器端提交请求时,浏览器需要将大量的数据一同提交给 Server 端, 而提交前,浏览器需要按照 Server 端可以识别的方式进行编码,对于普通 的表单数据,这种编码方式很简单,编码后的结果通常是 field1=value2&field2=value2&… 的形式,如 name=ltq&age=18。通常使用的表单也是采用这种方式编码的,Servlet 的 API 提供了对这种 编码方式解码的支持,只需要调用 ServletRequest 类中的方法就可以得到 用户表单中的字段和数据。
这种编码方式( application/x-www-form-urlencoded )虽然简单,但对于 传输大块的二进制数据显得力不从心,对于传输这类数据,浏览器采用 了另一种编码方式,即 "multipart/form-data" 的编码方式,采用这种方式, 浏览器可以很容易的表单内的数据和文件一起。这种编码方式先定义好 一个不可能在数据中出现的字符串作为分界符,然后用它将各个数据段 分开,而对于每个数据段都对应着 HTML 页面表单中的一个 Input 区,包 括一个 content-disposition 属性,说明了这个数据段的一些信息,如果这个 数据段的内容是一个文件,还会有 Content-Type 属性,然后就是数据本身。 这里,我们可以编写一个简单的 Servlet 来看到浏览器到底是怎样编码的。
实现流程:
- 重载 HttpServlet 中的 doPost 方法
- 调用 request.getContentLength() 得到 Content-Length ,并定义一个与 Content-Length 大小相等的字节数组 buffer 。
- 从HttpServletRequest 的实例 request 中得到一个 InputStream, 并把它读入 buffer 中。
- 打印到控制台,查看上传内容。
代码清单
- 前端代码
复制代码Insert title here
- 服务端代码
package com.itheima.upload;import java.io.IOException;import java.io.InputStream;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class UploadServlet1 extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int len = request.getContentLength(); byte buffer[] = new byte[len]; InputStream in = request.getInputStream(); int total = 0; int once = 0; while ((total < len) && (once >=0)) { once = in.read(buffer,total,len); total += once; } System.out.println(new String(buffer,0,len)); }}复制代码
在使用
这里 ------WebKitFormBoundaryMsG9gupKbNVAw2Dn
就是浏览器指定的分界符,不同的浏览器有不同的确定分界符的方法,但都需要保证分界符不会在文件内容中出现.
------WebKitFormBoundaryMsG9gupKbNVAw2Dn
Content-Disposition: form-data; name="name"
李同钱
------WebKitFormBoundaryMsG9gupKbNVAw2Dn
Content-Disposition: form-data; name="photo"; filename="robots.txt"
Content-Type: text/plain
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:
------WebKitFormBoundaryMsG9gupKbNVAw2Dn--
以上是上传输出内容
浏览器采用默认的编码方式是 application/x-www-form-urlencoded , 可以通过指定 form 标签中的 enctype 属性使浏览器知道此表单是用 multipart/form-data 方式编码如:
<form enctype="multipart/form-data" action="${pageContext.request.contextPath}/servlet/uploadServlet2" method="post" > |
提交请求
提交请求的过程由浏览器完成的,并且遵循 HTTP 协议,每一个从浏览 器端到服务器端的一个请求,都包含了大量与该请求有关的信息, 在 Servlet 中,HttpServletRequest 类将这些信息封装起来,便于我们提取 使用。在文件上载和表单提交的过程中,有两个指的关心的问题,一是 上载的数据是是采用的那种方式的编码,这个问题的可以从 Content-Type 中得到答案,另一个是问题是上载的数据量有多少即 Content-Length , 知道了它,就知道了 HttpServletRequest 的实例中有多少数据可以读取 出来。这两个属性,我们都可以直接从 HttpServletRequest 的一个实例 中获得,具体调用的方法是 getContentType() 和 getContentLength() 。
Content-Type 是一个字符串,在上面的例子中,增加
System.out.println(request.getContentType()); |
可以得到这样的一个输出字符串:
multipart/form-data; boundary=----WebKitFormBoundaryLJzBFw0CbuD1LLFn |
前半段正是编码方式,而后半段正是分界符,通过 String 类中的方法, 我们可以把这个字符串分解,提取出分界符。
String contentType=request.getContentType();int start=contentType.indexOf("boundary=");int boundaryLen=new String("boundary=").length();String boundary=contentType.substring(start+boundaryLen);boundary="--"+boundary;复制代码
解码
经过以上的流程, 我们可以得到一个包含有所有上载数据的一个字节数组和一个分界符, 而我们要得到以下内容:
- 提交的表单中的各个字段以及对应的值
- 如果表单中有 file 控件,并且用户选择了上传文件, 则需要分析出字段的名称、文件在浏览器端的名字、文件的 Content-Type 和文件的内容。
字节数组的内容可以分解如下:
字节中上传的内容可能包括file类型的和普通表单类型,两种类型DATA存在的字段存在一些差异
具体解码过程也可以分为两个步骤:
将上传的数据分解成数据段,每个数据段对应着表单中的一个 Input 区。对每个数据段,再进行分解,提出上述要求得到的内容。
通过分界符boundary,提出DATA,
实现将boundary转换成buffer,与传输的Buffer循环比较找到相同的块,切割出DATA,
在递归DATA比较其中的字段,提出对应的字段,保存在集合中
https://www.ibm.com/developerworks/cn/java/fileup