关于HTTP POST上传文件时服务端的接收问题

时间:2022-09-13 14:49:53
HTTP POST方法上传文件,客户端是socket发送,服务端Servlet接收,请求报文头如下:

POST /upload.jsp HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Content-Type: multipart/form-data; boundary=---------------------------7da29f2d890386
Host: 192.9.200.246:8080
Content-Length: 1516663
Connection: Keep-Alive
Cache-Control: no-cache  

-----------------------------7da29f2d890386
Content-Disposition: form-data; name="FileData"; filename="D:\set.pdf"
Content-Type: application/pdf

%PDF-1.6
%����
1 0 obj<</Type/Page/Contents 2 0 R/Parent 3 0 R/Resources 4 0 R/CropBox[0 0 595.22 842]/MediaBox[0 0 595.22 842]/Rotate 0>>
endobj
4 0 obj<</ColorSpace<</Cs6 5 0 R>>/ExtGState<</GS1 6 0 R>>/ProcSet[/PDF/Text]/Font<</F1 7 0 R/F3 8 0 R/TT3 9 0 R/F2 10 0 R>>>>
endobj
2 0 obj <</Filter/FlateDecode/Length 1887>>stream
-----------------------------7da29f2d890386--

服务端接收如下

public void doPost(HttpServletRequest request, HttpServletResponse response)     throws ServletException, IOException {
    final int NONE=0;
    final int DATAHEADER=1;
    final int FILEDATA=2;
    final int FIELDDATA=3;
    //将请求消息的实体送到b变量中
    int TotalBytes=request.getContentLength();
    byte[] b=new byte[TotalBytes];
    String contentType=request.getContentType();//请求消息类型
    String fieldname="";                        //表单域的名称
    String fieldvalue="";                       //表单域的值
    String filename="";                         //文件名
    String boundary="";                         //分界符
    String lastboundary="";                     //结束符
    int filesize=0;                             //文件长度
    Hashtable<String, String> formfields=new Hashtable<String, String>();
    int pos=contentType.indexOf("boundary=");
    //String fileID;  //上传的文件ID
    if(pos!=-1)     //取得分界符和结束符
    {
      pos+="boundary=".length();
      boundary="--"+contentType.substring(pos);
      lastboundary=boundary+"--";
    }
    int state=NONE;
    //得到数据输入流reqbuf
    DataInputStream in=new DataInputStream(request.getInputStream());
    in.readFully(b);
    in.close();
    String reqContent=new String(b,"UTF-8");//********如果文件过大会造成内容溢出,但是如果没有读入全部,又无法过滤消息头而得到内容字节数组*********/
    BufferedReader reqbuf=new BufferedReader(new StringReader(reqContent));
    boolean flag=true;
    int i=0;
    while(flag==true)
    {
      String s=reqbuf.readLine();
      if((s==lastboundary)||(s==null)) break;
      switch(state)
      {
         case NONE:
            if (s.startsWith(boundary))
              {
                 state=DATAHEADER;
                 i+=1;
              }
              break;
         case DATAHEADER:
            pos=s.indexOf("filename=");
            if (pos==-1)
            {                           //将表单域的名字解析出来
              pos=s.indexOf("name=");
              pos+="name=".length()+1;
              s=s.substring(pos);
              int l=s.length();
              s=s.substring(0,l-1);
              fieldname=s;
              state=FIELDDATA;
            }
            else
            {                           //将文件名解析出来
              String temp=s;
              pos=s.indexOf("filename=");
              pos+="filename=".length()+1;
              s=s.substring(pos);
              int l=s.length();
              s=s.substring(0,l-1);
              pos=s.lastIndexOf("\\");
              s=s.substring(pos+1);
              filename=s;
                                       //从字节数组中取出文件数组
              pos=byteIndexOf(b,temp,0);
              b=subBytes(b,pos+temp.getBytes().length+2,b.length);//去掉前面的部分
              s=reqbuf.readLine();
              b=subBytes(b,s.getBytes().length+4,b.length);
              pos=byteIndexOf(b,boundary,0);
              b=subBytes(b,0,pos-1);
              File f=new File(formfields.get("FilePath")+"\\"+filename);  //写入文件
              DataOutputStream fileout=new DataOutputStream(new FileOutputStream(f));
              fileout.write(b,0,b.length-1);
              filesize=b.length-1;
              state=FILEDATA;
            }
            break;
         case FIELDDATA:
            s=reqbuf.readLine();
            fieldvalue=s;
            formfields.put(fieldname,fieldvalue);
            state=NONE;
            break;
         case FILEDATA:
            while((!s.startsWith(boundary))&&(!s.startsWith(lastboundary)))
            {
               s=reqbuf.readLine();
               if (s.startsWith(boundary))
               {
                  state=DATAHEADER;
                  break;
               }
            }
            break;
      }
    }
    }

