利用Servlet上传文件(在servlet中处理MIME类型的post数据)

来源:互联网 发布:知乎 特朗普 希拉里 编辑:程序博客网 时间:2024/06/06 02:20



一、得到HTTP请求消息的内容
    下面是一个纯JAVA的应用程序,它可以获取表单提交时请求消息的全部内容,该程序将请求消息的内容显示在屏幕上同时写入程序运行路径的一个名为request.txt的文本文件中。
     该程序涉及到JAVA的基本知识:Socket编程、多线程、Servlet编程,但都是一些最基本的知识,只要学习过JAVA应该不难看懂。另外,我在这里放这个程序的另一个目的是帮助我们对HTTP协议有一点感性的认识。
1.1、HServerAccept.java
import java.io.*;
import java.net.*;
public class HServerAccept implements Runnable{
 //侦听端口
 final static int LISTEN_PORT=8091;
 public void run(){
  //服务Socket
  ServerSocket server=null;
  try{
   server=new ServerSocket(this.LISTEN_PORT);
   System.out.println("waiting for connecting......");
  }catch(IOException e){
   System.out.println("can't connect to the port:"+this.LISTEN_PORT
    +";"+e.getMessage());
   System.exit(0);
  }
 
  while(true){
   try{
    //当与客户连接时server.accept()返回一个Socket对象
    new Thread(new ServerEchoRequest(server.accept())).start();
    System.out.println("has start a connection.");
   }catch(IOException e){
    System.out.println("can't recieve data!"+e.getMessage());
   }
  }
 }
 //
 public static void main(String[] args){
  new Thread(new HServerAccept()).start();
 }
}

class ServerEchoRequest implements Runnable{
 private Socket m_Socket=null;
 final static int MAX_BUFF=400;
 private final static int TIMEOUT=3000;
 
 public ServerEchoRequest(Socket socket)throws SocketException{
  this.m_Socket=socket;
  //m_Socket.setSoTimeout(TIMEOUT);
 }
 
 public void run(){
  try{
   getClient();
  }catch(IOException e){
   System.out.println(e);
   System.exit(0);
  }catch(ClassNotFoundException cl){
   System.out.println(cl);
   System.exit(0);
  }
 }
 
 protected void getClient()throws IOException,ClassNotFoundException{
  DataInputStream in=new DataInputStream(m_Socket.getInputStream());
  String s;
  File file=new File("request.txt");
  PrintWriter fileOut=new PrintWriter(new FileWriter(file));
  while((s=in.readLine())!=null){
   System.out.println(s);
   fileOut.println(s);
   fileOut.flush();
  }
 }
}
ServerEchoRequest不是一个公有类可以和HServerAcccept类一齐放在HSereverAccept.java文件中。
1.2、客户端是一个html文件,下面是Client.html的源代码:
<html>
<head><title>文件上传</title></head>
<body>
<form method="POST" action="http://localhost:8091" enctype="multipart/form-data">
<p>&nbsp;ID:<input type="text" name="fileId" size="20"></p>
<p>File:<input type="file" name="fileData" size="20"></p>
<p><input type="submit" value="UpLoad" name="uploadFile"></p>
</form>
</body>
</html>
1.3、下面是程序往request.txt中写入的典型的内容
POST / HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash,

application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-cn
Content-Type: multipart/form-data; boundary=---------------------------7d71c50110368
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
Host: localhost:8091
Content-Length: 63792
Connection: Keep-Alive
Cache-Control: no-cache

-----------------------------7d71c50110368
Content-Disposition: form-data; name="fileId"

123456789
-----------------------------7d71c50110368
Content-Disposition: form-data; name="fileData"; filename="E:/JAVA?à

??/hackers/j2ee/servlet/passfile/passedFile.jpg"
Content-Type: image/pjpeg

这里是一堆乱码(也就是上传文件的数据)
-----------------------------7d71c50110368
Content-Disposition: form-data; name="uploadFile"

UpLoad
-----------------------------7d71c50110368--
二、利用Servlet得到上传的文件
2.1、目录结构
     WEB-INF
           classes
                  shilei
                         UploadFileServlet.java
                         UploadFileServlet.class
     index.html
