请稍等ManixChen正在解析过程中………



Custom Fileupload



本文的目的是简要说明如何编写一个文件上传组件,使他的功能类似 commons-fileupload, 并在结尾处提供了完整代码的获取方式。

#HTTP 本文讨论的是基于 HTTP 协议的文件上传,下面先来看看 HTTP 请求的真面目。

首先,用 JavaSe 类库中的 Socket 搭建一个超简单的服务器,这个服务器只有一个功能,就是完整地打印整个 HTTP 请求体。

public class Server {

    private ServerSocket serverSocket;

    public Server() throws  IOException{
        serverSocket = new ServerSocket(8080);
    }

    public void show() throws IOException{
        while(true){
            Socket socket = serverSocket.accept();
            byte[] buf = new byte[1024];
            InputStream is =  socket.getInputStream();
            OutputStream os = new ByteArrayOutputStream();
            int n = 0;
            while ((n = is.read(buf)) > -1){
                os.write(buf,0,n);
            }
            os.close();
            is.close();
            socket.close();

            System.out.println(os);
        }
    }

    public static void main(String[] args) throws IOException {
        new Server().show();
    }

}

将服务器运行起来之后,在浏览器中输入地址:http://localhost:8080

在我的机器上,显示如下内容,可以看到,这个一个get请求

http-get

下面利用一个 html 的 form表单提交 post 请求

    <form action="http://localhost:8080" method="post" enctype="multipart/form-data">
       <input type="text" name="time" value="1970-01-01"/>
        <input type="file" name="file"/>
        <input type="submit"/>
    </form>

在我的机器上,显示如下内容

http-post

注意图中被红色框起来的部分,第一个红框指示了本次请求中,用来分隔不同元素的分隔线。

每个元素将以此分隔线作为第一行,后面紧跟对元素的描述,描述与内容用空行分隔。

分隔线的后面加两个小短横代表整个请求体结束,即EOF

我们需要做的工作,就是利用分隔线,从请求体中分离出每个元素,分析HTTP请求头的工作可以交给Servlet。

#分析

那么,如何分离呢?

java中的 InputStream 只能读取一次,所以我们想要方便地分析一个流,最直接的办法就是将其缓存下来。

RandomAccessFile 或许能够满足需求,RandomAccessFile 可以提供一个指针用于在文件中的随意移动,然而需要读写本地文件的方案不会是最优方案。

先将整个流读一遍将内容缓存到内存中? 这种方案在多个客户端同时提交大文件时一定是不可靠的。

最理想的方案可能是,我只需要读一遍 InputStream , 读完后将得到一个有序列表,列表中存放每个元素对象。

很明显,JavaSe的流没有提供这个功能

我们知道从 InputStreeam 中获取内容需要使用 read 方法,返回 -1 表示读到了流的末尾,如果我们增强一下read的功能,让其在读到每个元素末尾的时候返回 -1,这样不就可以分离出每个元素了吗,至于判断是否到了整个流的末尾,自有办法。

#设计

如何增强read方法呢?

read方法要在读到元素末尾时返回-1 , 一定需要先对已读取的内容进行分析,判断是否元素末尾。

我的做法是,内部维护一个buffer,read方法在读取时先将字节写入到这个buffer中,然后分析其中是否存在分隔线,然后将buffer中可用的元素复制到客户端提供的buffer。

这个内部维护的buffer并不总是满的,其中的字节来自read方法的原始功能,所以我们需要一个变量来记录buffer中有效字节的末尾位置 tail

我们还需要一个变量 pos 来标记buffer中是否存在分隔线,pos的值即为分隔线的开头在buffer中的位置,如果buffer中不存在分隔线pos的值将为-1。

但是问题没这个简单,分隔线在buffer中存在状态有两种情况:

情况A,分隔线完好地存在于buffer中,图中的bundary即为分隔线

boundary-A

情况B,分隔线的一部分存在于buffer中

boundary-B