问题:服务端既想一下读到全部字符串(用来解析消息头,然后过滤掉得到字节数组),又想保存完整的字节数组byte[] b(b用来去除消息头后写入文件),如果文件过大,这里就会造成内存溢出,但是如果没有一下子读完,后面又无法解析,大家看一下有没有更好的解决方法。

DataInputStream in=new DataInputStream(request.getInputStream());
    in.readFully(b);
    in.close();
    String reqContent=new String(b,"UTF-8");//会造成内存溢出

19 个解决方案

#1


可以先获取总体的字节数吧,只读取相应字节数的数据。

文件过大,可不可以在客户端坐下判断,规定个传输文件的最大SIZE

#2


引用 1 楼 stl0 的回复:
可以先获取总体的字节数吧,只读取相应字节数的数据。

文件过大,可不可以在客户端坐下判断,规定个传输文件的最大SIZE
就是文件相应字节数据过大,现在测试读到20多M就会溢出了。

#3


我觉得处理都放在while(flag==true)循环中来做,是导致内存溢出的原因,对于post数据,可以一行一行的读啊

while ((s = reader.readLine()) != null) {
    ....(具体处理)
}

#4


引用 3 楼 stl0 的回复:
我觉得处理都放在while(flag==true)循环中来做,是导致内存溢出的原因,对于post数据,可以一行一行的读啊

Java code

while ((s = reader.readLine()) != null) {
    ....(具体处理)
}
一行一行处理的话,用BufferedWriter.write(s)写入文件就会有错误了(字符编码,格式等问题)

#5


消息头可以一行一行读取,但是文件(二进制数据)一行一行读取然后再写入就会有问题。

#6


引用 5 楼 wudeaaa 的回复:
消息头可以一行一行读取,但是文件(二进制数据)一行一行读取然后再写入就会有问题。

POST数据的本体部分 也就是文件部分,你现在是一行行读取,一行行的写入吧

为什么不读取每行之后,存到一个object中,然后统一写入文件呢?

#7


引用 6 楼 stl0 的回复:
引用 5 楼 wudeaaa 的回复:

消息头可以一行一行读取,但是文件(二进制数据)一行一行读取然后再写入就会有问题。

POST数据的本体部分 也就是文件部分,你现在是一行行读取,一行行的写入吧

为什么不读取每行之后,存到一个object中,然后统一写入文件呢?
首先是把所有数据(头和实体)全部读入到一个byte数组中,然后由byte数组生成一个字符串,由
字符串得到字符流BufferedReader,然后用 reader.readline()来读取请求头,并从byte数组中过滤掉,
最后把过滤后的byte数组写入文件。

#8


>关于内存溢出
    int TotalBytes=request.getContentLength();
    byte[] b=new byte[TotalBytes];
