`
popwang
  • 浏览: 59820 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论
阅读更多

用 Servlet 进行上载的原理和实现

 

李常庚 (coid@sina.com ), 自由撰稿人

简介:  Servlet 是用 Java 编写的、协议和平台都独立的服务器端组件,使用请求/响应的模式,提供了一个基于 Java 的服务器解决方案。使用 Servlet 可以方便地处理在 HTML 页面表单中提交的数据,但 Servlet 的 API 没有提供对以 mutilpart/form-data 形式编码的表单进行解码的支持,因而对日常应用中经常涉及到到文件上传等事务无能为力。本文将从文件传输的基本原理入手,分析如何用 Servlet 进行文件的上传,并提出解决方案。

本文的标签:   _unassigned , best_practices , 应用开发

发布日期:  2001 年 3 月 01 日
级别:  初级
访问情况  1520 次浏览
建议:  0 (添加评论 )

1 star 2 stars 3 stars 4 stars 5 stars 平均分 (共 4 个评分 )

一、基本原理

通过 HTML 上载文件的基本流程如下图所示。浏览器端提供了供用户选择提交内 容的界面(通常是一个表单),在用户提交请求后,将文件数据和其他表单信息 编码并上传至服务器端,服务器端(通常是一个 cgi 程序)将上传的内容进行解 码了,提取出 HTML 表单中的信息,将文件数据存入磁盘或数据库。


二、各过程详解

A)填写表单并提交

通过表单提交数据的方法有两种,一种是 GET 方法,另一种是 POST 方法, 前者通常用于提交少量的数据,而在上传文件或大量数据时,应该选用 POST 方法。在 HTML 代码中,在 <form> 标签中添加以下代码可以 页面上显示一个选择文件的控件。

<input type="file" name="file01">

 

在页面中显示如下(可能随浏览器不同而不同)

可以直接在文本框中输入文件名,也可以点击按钮后弹出供用户选择文件的对话框。

B)浏览器编码

在向服务器端提交请求时,浏览器需要将大量的数据一同提交给 Server 端, 而提交前,浏览器需要按照 Server 端可以识别的方式进行编码,对于普通 的表单数据,这种编码方式很简单,编码后的结果通常是 field1=value2&field2=value2&… 的形式,如 name=aaaa&Submit=Submit。这种编码的具体规则可以在 rfc2231 里查到, 通常使用的表单也是采用这种方式编码的,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 中。
  • 使用 FileOutputStream 将 buffer 写入指定文件。


代码清单

// ReceiveServlet.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
//示例程序:记录下Form提交上来的数据,并存储到Log文件中
public class  ReceiveServlet extends HttpServlet
{
    public void doPost(HttpServletRequest request,HttpServletResponse response)
    throws IOException, ServletException
    {
//1
        int len = request.getContentLength();
        byte buffer[] = new byte[len];
//2
        InputStream in = request.getInputStream();
        int total = 0;
        int once = 0;
        while ((total < len) && (once >=0)) {
            once = in.read(buffer,total,len);
            total += once;
        }
//3
        OutputStream out=new BufferedOutputStream(
            new FileOutputStream("Receive.log",true));
        byte[] breaker="\r\nNewLog: -------------------->\r\n".getBytes();
        System.out.println(request.getContentType());
        out.write(breaker,0,breaker.length);
        out.write(buffer);
        out.close();
    }
}

 

在使用 Opera 作为浏览器测试时,从指定的文件( Receive.log )中可以看到如下的内容

--_OPERAB__-T/DQLi2fn47+D52OOrpdrz
Content-Disposition: form-data; name="id"
id00
--_OPERAB__-T/DQLi2fn47+D52OOrpdrz
Content-Disposition: form-data; name="file3"; filename="Autoexec.bat"
Content-Type: application/octet-stream
@echo off
prompt $d $t [ $p ]$_$$
--_OPERAB__-T/DQLi2fn47+D52OOrpdrz--

 

这里 _OPERAB__-T/DQLi2fn47+D52OOrpdrz 就是浏览器指定的分界符,不同的浏览器有不同的确定分界符的方法,但都需要保证分界符不会在文件内容中出现。下面是用 IE 进行测试的结果

-----------------------------7d137a26e18
Content-Disposition: form-data; name="name"
123
-----------------------------7d137a26e18
Content-Disposition: form-data; name="introduce"
I am...
  I am..
-----------------------------7d137a26e18
Content-Disposition: form-data; name="file3"; filename="C:\Autoexec.bat"
Content-Type: application/octet-stream
@echo off
prompt $d $t [ $p ]$_$$
SET PATH=d:\pf\IBMVJava2\eab\bin;%PATH%;D:\PF\ROSE98I\COMMON
-----------------------------7d137a26e18--

 

这里 ---------------------------7d137a26e18 作为分界符。 关于分界符的规则可以概况为两条:

  • 除了最后一个分界符,每个分界符后面都加一个 CRLF 即 '\u000D' 和 '\u000A', 最后一个分界符后面是两个分隔符"--"
  • 每个分界符的开头也要加一个 CRLF 和两个分隔符("-")。

浏览器采用默认的编码方式是 application/x-www-form-urlencoded , 可以通过指定 form 标签中的 enctype 属性使浏览器知道此表单是用 multipart/form-data 方式编码如:

< form action="/servlet/ReceiveServlet" ENCTYPE="multipart/form-data" method=post >

 

C)提交请求

提交请求的过程由浏览器完成的,并且遵循 HTTP 协议,每一个从浏览 器端到服务器端的一个请求,都包含了大量与该请求有关的信息, 在 Servlet 中,HttpServletRequest 类将这些信息封装起来,便于我们提取 使用。在文件上载和表单提交的过程中,有两个指的关心的问题,一是 上载的数据是是采用的那种方式的编码,这个问题的可以从 Content-Type 中得到答案,另一个是问题是上载的数据量有多少即 Content-Length , 知道了它,就知道了 HttpServletRequest 的实例中有多少数据可以读取 出来。这两个属性,我们都可以直接从 HttpServletRequest 的一个实例 中获得,具体调用的方法是 getContentType() 和 getContentLength() 。

Content-Type 是一个字符串,在上面的例子中,增加

System.out.println(request.getContentType());

 

可以得到这样的一个输出字符串:

multipart/form-data; boundary=---------------------------7d137a26e18

 

前半段正是编码方式,而后半段正是分界符,通过 String 类中的方法, 我们可以把这个字符串分解,提取出分界符。

String contentType=request.getContentType();
int start=contentType.indexOf("boundary=");
int boundaryLen=new String("boundary=").length();
String boundary=contentType.substring(start+boundaryLen);
boundary="--"+boundary;

 

判断编码方式可以直接用 String 类中的 startsWith 方法判断。

if(contentType==null || !contentType.startsWith("multipart/form-data"))

 

这样,我们在解码前可以知道:
编码的方式是否是multipart/form-data
数据内容的分界符
数据的长度

我们可以用类似于 ReceiveServlet 中的方式将这个请求的输入流读 入一个长度为 Content-Length 的字节数组,接下来就是将这个字节数组里 的内容全部提取出来了。

D)解码

解码对我们来说是整个上载过程最繁琐的一个步骤,经过以上的流程, 我们可以得到一个包含有所有上载数据的一个字节数组和一个分界符, 通过对 Receive.log 分析,还可以得到每个数据段中的分界符。 而我们要得到以下内容:

  • 提交的表单中的各个字段以及对应的值
  • 如果表单中有 file 控件,并且用户选择了上载文件, 则需要分析出字段的名称、文件在浏览器端的名字、文件的 Content-Type 和文件的内容。

字节数组的内容可以分解如下:


具体解码过程也可以分为两个步骤:

  • 将上载的数据分解成数据段,每个数据段对应着表单中的一个 Input 区。
  • 对每个数据段,再进行分解,提出上述要求得到的内容。

这两个步骤主要的操作有两个,一个是从一个数组中找出另一个数组的位置,类似于 String 类中的 indexOf 的功能,另一个是从一个数组中提取出另一个数组, 类似于 String 类中的 substring 的功能,为此我们可以专门写两个方法,实现这种功能。

int byteIndexOf (byte[] source,byte[] search,int start)
byte[] subBytes(byte[] source,int from,int end)

 

为了便于使用,可以从这两个方法中衍生出下列方法

int byteIndexOf (byte[] source,String search,int start)   以一个 String 作为搜索对象参数
String subBytesString(byte[] source,int from,int end)     直接返回一个 String
int bytesLen(String s)                  返回字符串转化为字节数组后,字节数组的长度

 

这样,从一个字节数组中,根据标记提取出另一个字节数组可以表示如下:


假设我们已经将数据存入字节数组 buffer 中,分界符存入 String boundary 中

       int pos1=0;            //pos1 记录 在buffer 中下一个 boundary 的位置
                                     //pos0,pos1 用于 subBytes 的两个参数
        int   pos0=byteIndexOf(buffer,boundary,0);
                                     //pos0 记录 boundary 的第一个字节在buffer 中的位置
        do
        {
            pos0+=boundaryLen;
//记录boundary后面第一个字节的下标
            pos1=byteIndexOf(buffer,boundary,pos0);
            if (pos1==-1)
                break;
            pos0+=2;          //考虑到boundary后面的 \r\n
            PARSE[(subBytes(buffer,pos0,pos1-2));]
                                   //考虑到boundary后面的 \r\n
            pos0=pos1;
        }while(true);
       

 

其中 PARSE 部分是对每一个数据段进行解码的方法,考虑到 Content-Disposition 等属性,首先定义一个 String 数组

   String[] tokens={"name=\"",
    "\"; filename=\"",
    "\"\r\n",
    "Content-Type: ",
    "\r\n\r\n"
    };
   

 

对于一个不是文件的数据段,只可能有 tokens 中的第一个元素和最后一个元素,如果是一个文件数据段,则包含所有的元素。第一步先得到 tokens 中每个元素在这个数据段中的位置

       int[] position=new int[tokens.length];
        for (int i=0;i < tokens.length ;i++ )
        {
            position[i]=byteIndexOf(buffer,tokens[i],0);
        }

 

第二步判断是否是一个文件数据段,如果是一个文件 数据段则 position[1] 应该大于0,并且 postion[1] 应该小于 postion[2] 即 position[1] > 0 && position[1] < position[2] 如果为真,则为一个文件数据段,

1.得到字段名
String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[1]);
2.得到文件名
String file= subBytesString(buffer,position[1]+bytesLen(tokens[1]),position[2]);
3.得到 Content-Type
String contentType=subBytesString(buffer,position[3]+bytesLen(tokens[3]),position[4]);
4.得到文件内容
byte[] b=subBytes(buffer,position[4]+bytesLen(tokens[4]),buffer.length);
否则,说明数据段是一个 name/value 型的数据段,
且name 在 tokens[0] 和 tokens[2] 之间,value 在 tokens[4]之后
//1.得到 name
String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[2]);
//2.得到 value
String value= subBytesString(buffer,position[4]+bytesLen(tokens[4]),buffer.length);

 

三、具体实现

为便于使用,定义 upload 包,包括以下类:
ContentFactory
对从 client 中传来的数据进行解码,并提供一系列 get 方法,从中得到上传的各种信息。
具体接口如下

staticContentFactory getContentFactory (javax.servlet.http.HttpServletRequestrequest)
返回根据当前请求生成的一个 ContentFactory 实例
staticContentFactory getContentFactory (javax.servlet.http.HttpServletRequestrequest, intmaxLength)
返回根据当前请求生成的一个 ContentFactory 实例
FileHolder getFileParameter (java.lang.Stringname)
返回一个 FileHolder 实例,该实例包含了通过字段名为 name 的 file 控件上载的文件信息, 如果不存在这个字段或者提交页面时,没有选择上载的文件,则返回 null。
java.util.Enumeration getFileParameterNames ()
返回一个 由 String 对象构成的 Enumeration ,包含了 Html 页面 窗体中所有 file 控件的 name 属性。
FileHolder[] getFileParameterValues (java.lang.Stringname)
返回一个 FileHolder 数组,该数组包含了所有通过字段名为 name 的 file 控件上载的文件信息, 如果不存在这个字段或者提交页面时,没有选择任何上载的文件,则返回一个 零元素的数组(不是 null )。
java.lang.String getParameter (java.lang.Stringname)
String 类型返回请求的参数的值,如果该参数不存在,则返回为 null 。参数存于提交的表单数据中。
java.util.Enumeration getParameterNames ()
返回一个 String 类型的 Enumeration 对象,该对象包含了所有提交请求的参数名称。
java.lang.String[] getParameterValues (java.lang.Stringname)
返回 String 类型的数组,该数组包含了指定名称的参数对应的所有的值,如果参数不存在,则返回为 null

FileHolder
封装一个文件数据段,可以从中提取文件名, Content-Type 和文件内容等属性。 接口如下:

byte[] getBytes ()
返回一个文件内容的字节数组
java.lang.String getContentType () 返回该文件的 Content-Type
java.lang.String getFileName ()
返回该文件在文件上载前在客户端的名称
java.lang.String getParameterName ()
返回上载该文件时,Html 页面窗体中 file 控件的 name 属性
void saveTo (java.io.Filefile)
把文件的内容存到指定的文件中
void saveTo (java.lang.Stringname)
把文件的内容存到指定的文件中

ContentFactoryException
在 ContentFactory.getContentFactory 方法中可能抛出。
各类的源文件详解代码清单。

四、使用示例

附录中包含了一个 Servlet 示例,该示例重载了 HttpServlet 的两个方法 ( doGet, doPost ),在浏览器发送 GET 请求时,产生一个表单,在用户提交表单时, 将文件和数据上载,并在浏览器端显示出上载文件存盘后的 URL , 以及页面中的各字段的 name 和 value 。该示例及各类在Windows98、jdk1.3和tomcat3.1, 浏览器为IE5和Opera3.6的环境下调试通过。

五、附录

 

参考资料

  • RFC1867 Form-based File Upload in HTML

  • RFC2045/2046 MIME (Multipurpose Internet Mail Extensions)

  • RFC1806 The Content-Disposition Header

  • RFC2388 Returning Values from Forms: multipart/form-data
分享到:
评论
1 楼 cnluoxh 2011-11-02  
很不错,分析得很透彻,学些了

相关推荐

    servlet-api-3.0.jar包

    7. **MIME多部分解析**:Servlet 3.0引入了`Part`接口,用于处理MIME多部分请求,这在处理上传文件时非常有用。 8. **安全增强**:添加了新的安全相关API,如`HttpServletRequest.isUserInRole()`和`...

    servlet-api-3.0-alpha-1.jar

    开发者可以使用@MultipartConfig注解来处理多部分请求,例如文件上传,无需额外的库。 总之,Servlet API 3.0-alpha-1.jar为Web开发带来了许多增强功能和改进,包括更好的依赖管理、异步处理、更简洁的配置方式...

    servlet-api-src.jar

    通过学习和使用Servlet API,开发者可以创建处理各种HTTP请求的服务器端应用,包括登录验证、数据处理、文件上传下载等。同时,Servlet API还与Java的JSP(JavaServer Pages)技术紧密结合,使得开发者可以在HTML...

    servlet-3_1-final(中文版).doc

    《Java Servlet 3.1 规范》是中国Java开发者学习Servlet技术的重要参考资料,它详细阐述了Servlet 3.1版本中的各项特性与规范。该文档旨在辅助英文版的学习,帮助开发者更深入地理解和应用Servlet技术。 Servlet是...

    jsp-api.jar(2.3)与servlet-api.jar (3.1)

    6. **多文件上传**:在Servlet 3.1中,可以通过单个`Part`接口处理多个文件上传,使得文件上传更加方便。 7. **URL路径匹配**:增强的URL映射规则允许更灵活的路由策略,可以使用通配符和正则表达式进行路径匹配。 ...

    servlet-api 3.0版本

    今天找servlet-api 3.0的找疯了,网上都找不到,最后在tomcat7里面拉了一个出来了,上传给大家共享一下吧,哈哈

    servlet-api 集合

    2.2版本添加了对Multipart/form-data的支持,使得文件上传成为可能。此外,还提供了对HTTPS的支持,增强了Web应用的安全性。 2. **Servlet-API 2.3**:发布于2000年,这个版本引入了Servlet Filter和Servlet ...

    servlet-api.jar+SmartUpload.jar

    Servlet-API.jar和SmartUpload.jar是两个在Web开发中常见的库文件,主要应用于Java Servlet环境,尤其是处理文件上传功能。在本文中,我们将深入探讨这两个组件以及它们在IT行业的应用。 Servlet-API.jar是Java ...

    servlet-api.jar

    servlet-api.jar 发现经常需要找这个文件,所以就上传了.希望对大家有用处

    servlet-3_0-final-spec.pdf

    - **文件上传**:Servlet 3.0改进了文件上传的功能,提供了一个新的API来处理多部分表单数据。 - **实现**:使用`Part`接口,开发者可以直接处理上传的文件,而无需使用第三方库。 ##### 5. HTTP Upgrade机制 - **...

    servlet-2.4规范

    ### servlet-2.4规范详解 #### 一、概述 `Servlet` 规范是 Java Web 开发的基础之一,它定义了服务器端组件(即 Servlet)的标准接口与生命周期,允许开发者构建可移植的 Web 应用程序。Servlet-2.4 规范是 Java ...

    org.springframework.web.servlet-3.0.0.M4.jar

    《Spring MVC框架详解——以org.springframework.web.servlet-3.0.0.M4.jar为例》 在Java Web开发领域,Spring框架无疑是最具影响力的框架之一,其中Spring MVC是它的重要组成部分,用于构建强大的、灵活的Web应用...

    servlet-api

    最原始的 Servlet-api 的 JAR 包,用于和 tomcat 配合完成 JavaWeb 的后端交互操作。因为外网下载较慢,所以在国内 CSDN 也上传一个备份文件方便大家使用。本资源永久无需积分下载!

    servlet-2.5-mrel-spec.rar

    8. **Multipart请求处理**:Servlet 2.5引入了`Part`接口,支持处理HTTP多部分请求,方便上传文件。 9. **注解支持**:Servlet 2.5开始支持注解(Annotation),开发者可以直接在Servlet、Filter和Listener类上使用...

    javax.servlet-api-4.0.1.jar

    通过编写Servlet,开发者可以处理复杂的业务逻辑,如用户登录验证、数据库操作、文件下载上传等。 总结,`javax.servlet-api-4.0.1.jar`作为Java Servlet API的一个版本,是构建Web应用不可或缺的部分。理解并熟练...

    jsp-api.jar servlet-api.jar

    比如,用户提交表单、登录验证、文件上传等操作通常由Servlet来完成。 3. **MVC架构**:在基于Model-View-Controller(MVC)架构的Web应用中,Servlet通常作为Controller,负责接收请求,调用业务逻辑(Model),并...

    Servlet-考试系统.zip

    在"Servlet-考试系统"中,每个Servlet类可能会对应一个特定的HTTP请求处理,比如登录Servlet用于处理用户的登录请求,试题管理Servlet处理教师上传、编辑或删除试题的操作,考试Servlet则负责组织和提交在线考试。...

    semanticcms-openfile-servlet-1.4.zip

    "OpenFileServlet"通常指的是一个用于处理文件上传和下载请求的HTTP Servlet,它在Web应用中扮演着关键角色,允许用户通过Web界面访问或上传文件。"Semantic"可能意味着该项目关注语义化数据或者具有良好的数据结构...

    servlet-3.0最新规范pdf

    10. **MIME类型自动检测**:通过实现Part接口,Servlet 3.0可以自动检测上传文件的MIME类型,使得文件上传处理更加智能。 综上所述,Servlet 3.0规范带来了许多创新特性,极大地提升了Java Web开发的便捷性和效率。...

Global site tag (gtag.js) - Google Analytics