在B情况下,boundary有多少字节存在于buffer中是不确定的,而且依靠这些不完整的字节根本无法判断他是否属于boundary开头。

例如,buffer中没有发现boundary,但是buffer末尾的3个字节与boundary开头相同,这种情况可能只是巧合,boundary并没有被截断。

对于这个问题,有一个解决办法,我们不必检查到buffer末尾,而是在buffer末尾留一个关健区pad

这个关健区中很有可能存在被截断boundary,每次检查到pad开头时立即收手,此位置之前的数据可以确保没有boundary,在下次填充buffer时,将这个关健区中的数据复制到buffer开头再处理。很显然,关健区pad长度应该等于boundary,如图:

pad

#关键代码

在buffer中检查boundary

private int findSeparator() {
    int first;
    int match = 0;
    //若buffer中head至tail之间的字节数小于boundaryLength,那么maxpos将小于head,循环将不会运行,返回值为-1
    int maxpos = tail - boundaryLength;
    for (first = head; first <= maxpos && match != boundaryLength; first++) {
        first = findByte(boundary[0], first);
        if (first == -1 || first > maxpos) {
            return -1;
        }
        for (match = 1; match < boundaryLength; match++) {
            if (buffer[first + match] != boundary[match]) {
                break;
            }
        }
    }
    if (match == boundaryLength) {
        return first - 1;
    }
    return -1;
}

填充buffer

private int makeAvailable() throws IOException {
    //该方法在available返回0时才会被调用,若pos!=-1那pos==head,表示boundary处于head位,可用字节数为0
    if (pos != -1) {
        return 0;
    }

    // 将pad位之后的数据移动到buffer开头
    total += tail - head - pad;
    System.arraycopy(buffer, tail - pad, buffer, 0, pad);

    // 将buffer填满
    head = 0;
    tail = pad;
    //循环读取数据,直至将buffer填满,在此过程中,每次读取都将检索buffer中是否存在boundary,无论存在与否,都将即时返回可用数据量
    for (;;) {
        int bytesRead = input.read(buffer, tail, bufSize - tail);
        if (bytesRead == -1) {
            //理论上因为会对buffer不断进行检索,读到boundary时就会return 0,read方法将返回 -1,
            //所以不会读到input末尾,如果运行到了这里,表示发生了错误.
            final String msg = "Stream ended unexpectedly";
            throw new RuntimeException(msg);
        }
        if (notifier != null) {
            notifier.noteBytesRead(bytesRead);
        }
        tail += bytesRead;
        findSeparator();
        //若buffer中的数据量小于keepRegion(boundaryLength),av将必定等于0,循环将继续,直至数据量大于或等于keepRegion(boundaryLength).
        //此时将检索buffer中是否包含boundary,若包含,将返回boundary所在位置pos之前的数据量,若不包含,将返回pad位之前的数据量
        int av = available();

        if (av > 0 || pos != -1) {
            return av;
        }
    }
}

强化后的read方法

@Override
public int read(byte[] b, int off, int len) throws IOException {
    if (closed) {
        throw new RuntimeException("the stream is closed");
    }
    if (len == 0) {
        return 0;
    }
    int res = available();
    if (res == 0) {
        res = makeAvailable();
        if (res == 0) {
            return -1;
        }
    }
    res = Math.min(res, len);
    System.arraycopy(buffer, head, b, off, res);
    head += res;
    total += res;
    return res;
}

#源码获取

我已经按照这套想法完整地实现了文件上传组件

有兴趣的朋友可以从我的Gighub获取源码 点我获取

使用方法:点我查看

fileupload

Java

Http

markdown自动生成github博客(前篇)

RMI

Servlet乱码分析

spring security 探秘

Bean Validation

Java中用js解析json

SQL 拼接

WebService

Java8 新特性

Java Concurrent

Java虚拟机

Effective Java

超类中的泛型

Flex

可重入锁

web fileupload Java Http Apr 15, 2014