以上代码,文件较大的时候肯定是要内存溢出的,这是一个基础问题,没有人会这样做的。如果你这样写程序,那么你的程序一点都经不起考验。一般申请内存都是new byte[4096]/new byte[2048]等,你一下子申请那么大的内存块肯定不行。
>关于你想解析的问题
  你解析的应该是form表单域的数据,那么你最好还是按照字节来解析吧,遇到特殊分隔符号才意味着某个字符串的结束,你想直接把byte读入String,然后判断String的条件看来好像不是很合适。具体我也不是特清楚了,只是感觉比如form表单域,每一个项目都应该是以【CRLF】---【0D0A】为分隔符的。
  以上你可能看不明白,不过我也不知道怎么表达好...

#9


引用 8 楼 qingkangxu 的回复:
>关于内存溢出
  int TotalBytes=request.getContentLength();
  byte[] b=new byte[TotalBytes];
以上代码,文件较大的时候肯定是要内存溢出的,这是一个基础问题,没有人会这样做的。如果你这样写程序,那么你的程序一点都经不起考验。一般申请内存都是new byte[4096]/new byte[2048]等,你一下子申请那么……
是这样的,这只是一个测试而已,你的意思是按照字节读取,当读到CRLF的表示一行结束,然后再解析这一行的字符串?

#10


求解,大家有没有好的方法。

#11


该回复于2010-12-02 09:30:33被版主删除

#12


建一个临时文件,追加写入,完成后移动到目标位置(目录、数据库、网络......)


java.io.BufferedOutputStream
包装
java.io.FileOutputStream

读System.getenv()找临时目录

#13


将贴顶上去,然后我就有积分了

#14


引用 12 楼 zhblue 的回复:
建一个临时文件,追加写入,完成后移动到目标位置(目录、数据库、网络......)


java.io.BufferedOutputStream
包装
java.io.FileOutputStream

读System.getenv()找临时目录
恩,谢谢,关键在解析报文的时候怎么做?

#15


先读1-2k字节,应该已经包括头了,取出form字段,已读取的去掉字段信息剩下的找出属于文件的内容,写入文件,再继续处理剩下的。

#16


顶啦,但是这样解析的很麻烦

#17


该回复于2010-12-03 11:36:48被版主删除

#18


引用 15 楼 zhblue 的回复:
先读1-2k字节,应该已经包括头了,取出form字段,已读取的去掉字段信息剩下的找出属于文件的内容,写入文件,再继续处理剩下的。
读取到的文件有误,不知道哪里出错了

#19


引用 12 楼 zhblue 的回复:
建一个临时文件,追加写入,完成后移动到目标位置(目录、数据库、网络......)


java.io.BufferedOutputStream
包装
java.io.FileOutputStream

读System.getenv()找临时目录
对于有格式的文件如何追加?比如word,pdf

#1


可以先获取总体的字节数吧,只读取相应字节数的数据。

文件过大,可不可以在客户端坐下判断,规定个传输文件的最大SIZE

#2


引用 1 楼 stl0 的回复:
可以先获取总体的字节数吧,只读取相应字节数的数据。

文件过大,可不可以在客户端坐下判断,规定个传输文件的最大SIZE
就是文件相应字节数据过大,现在测试读到20多M就会溢出了。

#3


我觉得处理都放在while(flag==true)循环中来做,是导致内存溢出的原因,对于post数据,可以一行一行的读啊

while ((s = reader.readLine()) != null) {
    ....(具体处理)
}

#4


引用 3 楼 stl0 的回复:
我觉得处理都放在while(flag==true)循环中来做,是导致内存溢出的原因,对于post数据,可以一行一行的读啊

Java code

while ((s = reader.readLine()) != null) {
    ....(具体处理)
}
一行一行处理的话,用BufferedWriter.write(s)写入文件就会有错误了(字符编码,格式等问题)

#5


消息头可以一行一行读取,但是文件(二进制数据)一行一行读取然后再写入就会有问题。

#6