2.2、UploadFileServlet.java
package shilei;
/**
 *上传文件
 */
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class UploadFileServlet extends HttpServlet{
 public static final int NONE=0;//状态码,表示没有特殊操作
 public static final int DATA_HEADER=1;//下一行要读到报头信息
 public static final int FIELD_DATA=2;//下面要读到表单域的文本值
 public static final int FILE_DATA=3;//下面要读的是上传文件和二进制数据
 //请求消息实体的总长度(请求消息中除消息头之外的数据长度)
 private int totalBytes;
 //容纳请求消息实体的字节数组
 /**
  *在读取二进制数据文件时需要注意,在用文本形式按行读入数据时,因为readLine()
  *不能读入回车换行符,按行读入数据会丢失二进制数据中的"0A"和"0D"(十六进制)字
  *符.所以将响应实体放入一个二进制字节数组中,然后在字节数组中对文件数据进行定
  *位,这样就不会丢失数据.
  */
 private byte[] requestMessage;
 private String requestMessageStr;//表示消息实体的字符串
 //
 private String contontType="";//请求消息类型
 private String fieldName="";//表单域的名称
 private String fieldValue="";//表单域的值
 //
 private String boundary="";//分界符字符串
 private String lastBoundary="";//结束分界符字符串
 //
 private int fileLength=0;//上传文件的长度
 private String fileId="";//上传文件的ID
 private String fileName="";//上传文件的名称
 //容纳表单域的名称/值的哈希表
 private Hashtable formFields=new Hashtable();
 public void doPost(HttpServletRequest req,HttpServletResponse res)
  throws ServletException,IOException{
  this.totalBytes=req.getContentLength();
  this.requestMessage=new byte[this.totalBytes];
  this.contontType=req.getContentType();
  //在消息头类型中找到分界符的定义
  int pos=this.contontType.indexOf("boundary=");
  if(pos!=-1){
   pos+="boundary=".length();
   //解析出分界符
   //在数据部分的分界符比在Content-Type中规定的分界符在前面将会
   //多出两个短杠"--",而在所有数据结束时分界符的前后都多了两个
   //"--"这是HTTP协议的规定.
   this.boundary="--"+this.contontType.substring(pos);
   this.lastBoundary=this.boundary+"--";//得到结束分界符
  }
  int state=this.NONE;//起始状态为NONE
  //得到请求消息的数据输入流
  DataInputStream in=new DataInputStream(req.getInputStream());
  in.readFully(this.requestMessage);//根据长度,将消息实体的内容读入到字节数组中
  in.close();
  //从字节数组中得到表示实体的字符串
  this.requestMessageStr=new String(this.requestMessage);
  //从字符串中得到输出缓冲流
  BufferedReader buffer=new BufferedReader(new StringReader(this.requestMessageStr));
  //暂存读出的一行
  String readLine="";
  while(true){
   readLine=buffer.readLine();
   //如果到达结束分界符就跳出循环
   if(readLine==null||readLine.equalsIgnoreCase(this.lastBoundary))break;
   switch(state){
    case NONE:
     if(readLine.startsWith(this.boundary)){
      //如果读到分界符,则表示下一行一个表头
      state=this.DATA_HEADER;//状态设为表示表头信息
     }
     break;
    case DATA_HEADER:
     pos=readLine.indexOf("filename=");
     //先判断出这是一个文本表单域的头信息,还是一个上传文件的头信息
     if(pos==-1){
      //如果是文本表单域的头信息,解析出表单域的名称
      pos=readLine.indexOf("name=");
      pos+="name=".length()+1;//1表示后面"的占位
      readLine=readLine.substring(pos);
      //去掉最后面一个"符号后将表单域名称放入fieldName
      fieldName=readLine.substring(0,readLine.length()-1);
      //状态设为表示表单域的文本值
      state=this.FIELD_DATA;
     }else{
      //如果是文件数据的头,先存储这一行,用于在字节数组中定位
      String temp=readLine;
      //先解析出文件名,1表示后面"的占位
      pos=readLine.indexOf("filename=")+"filename=".length()+1;
      //去掉最后面一个"符号
      readLine=readLine.substring(pos,readLine.length()-1);
      //定位文件名
      pos=readLine.lastIndexOf("//");//转义字符
      //文件名存入fileName
      fileName=readLine.substring(pos+1);
     
      //下面这一部分从字节数组中读取文件的数据
      pos=this.byteIndexOf(this.requestMessage,temp,0);//定位行
      //定位下一行,2表示一个回车和一个换行占2个字节
      this.requestMessage=this.subBytes(
       this.requestMessage,pos+temp.getBytes().length+2,
       this.requestMessage.length);
      //再读一行信息,是这一部分数据的Content-type
      readLine=buffer.readLine();
      //设置文件输入流,准备写文件
      File file=new File(this.fileName);
      DataOutputStream fileOut=new DataOutputStream(new FileOutputStream(file));
      /**
       *字节数组再往下一行,4表示两个回车换行占4个字节。本行(指Content-type行)的
       *回车换行2个字节,Content-type的下一行是回车换行表示的空行占2个字节
       *得到文件数据的起始位置
       */
      this.requestMessage=this.subBytes(
       this.requestMessage,readLine.getBytes().length+4,
       this.requestMessage.length);
      //定位文件数据的结尾
      pos=this.byteIndexOf(this.requestMessage,this.boundary,0);
      //获取文件数据,pos-2是因为在文件数据和boundary之间有一回车换行表示的空行
      this.requestMessage=this.subBytes(this.requestMessage,0,pos-2);
      //将文件数据存盘
      fileOut.write(this.requestMessage);
      //文件长度存入fileLength
      this.fileLength=this.requestMessage.length;
      //状态设为表示要读到上传文件和二进制数据
      state=this.FILE_DATA;
     }
     break;
    case FIELD_DATA:
     //读出表域的值,存入fileValue
     this.fieldValue=buffer.readLine();
     formFields.put(this.fieldName,this.fieldValue);
     //状态设为表示没有特殊操作
     state=this.NONE;
     break;
    case FILE_DATA:
     //如果是文件数据不进行分析直接读过去
     while((!readLine.startsWith(this.boundary))&&
      (!readLine.startsWith(this.lastBoundary)))readLine=buffer.readLine();
     if(readLine.startsWith(this.boundary)) state=this.DATA_HEADER;
     break;
   }//end switch
  }//end while
  res.setContentType("text/html;charset=gb2312");
  PrintWriter out=res.getWriter();
  out.println("<html>");
  out.println("<head><title>文件上传结果</title></head>");
  out.println("<body>");
  out.println("<h>文件上传结果</h><hr>");
  out.println("ID:为"+formFields.get("fileId")+"的文件:"+this.fileName+"已经上传成功!");
  out.println("文件长度为"+this.fileLength+"字节");
  out.println("</body></html>"); 
 }
 /**
  *@param b 要搜索的字节数组
  *@param s 要查找的字符串
  *@param start 搜索的起始位置
  *@return 如果找到返回s的第一个字节在字节数组中的下标,否则返回-1
  */
 private int byteIndexOf(byte[] b,String s,int start){
  return this.byteIndexOf(b,s.getBytes(),start);
 }
 /**
  *@param b 要搜索的字节数组
  *@param s 要查找的字节数组
  *@param start 搜索的起始位置
  *@return 如果找到返回s的第一个字节在字节数组中的下标,否则返回-1
  */
 private int byteIndexOf(byte[] b,byte[] s,int start){
  int i;
  if(s.length==0)return 0;
  int max=b.length-s.length;
  if(max<0)return -1;
  else if(start>max)return -1;
  else if(start<0) start=0;
  search:
   for(i=start;i<max;i++){
    if(b[i]==s[0]){
     //找到了s的第一个元素后比较剩余部分是否相等
     int k=1;
     while(k<s.length){
      if(b[k+i]!=s[k]) continue search;
      k++;
     }
     return i;
    }
   }
   return -1;
 }
 /**
  *在一个字节数组中提取一个字节数组
  */
 private byte[] subBytes(byte[] b,int from,int end){
  byte[] result=new byte[end-from];
  System.arraycopy(b,from,result,0,end-from);
  return result;
 }
 /**
  *在一个字节数组中提取一个字符串
  */
 private String subBytesToString(byte[] b,int from,int end){
  return new String(this.subBytes(b,from,end));
 }
}
2.3、index.html
<html>
<head><title>文件上传</title></head>
<body>
<form method="post" action="UploadFileServlet" enctype="multipart/form-data">
<p>ID:<input type="text" name="fileId" size="20"></p>
<p>File:<input type="file" name="fileData" size="20"></p>
<p><input type="submit" value="Upload" name="uploadFile"></p>
</form>
</body>
</html>
2.4、web.xml文件
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
   version="2.5">

  <display-name>PassFile</display-name>
  <description>PassFile</description>
  <servlet>
   <servlet-name>UploadFileServlet</servlet-name>
   <servlet-class>shilei.UploadFileServlet</servlet-class>
  </servlet>
  <servlet-mapping>
   <servlet-name>UploadFileServlet</servlet-name>
    <url-pattern>/UploadFileServlet</url-pattern>
  </servlet-mapping>
</web-app>
说明:本web.xml的主要功能是配置Servlet。
完成上述步骤并建立相关的文件后就可以把passFile文件夹直接部署在tomcat的webapps文件夹的下面,启动tomcat后直接在浏览器中输入http://localhost:8080/passFile就可以演示本例子了。当然不要忘了将.java文件编译成.class文件。