在项目开发过程中,需要对服务器上的某些文件进行下载,最好还是多线程的,网查查阅一下资料,整理两个可以用的案例,特此记录。两个案例分别采用文件流的方式和RandomAccessFile实现。

一、文件流操作

DownloadManager:下载管理,是总线程,在此线程中开启多个分线程进行文件下载

public class DownloadManager implements Runnable {
    // 保存路径
    private String savePath;
    // 总的下载线程数
    private int threadNum;
    // 下载的链接地址
    private String urlFile;
    // 是否下载开始
    private boolean isStarted;
    // 用于监视何时合并文件存放Thread的list
    private List<DownloadThread> downloadList = new ArrayList<DownloadThread>();

    public DownloadManager(String savePath, int threadNum, String urlFile) {
        super();
        this.savePath = savePath;
        this.threadNum = threadNum;
        this.urlFile = urlFile;
    }

    // 最终调用线程下载。本线程中调用分线程。
    public void action() {
        new Thread(this).start();
    }

    @Override
    public void run() {
        long t1 = System.currentTimeMillis();
        System.out.println(t1);
        // 如果没有下载 , 就开始 , 并且将已经下载的变量值设为true
        if (!isStarted) {
            startDownload();
            isStarted = true;
        }
        while (true) {
            // 初始化认为所有线程下载完成,逐个检查
            boolean finish = true;
            // 如果有任何一个没完成,说明下载没完成,不能合并文件
            for (DownloadThread thread : downloadList) {
                if (!thread.isFinish()) {
                    finish = false;
                    break;
                }
            }
            // 全部下载完成才为真
            if (finish) {
                // 合并文件
                mergeFiles();
                // 跳出循环 , 下载结束
                break;
            }
            // 休息一会 , 减少cpu消耗
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long t2 = System.currentTimeMillis();
        System.out.println(t2);
        System.out.println("下载用时:" + (t2 -t1));
    }

    public void startDownload() {
        // 得到每个线程开始值 , 下载字节数大小
        int[][] posAndLength = getPosAndLength();
        // 根据下载信息创建每个下载线程,并且启动他们。
        for (int i = 0; i < posAndLength.length; i++) {
            int pos = posAndLength[i][0];
            int length = posAndLength[i][1];
            DownloadThread downloadThread = new DownloadThread(i + 1, length,
                    pos, savePath, urlFile);
            new Thread(downloadThread).start();
            downloadList.add(downloadThread);
        }
    }

    /**
     * 获得文件大小
     *
     * @return 文件大小
     */
    public long getFileLength() {
        System.out.println("获得文件大小  start......");
        HttpURLConnection conn = null;
        long result = 0;
        try {
            URL url = new URL(urlFile);
            conn = (HttpURLConnection) url.openConnection();
            // 使用Content-Length头信息获得文件大小
            result = Long.parseLong(conn.getHeaderField("Content-Length"));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
        System.out.println("获得文件大小  end......" + result);
        return result;
    }

    // 具体细节求出每个线程的开始位置和文件下载大小
    public int[][] getPosAndLength() {
        int[][] result = new int[threadNum][2];
        int fileLength = (int) getFileLength();
        int every = fileLength % threadNum == 0 ? fileLength / threadNum
                : fileLength / threadNum + 1;
        for (int i = 0; i < result.length; i++) {
            int length = 0;
            if (i != result.length - 1) {
                length = every;
            } else {
                length = fileLength - i * every;
            }
            result[i][0] = i * every;
            result[i][1] = length;
        }
        return result;
    }

    // 合并文件
    public void mergeFiles() {
        System.out.println("合并文件  start......");
        OutputStream out = null;
        try {
            out = new FileOutputStream(savePath);
            for (int i = 1; i <= threadNum; i++) {
                InputStream in = new FileInputStream(savePath + i);
                byte[] bytes = new byte[2048];
                int read = 0;
                while ((read = in.read(bytes)) != -1) {
                    out.write(bytes, 0, read);
                    out.flush();
                }
                if (in != null) {
                    in.close();
                    new File(savePath + i).delete();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("合并文件  end......");
    }

    public String getSavePath() {
        return savePath;
    }

    public void setSavePath(String savePath) {
        this.savePath = savePath;
    }

    public int getThreadNum() {
        return threadNum;
    }

    public void setThreadNum(int threadNum) {
        this.threadNum = threadNum;
    }

    public String getUrlFile() {
        return urlFile;
    }

    public void setUrlFile(String urlFile) {
        this.urlFile = urlFile;
    }

    public boolean isStarted() {
        return isStarted;
    }

    public void setStarted(boolean isStarted) {
        this.isStarted = isStarted;
    }

    public List<DownloadThread> getDownloadList() {
        return downloadList;
    }

    public void setDownloadList(List<DownloadThread> downloadList) {
        this.downloadList = downloadList;
    }
}

DownloadThread:分线程,实现Runnable接口,重写run()方法进行文件下载

public class DownloadThread implements Runnable {
    // 当前第几个线程 , 用于给下载文件起名 file1 file2 file3 ...
    private int whichThread;
    // 监听单一线程下载是否完成
    private boolean isFinish;
    // 本线程要下载的文件字节数
    private int length;
    // 本线程向服务器发送请求时输入流的首位置
    private int startPosition;
    // 保存的路径
    private String savePath;
    // 要下载的文件 , 用于创建连接
    private String url;

    @Override
    public void run() {
        HttpURLConnection conn = null;
        InputStream in = null;
        OutputStream out = null;
        try {
            System.out.println("正在执行的线程:" + whichThread);
            URL fileUrl = new URL(url);
            // 与服务器创建连接
            conn = (HttpURLConnection) fileUrl.openConnection();
            // 下载使用get请求
            conn.setRequestMethod("GET");
            // 告诉服务器 , 我是火狐 , 不要不让我下载。
            conn.setRequestProperty(
                    "User-Agent",
                    "Firefox Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3");
            // 这里是设置文件输入流的首位置
            conn.setRequestProperty("Range", "bytes=" + startPosition + "-");
            // 与服务器创建连接
            conn.connect();
            // 获得输入流
            in = conn.getInputStream();
            // 在硬盘上创建file1 , file2 , ...这样的文件 , 准备往里面写东西
            out = new FileOutputStream(savePath + whichThread);
            // 用于写入的字节数组
            byte[] bytes = new byte[4096];
            // 一共下载了多少字节
            int count = 0;
            // 单次读取的字节数
            int read = 0;
            while ((read = in.read(bytes)) != -1) {
                // 检查一下是不是下载到了本线程需要的长度
                if (length - count < bytes.length) {
                    // 比如说本线程还需要900字节,但是已经读取1000
                    // 字节,则用要本线程总下载长度减去
                    // 已经下载的长度
                    read = length - count;
                }
                // 将准确的字节写入输出流
                out.write(bytes, 0, read);
                // 已经下载的字节数加上本次循环字节数
                count = count + read;
                // 如果下载字节达到本线程所需要字节数,消除循环,
                // 停止下载
                if (count == length) {
                    break;
                }
            }
            // 将监视变量设置为true
            isFinish = true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 最后进行输入、输出、连接的关闭
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    public int getStartPosition() {
        return startPosition;
    }

    public void setStartPosition(int startPosition) {
        this.startPosition = startPosition;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public int getWhichThread() {
        return whichThread;
    }

    public void setWhichThread(int whichThread) {
        this.whichThread = whichThread;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public String getSavePath() {
        return savePath;
    }

    public void setSavePath(String savePath) {
        this.savePath = savePath;
    }

    public DownloadThread(int whichThread, int length, int startPosition,
                          String savePath, String url) {
        super();
        this.whichThread = whichThread;
        this.length = length;
        this.startPosition = startPosition;
        this.savePath = savePath;
        this.url = url;
    }

    public DownloadThread() {
        super();
    }

    public boolean isFinish() {
        return isFinish;
    }

    public void setFinish(boolean isFinish) {
        this.isFinish = isFinish;
    }
}

TestDownload:测试,开启总线程。

public class TestDownload {

    public static void main(String[] args) {
        DownloadManager downloadManager = new DownloadManager("e:/test.png" , 8 , "http://localhost:8080/test.png");
        downloadManager.action();
    }
}

二、RandomAccessFile 操作

 RandomAccessFile是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法,有兴趣的同学请自行百度

 DownloadThread :继承Thread类,重写run()方法下载文件

/**
 * 下载文件的子线程 每一个文件都下载对应的数据
 * @author YUANYUAN
 *
 */
public class DownloadThread extends Thread{
    private String path;
    private int threadId;
    private int startIndex;
    private int endIndex;

    //下载所使用的线程数
    private static int threadCount=8;
    //当前活动的线程数
    private static int activeThread;

    /**
     * 构造方法
     * @param path 下载文件的路径
     * @param threadId 下载文件的线程
     * @param startIndex 下载文件开始的位置
     * @param endIndex 下载文件结束的位置
     */
    public DownloadThread(String path, int threadId, int startIndex,
                          int endIndex) {
        this.path = path;
        this.threadId = threadId;
        this.startIndex = startIndex;
        this.endIndex = endIndex;
    }



    @Override
    public void run() {
        //构造URL地址
        try {

            File tempFile=new File(threadId+".txt");
            //检查记录是否存在,如果存在读取数据
            if (tempFile.exists()) {
                FileInputStream fis=new FileInputStream(tempFile);
                byte[] temp=new byte[1024];
                int length=fis.read(temp);
                //读取到已经下载的位置
                int downloadNewIndex=Integer.parseInt(new String(temp, 0, length));
                //设置重新开始下载的开始位置
                startIndex=downloadNewIndex;
                fis.close();
                //显示真实下载数据的区间
                System.out.println("线程【"+threadId+"】真实开始下载数据区间:"+startIndex+"---->"+endIndex);
            }

            URL url = new URL(path);
            HttpURLConnection conn=(HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5000);
            conn.setRequestMethod("GET");
            //设置请求属性,请求部分资源
            conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
            int code=conn.getResponseCode();
            if (code==206) {//下载部分资源,正常返回的状态码为206
                InputStream is=conn.getInputStream();//已经设置了请求的位置,所以返回的是对应的部分资源
                //构建随机访问文件
                RandomAccessFile raf=new RandomAccessFile("test.png", "rwd");
                //设置 每一个线程随机写文件开始的位置
                raf.seek(startIndex);
                //开始写文件
                int len=0;
                byte[] buffer=new byte[1024];
                //该线程已经下载数据的长度
                int total=0;

                while((len=is.read(buffer))!=-1){//读取输入流
                    //记录当前线程已下载数据的长度
                    RandomAccessFile file=new RandomAccessFile(threadId+".txt","rwd");
                    raf.write(buffer,0,len);//写文件
                    total+=len;//更新该线程已下载数据的总长度
                    System.out.println("线程【"+threadId+"】已下载数据:"+(total+startIndex));
                    //将已下载数据的位置记录写入到文件
                    file.write((startIndex+total+"").getBytes());
                    file.close();
                }
                is.close();
                raf.close();
                //提示下载完毕
                System.out.println("线程【"+threadId+"】下载完毕");
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("线程【"+threadId+"】下载出现异常!!");
        }finally{
            //活动的线程数减少
            activeThread--;
            if (activeThread==0) {
                for (int i = 1; i <= threadCount; i++) {
                    File tempFile=new File(i+".txt");
                    tempFile.delete();
                }
                System.out.println("下载完毕,已清除全部临时文件");
            }
        }

    }
}

TestDownload:测试类

public class TestDownload {
    //下载所使用的线程数
    private static int threadCount=8;
    //当前活动的线程数
    private static int activeThread;

    public static void main(String[] args) throws Exception{
        //请求服务器的路径
        String path="http://localhost:8080/test.png";
        //构造URL地址
        URL url=new URL(path);
        //打开连接
        HttpURLConnection conn=(HttpURLConnection) url.openConnection();
        //设置请求超时的时间
        conn.setConnectTimeout(5000);
        //设置请求方式
        conn.setRequestMethod("GET");
        //获取相应码
        int code=conn.getResponseCode();
        if (code==200) {//请求成功
            //获取请求数据的长度
            int length=conn.getContentLength();
            //在客户端创建一个跟服务器文件大小相同的临时文件
            RandomAccessFile raf=new RandomAccessFile("test.png", "rwd");
            //指定临时文件的长度
            raf.setLength(length);
            raf.close();
            //假设3个线程去下载资源
            //平均每一个线程要下载的文件的大小
            int blockSize=length/threadCount;
            for (int threadId = 1; threadId <= threadCount; threadId++) {
                //当前线程下载数据的开始位置
                int startIndex=blockSize*(threadId-1);
                //当前线程下载数据的结束位置
                int endIndex=blockSize*threadId-1;
                //确定最后一个线程要下载数据的最大位置
                if (threadId==threadCount) {
                    endIndex=length;
                }
                //显示下载数据的区间
                System.out.println("线程【"+threadId+"】开始下载:"+startIndex+"---->"+endIndex);
                //开启下载的子线程
                new DownloadThread(path, threadId, startIndex, endIndex).start();
                activeThread++;
                System.out.println("当前活动的线程数:"+activeThread);
            }

        }else{//请求失败
            System.out.println("服务器异常,下载失败!");
        }
    }
}

三、两者结合,访问接口写法

DownloadController:服务器接口

@RestController
public class DownloadController {

    @RequestMapping(value = "/download", method = RequestMethod.GET)
    public void getDownload(String url, HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException {
        //获取文件名称
        String fileName = url.substring(url.indexOf("/",url.indexOf("/",url.indexOf("/",url.indexOf("/")+1)+1))+1);
        File downloadFile = ResourceUtils.getFile("classpath:static/"+fileName);

        ServletContext context = request.getServletContext();
        // 获取文件类型
        String mimeType = context.getMimeType(url);
        if (mimeType == null) {
            // 设置二进制类型
            mimeType = "application/octet-stream";
        }
        // 设置响应内容类型
        response.setContentType(mimeType);

        // 设置响应头
        String headerKey = "Content-Disposition";
        String headerValue = String.format("attachment; filename=\"%s\"", downloadFile.getName());
        response.setHeader(headerKey, headerValue);
        // 解析断点续传相关信息
        response.setHeader("Accept-Ranges", "bytes");
        long downloadSize = downloadFile.length();
        long fromPos = 0, toPos = 0;
        if (request.getHeader("Range") == null) {
            response.setHeader("Content-Length", downloadSize + "");
        } else {
            // 若客户端传来Range,说明之前下载了一部分,设置206状态(SC_PARTIAL_CONTENT)
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            String range = request.getHeader("Range");
            String bytes = range.replaceAll("bytes=", "");
            String[] ary = bytes.split("-");
            fromPos = Long.parseLong(ary[0]);
            if (ary.length == 2) {
                toPos = Long.parseLong(ary[1]);
            }
            int size;
            if (toPos > fromPos) {
                size = (int) (toPos - fromPos);
            } else {
                size = (int) (downloadSize - fromPos);
            }
            response.setHeader("Content-Length", size + "");
            downloadSize = size;
        }
        //复制内容到响应流
        RandomAccessFile in = null;
        OutputStream out = null;
        try {
            in = new RandomAccessFile(downloadFile, "rw");
            // 设置下载起始位置
            if (fromPos > 0) {
                in.seek(fromPos);
            }
            // 缓冲区大小
            int bufLen = (int) (downloadSize < 2048 ? downloadSize : 2048);
            byte[] buffer = new byte[bufLen];
            int num;
            int count = 0; // 当前写到客户端的大小
            out = response.getOutputStream();
            while ((num = in.read(buffer)) != -1) {
                out.write(buffer, 0, num);
                count += num;
                //处理最后一段,计算不满缓冲区的大小
                if (downloadSize - count < bufLen) {
                    bufLen = (int) (downloadSize-count);
                    if(bufLen==0){
                        break;
                    }
                    buffer = new byte[bufLen];
                }
            }
            response.flushBuffer();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != out) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != in) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

TestDownload:测试类

public class TestDownload {
    //下载所使用的线程数
    private static int threadCount=3;

    public static void main(String[] args) throws Exception{

        URL url=new URL("http://localhost:8080/idea.jpeg");
        //打开连接
        HttpURLConnection conn=(HttpURLConnection) url.openConnection();
        //设置请求超时的时间
        conn.setConnectTimeout(5000);
        //设置请求方式
        conn.setRequestMethod("GET");
        //获取相应码
        int code=conn.getResponseCode();
        int length;
        if (code==200) {//请求成功
            length=conn.getContentLength();
            int blockSize=length/threadCount;
            for (int threadId = 1; threadId <= threadCount; threadId++) {
                //当前线程下载数据的开始位置
                long startIndex=blockSize*(threadId-1);
                //当前线程下载数据的结束位置
                long endIndex=blockSize*threadId-1;
                //确定最后一个线程要下载数据的最大位置
                if (threadId==threadCount) {
                    endIndex=length;
                }
                //显示下载数据的区间
                System.out.println("线程【"+threadId+"】开始下载:"+startIndex+"---->"+endIndex);
                //开启下载的子线程
                new DownloadThread("http://localhost:8080/download?url=http://localhost:8080/idea.jpeg", startIndex, endIndex,"E://file.jpeg").start();
            }
        }

    }

    public static class DownloadThread extends Thread{
        private String url;
        private long from;
        private long to;
        private String savePath;

        public DownloadThread(String url, long from,
                              long to, String savePath) {
            this.url = url;
            this.from = from;
            this.to = to;
            this.savePath = savePath;

        }

        @Override
        public void run() {
            try {
                URL link = new URL(url);
                HttpURLConnection conn = (HttpURLConnection) link.openConnection();
                // 设置断点续传的开始位置
                if (to != 0) {
                    conn.setRequestProperty("Range", "bytes=" + from + "-" + to);
                }else{
                    conn.setRequestProperty("Range", "bytes=" + from + "-");
                }
                if (conn.getResponseCode() == 206) {
                    RandomAccessFile file = new RandomAccessFile(savePath, "rw");
                    file.seek(from);
                    InputStream in = conn.getInputStream();
                    byte[] buffer = new byte[1024];
                    int num;
                    while ((num = in.read(buffer)) > 0) {
                        file.write(buffer, 0, num);
                    }
                    file.close();
                    in.close();
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}