引用 5 楼 wudeaaa 的回复:
消息头可以一行一行读取,但是文件(二进制数据)一行一行读取然后再写入就会有问题。

POST数据的本体部分 也就是文件部分,你现在是一行行读取,一行行的写入吧

为什么不读取每行之后,存到一个object中,然后统一写入文件呢?

#7


引用 6 楼 stl0 的回复:
引用 5 楼 wudeaaa 的回复:

消息头可以一行一行读取,但是文件(二进制数据)一行一行读取然后再写入就会有问题。

POST数据的本体部分 也就是文件部分,你现在是一行行读取,一行行的写入吧

为什么不读取每行之后,存到一个object中,然后统一写入文件呢?
首先是把所有数据(头和实体)全部读入到一个byte数组中,然后由byte数组生成一个字符串,由
字符串得到字符流BufferedReader,然后用 reader.readline()来读取请求头,并从byte数组中过滤掉,
最后把过滤后的byte数组写入文件。

#8


>关于内存溢出
    int TotalBytes=request.getContentLength();
    byte[] b=new byte[TotalBytes];
以上代码,文件较大的时候肯定是要内存溢出的,这是一个基础问题,没有人会这样做的。如果你这样写程序,那么你的程序一点都经不起考验。一般申请内存都是new byte[4096]/new byte[2048]等,你一下子申请那么大的内存块肯定不行。
>关于你想解析的问题
  你解析的应该是form表单域的数据,那么你最好还是按照字节来解析吧,遇到特殊分隔符号才意味着某个字符串的结束,你想直接把byte读入String,然后判断String的条件看来好像不是很合适。具体我也不是特清楚了,只是感觉比如form表单域,每一个项目都应该是以【CRLF】---【0D0A】为分隔符的。
  以上你可能看不明白,不过我也不知道怎么表达好...

#9


引用 8 楼 qingkangxu 的回复:
>关于内存溢出
  int TotalBytes=request.getContentLength();
  byte[] b=new byte[TotalBytes];
以上代码,文件较大的时候肯定是要内存溢出的,这是一个基础问题,没有人会这样做的。如果你这样写程序,那么你的程序一点都经不起考验。一般申请内存都是new byte[4096]/new byte[2048]等,你一下子申请那么……
是这样的,这只是一个测试而已,你的意思是按照字节读取,当读到CRLF的表示一行结束,然后再解析这一行的字符串?

#10


求解,大家有没有好的方法。

#11


该回复于2010-12-02 09:30:33被版主删除

#12


建一个临时文件,追加写入,完成后移动到目标位置(目录、数据库、网络......)


java.io.BufferedOutputStream
包装
java.io.FileOutputStream

读System.getenv()找临时目录

#13


将贴顶上去,然后我就有积分了

#14


引用 12 楼 zhblue 的回复:
建一个临时文件,追加写入,完成后移动到目标位置(目录、数据库、网络......)


java.io.BufferedOutputStream
包装
java.io.FileOutputStream

读System.getenv()找临时目录
恩,谢谢,关键在解析报文的时候怎么做?

#15


先读1-2k字节,应该已经包括头了,取出form字段,已读取的去掉字段信息剩下的找出属于文件的内容,写入文件,再继续处理剩下的。

#16


顶啦,但是这样解析的很麻烦

#17


该回复于2010-12-03 11:36:48被版主删除

#18


引用 15 楼 zhblue 的回复:
先读1-2k字节,应该已经包括头了,取出form字段,已读取的去掉字段信息剩下的找出属于文件的内容,写入文件,再继续处理剩下的。
读取到的文件有误,不知道哪里出错了

#19


引用 12 楼 zhblue 的回复:
建一个临时文件,追加写入,完成后移动到目标位置(目录、数据库、网络......)


java.io.BufferedOutputStream
包装
java.io.FileOutputStream

读System.getenv()找临时目录
对于有格式的文件如何追加?比如word,pdf

#20