多线程下载文件实践之旅

目录

1、使用场景

2、多线程下载原理

3、请求如何分段下载

3.1、需要请求的数据如何分段。

3.2、分段下载的数据如何组装成完整的数据文件。

4、关键代码实现

3、成果展现

4、总结

5、参考文章


1、使用场景

        因为最近在做把以前在百度公有云上的音视频和文档文件,需要迁移阿里云上。这里面还有一个小插曲;有位同事想出办法说邮递一个移动硬盘到百度云让直接Copy到移动硬盘之中。按照正规流程这个肯定是不可能的吧,作为一个大企业;必须的符合规范方式操作吧。个人意见应该可以通过对应销售人员或者公司里面专门联系一个百度的对接人员;到百度公司协商作为一个迁移项目形式;该付款付款。这样还可能实现。最后本人只能通过使用Baidu提供API文档;获得原始音视频文件的网络Url路径。自己写相关的多线程下载文件。最后分别把累加账号上230G音视频文件下载完毕;另外一个账号850G下载完毕。

2、多线程下载原理

  • 客户端要下载一个文件, 首先请求服务器,服务器将这个文件传送给客户端,客户端保存到本地, 完成了一个下载的过程.
  • 多线程下载的思想是客户端开启多个线程同时下载,每个线程只负责下载文件的一部分, 当所有线程下载完成的时候,文件下载完毕.
    • 并不是线程越多下载越快, 与网络环境有很大的关系
    • 在同等的网络环境下,多线程下载速度要高于单线程.
    • 多线程下载占用资源比单线程多,相当于用资源换取速度

多线程下载技术是很常见的一种下载方案,这种方式充分利用了多线程的优势,在同一时间段内通过多个线程发起下载请求,将需要下载的数据分割成多个部分,每一个线程只负责下载其中一个部分,然后将下载后的数据组装成完整的数据文件,这样便大大加快了下载效率。常见的下载器,迅雷,QQ旋风等都采用了这种技术。

3、请求如何分段下载

3.1、需要请求的数据如何分段。

Range,是在 HTTP/1.1里新增的一个 header field,它允许客户端实际上只请求文档的一部分,或者说某个范围。

  有了范围请求,HTTP 客户端可以通过请求曾获取失败的实体的一个范围(或者说一部分),来恢复下载该实体。当然这有一个前提,那就是从客户端上一次请求该实体到这次发出范围请求的时段内,该对象没有改变过。例如:

GET /bigfile.html HTTP/1.1
Host: www.joes-hardware.com
Range: bytes=4000-
User-Agent: Mozilla/4.61 [en] (WinNT; I)

     上述请求中,客户端请求的是文档开头 4000 字节之后的部分(不必给出结尾字节数,因为请求方可能不知道文档的大小)。在客户端收到了开头的 4000 字节之后就失败的情况下,可以使用这种形式的范围请求。还可以用 Range 首部来请求多个范围(这些范围可以按任意顺序给出,也可以相互重叠)。

Range头域使用形式如下。例如:

表示头500个字节:bytes=0-499  
表示第二个500字节:bytes=500-999  
表示最后500个字节:bytes=-500  
表示500字节以后的范围:bytes=500-  
第一个和最后一个字节:bytes=0-0,-1 

服务器接收到线程3的请求报文,发现这是一个带有Range头的GET请求

如果一切正常,服务器的**响应报文会有下面这行:

HTTP/1.1 206 OK**

表示处理请求成功,响应报文还有这一行

Content-Range: bytes 200-299/403

斜杠后面的403表示文件的大小

Http协议的发展历程

HTTP协议到现在为止总共经历了3个版本的演化,第一个HTTP协议诞生于1989年3月。

xml属性描述
HTTP/0.91991年
HTTP/1.01992-1996年
HTTP/1.11997-1999年
HTTP/2.02012-2014年

也就是HTTP/1.1 从1997-1999 年就应用了,所以现在基本上是支持断点续传的。

3.2、分段下载的数据如何组装成完整的数据文件。

   随机访问文件RandomAccessFile类

     RandomAccessFile适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从一处转移到另一处,然后读取或修改记录。

    随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。

通过UrlConnection下载部分资源。
  注意:
   1.需要Range头,key:Range   value:bytes:0-499 
          urlconnection.setRequestPropety("Range","bytes:0-499")
   2.需要设置每个线程在本地文件的保存的开始位置
          RandomAccessFile randomfile =new RandomAccessFile(File file,String mode)
          randomfile.seek(int startPostion);//本次线程下载保存的开始位置。

创建从中读取和向其中写入(可选)的随机访问文件流,该文件由 File 参数指定。将创建一个新的 FileDescriptor 对象来表示此文件的连接。
mode 参数指定用以打开文件的访问模式。允许的值及其含意为:

      “r“——以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
      “rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
      “rws“—— 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
    “rwd“——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。

4、关键代码实现

DownloadConstans.java

package com.wdcloud.publiccloud.files.tool.download.filedownload;

import java.util.concurrent.*;

/**
 * @Description
 * @auther jianxiapc
 * @create 2019-08-20 11:20
 */
public class DownloadConstans {
    public static final int MAX_THREAD_COUNT = getSystemProcessCount();
    private static final int MAX_IMUMPOOLSIZE = MAX_THREAD_COUNT;

    /**
     * 自定义线程池
     */
    private static ExecutorService MY_THREAD_POOL;
    /**
     * 自定义线程池
     */
    public static ExecutorService getMyThreadPool(){
        if(MY_THREAD_POOL == null){
            MY_THREAD_POOL = Executors.newFixedThreadPool(MAX_IMUMPOOLSIZE);
        }
        return MY_THREAD_POOL;
    }

    // 线程池
    private static ThreadPoolExecutor threadPool;

    /**
     * 单例,单任务 线程池
     * @return
     */
    public static ThreadPoolExecutor getThreadPool(){
        if(threadPool == null){
            threadPool = new ThreadPoolExecutor(MAX_IMUMPOOLSIZE, MAX_IMUMPOOLSIZE, 3, TimeUnit.SECONDS,
                    new ArrayBlockingQueue<Runnable>(16),
                    new ThreadPoolExecutor.CallerRunsPolicy()
            );
        }
        return threadPool;
    }

    /**
     * 获取服务器cpu核数
     * @return
     */
    private static int getSystemProcessCount(){
        //int count =Runtime.getRuntime().availableProcessors();
        //仅仅只启动4个线程进行下载
        int count=4;
        return count;
    }
}

FileMultiPartDownLoad.java

package com.wdcloud.publiccloud.files.tool.download.filedownload;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description
 * @auther jianxiapc
 * @create 2019-08-20 11:02
 */
public class FileMultiPartDownLoad {
    private static Logger logger = LoggerFactory.getLogger(FileMultiPartDownLoad.class);

    /**
     * 线程下载成功标志
     */
    private static int flag = 0;

    /**
     * 服务器请求路径
     */
    private String netWorkFileUrlPath;
    /**
     * 本地路径
     */
    private String localPath;
    /**
     * 线程计数同步辅助
     */
    private CountDownLatch latch;

    // 定长线程池
    private static ExecutorService threadPool;

    public FileMultiPartDownLoad(String netWorkFileUrlPath, String localPath) {
        this.netWorkFileUrlPath = netWorkFileUrlPath;
        this.localPath = localPath;
    }

    public boolean executeDownLoad() {
        try {
            URL url = new URL(netWorkFileUrlPath);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5000);//设置超时时间
            conn.setRequestMethod("GET");//设置请求方式
            conn.setRequestProperty("Connection", "Keep-Alive");
            int code = conn.getResponseCode();
            if (code != 200) {
                logger.error(String.format("无效网络地址:%s", netWorkFileUrlPath));
                return false;
            }
            //服务器返回的数据的长度,实际上就是文件的长度,单位是字节
//            int length = conn.getContentLength();  //文件超过2G会有问题
            long length = getRemoteFileSize(netWorkFileUrlPath);

            logger.info("文件总长度:" + length + "字节(B)");
            RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");
            //指定创建的文件的长度
            raf.setLength(length);
            raf.close();
            //分割文件
            int partCount = DownloadConstans.MAX_THREAD_COUNT;
            int partSize = (int)(length / partCount);
            latch = new CountDownLatch(partCount);
            threadPool = DownloadConstans.getMyThreadPool();
            for (int threadId = 1; threadId <= partCount; threadId++) {
                // 每一个线程下载的开始位置
                long startIndex = (threadId - 1) * partSize;
                // 每一个线程下载的结束位置
                long endIndex = startIndex + partSize - 1;
                if (threadId == partCount) {
                    //最后一个线程下载的长度稍微长一点
                    endIndex = length;
                }
                logger.info("线程" + threadId + "下载:" + startIndex + "字节~" + endIndex + "字节");
                threadPool.execute(new DownLoadThread(threadId, startIndex, endIndex, latch));
            }
            latch.await();
            if(flag == 0){
                return true;
            }
        } catch (Exception e) {
            logger.error(String.format("文件下载失败,文件地址:%s,失败原因:%s", netWorkFileUrlPath, e.getMessage()), e);
        }
        return false;
    }


    /**
     * 内部类用于实现下载
     */
    public class DownLoadThread implements Runnable {
        private Logger logger = LoggerFactory.getLogger(DownLoadThread.class);

        /**
         * 线程ID
         */
        private int threadId;
        /**
         * 下载起始位置
         */
        private long startIndex;
        /**
         * 下载结束位置
         */
        private long endIndex;

        private CountDownLatch latch;

        public DownLoadThread(int threadId, long startIndex, long endIndex, CountDownLatch latch) {
            this.threadId = threadId;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                //logger.info("线程" + threadId + "正在下载...");
                URL url = new URL(netWorkFileUrlPath);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestProperty("Connection", "Keep-Alive");
                conn.setRequestMethod("GET");
                //请求服务器下载部分的文件的指定位置
                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
                conn.setConnectTimeout(5000);
                int code = conn.getResponseCode();
                //logger.info("线程" + threadId + "请求返回code=" + code);
                InputStream is = conn.getInputStream();//返回资源
                RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");
                //随机写文件的时候从哪个位置开始写
                raf.seek(startIndex);//定位文件
                int len = 0;
                byte[] buffer = new byte[1024];
                while ((len = is.read(buffer)) != -1) {
                    raf.write(buffer, 0, len);
                }
                is.close();
                raf.close();
                logger.info("线程" + threadId + "下载完毕");
            } catch (Exception e) {
                //线程下载出错
                FileMultiPartDownLoad.flag = 1;
                logger.error(e.getMessage(),e);
            } finally {
                //计数值减一
                latch.countDown();
            }

        }
    }

    /**
     * 内部方法,获取远程文件大小
     * @param remoteFileUrl
     * @return
     * @throws IOException
     */
    private long getRemoteFileSize(String remoteFileUrl) throws IOException {
        long fileSize = 0;
        HttpURLConnection httpConnection = (HttpURLConnection) new URL(remoteFileUrl).openConnection();
        httpConnection.setRequestMethod("HEAD");
        int responseCode = 0;
        try {
            responseCode = httpConnection.getResponseCode();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (responseCode >= 400) {
            logger.debug("Web服务器响应错误!");
            return 0;
        }
        String sHeader;
        for (int i = 1;; i++) {
            sHeader = httpConnection.getHeaderFieldKey(i);
            if (sHeader != null && sHeader.equals("Content-Length")) {
                fileSize = Long.parseLong(httpConnection.getHeaderField(sHeader));
                break;
            }
        }
        return fileSize;
    }

    /**
     * 下载文件执行器
     * @param netWorkFileUrlPath
     * @param localDirPath
     * @param fileName
     * @return
     */
    public synchronized static String downLoad(String netWorkFileUrlPath,String localDirPath,String fileName) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();

        String[] names = netWorkFileUrlPath.split("\\.");
        if (names == null || names.length <= 0) {
            return null;
        }
        String fileTypeName = names[names.length - 1];
        String localStorageDirPath =localDirPath+"/" +fileName;
        System.out.println("localStorageDirPath: "+localStorageDirPath);
        FileMultiPartDownLoad m = new FileMultiPartDownLoad(netWorkFileUrlPath, localStorageDirPath);
        long startTime = System.currentTimeMillis();
        boolean flag = false;
        try{
            flag = m.executeDownLoad();
            long endTime = System.currentTimeMillis();
            if(flag){
                logger.info(fileName+" : 文件下载结束,共耗时" + (endTime - startTime)+ "ms");
                return localStorageDirPath;
            }
            logger.warn("文件下载失败");
            return null;
        }catch (Exception ex){
            logger.error(ex.getMessage(),ex);
            return null;
        }finally {
            FileMultiPartDownLoad.flag = 0; // 重置 下载状态
            if(!flag){
                File file = new File(localStorageDirPath);
                file.delete();
            }
            lock.unlock();
        }
    }
}

调用方法代码

   /**
          * 首先通过调用百度SDK API接口,获得基本信息,然后使用多线性 下载单个vod视频文件
     * @param bceClient
     * @param vodMediaId 视频id
     * @param fileStorageDiskPath 存储下载文件路径
     * @param excelFileName 下载后保存文件信息得excel
     * @param expiredInSeconds 过期时间默认3600s
     */
    public void downloadSingleVodMediaFile (VodClient bceClient, String vodMediaId,String fileStorageDiskPath,String excelFileName,long expiredInSeconds) {
    	logger.info("vodMediaId = " + vodMediaId);
    	GetMediaSourceDownloadResponse response = bceClient.getMediaSourceDownload(vodMediaId,expiredInSeconds);
        String netWorkFileUrl = response.getSourceUrl();
        logger.info("netWorkFileUrl = " + netWorkFileUrl);
        //测试线程下载和多线线程下载
        Date startDate = new Date();
        long downLoadStartTime=System.currentTimeMillis();
        //System.out.println("downLoadStartTime: "+downLoadStartTime);
        logger.info("downLoadStartTime: "+sdf.format(startDate));
        //OkHttpDownloadUtil.downNetWorkFile(netWorkFileUrl,fileStorageDiskPath,"single.mp4");
        Map<String, Object> vodFileInfoMap = getVodFileInfoByVodId(bceClient, vodMediaId);
        String fileName =vodFileInfoMap.get("title").toString();
        //针对当个文件下载的函数调用
        FileMultiPartDownLoad.downLoad(netWorkFileUrl,fileStorageDiskPath,fileName);
        Date endDate = new Date();
        long downLoadEndTime=System.currentTimeMillis();
        long customDownloadTime=downLoadEndTime-downLoadStartTime;
        String downloadTimeFormat=CommonConvertUtils.formatMillisTime(customDownloadTime);
        //System.out.println("downloadTimeFormat: "+downloadTimeFormat);
        logger.info("文件 "+vodMediaId+" 下载开始时间"+sdf.format(startDate)+" 下载完成时间:"+sdf.format(endDate));
        logger.info("文件 "+vodMediaId+" downloadTimeFormat: "+downloadTimeFormat);
    }

3、成果展现

4、总结

        通过本次下载文件相关代码编写深刻认识了多线程下载的有点;同时也系统性学习一下如何多线程下载文件。同时也实践如何使用多线程去下载文件。

5、参考文章

Java多线程下载原理与实现

Java使用多线程的好处以及断点续传原理

多线程加速下载的原理

Java--多线程断点下载

多线程下载和多线程断点下载的原理

线程可以理解为下载的通道,一个线程就是一个文件下载通道,多线程也就是同时开起好几个下载通道.当服务器提供下载服务时,使用下载者是共享带宽的,在优先级相同的情况下,总服务器会对总下载线程进行平均分配.线程越多,下载速度越快.当前的下载软件都支持多线程技术. 通常服务器同时与多个用户连接,用户之间共享带宽。如果N个用户的优先级都相同,那么每个用户连接到该服务器上的实际带宽就是服务器带宽的N分之一。如果户数目较多,则每个用户只能占有可怜的一点带宽,下载将会是个漫长的过程。 具体研究内容:下载功能实现,包括单线程下载功能,多线程下载功能,多任务下载功能,删除任务的实现。断点续传等功能包括,下载过程中,暂停下载,承接上次未完成的下载任务。 关键词:多线程;线程安全;断点续传 Abstract The line Cheng can understand the passage for time be loaded with , a line Cheng is a document time be loaded with passage , multiwire Cheng is just to open up several time be loaded with passage at the same time. Being put into use downloading person is to share bandwidth's while the server provides time be loaded with service, the average the general server meeting is carried out on general time be loaded with line Cheng distributes under preferential step is identical situation. The line Cheng is getting quicker as downloading speed much more. The popular time be loaded with software all supports multiwire Cheng. The simultaneous and many consumer of server links up generally , shares bandwidth between the consumer. If consumer's N preferential step is all identical,every consumer links N one of mark being server bandwidth to owing upper reality of server bandwidth so. Fruit the consumer number is more, every consumer can only occupy pitiful little bandwidth then , downloading the meeting is endless process. Study content concretely: The time be loaded with function realizes , includes the single line Cheng time be loaded with function, multi-thread time be loaded with function, the multitasking downloads a function , delete the mission realization. Breaking point adds biography wait for a function to suspend time be loaded with in including , downloading process, carries on not be completed time be loaded with last time mission. Keywords: Multithreading; Thread security; Broken/Resume 目 录 1 引言 1 1.1 网络下载技术 3 1.2 网络基本构架 3 1.3 多线程技术 3 2.1 课题的研究背景与意义 3 2.1.1 课题的研究背景 3 2.1.2 课题的研究意义 4 2.1.3 多线程下载的现状及发展趋势 4 2.2 可行性分析 5 2.2.1 技术可行性 5 2.2.2 操作可行性 6 3 相关基础知识以 6 3.1 JAVA中的多线程与线程安全 6 3.1.1 Java中的多线程 6 3.1.2 Java中的线程安全 7 3.2 HTTP协议简介 8 3.3 断点续传原理 8 4 需求分析 10 4.1用户需求分析 10 4.2 业务流分析 11 5. 整体设计 11 5.1 系统设计要点 11 5.2 系统总体功能结构 12 5.3 开发环境:myeclipse SWT 15 5.4 URL类和URLConnection类的使用 15 6.系统实现 16 6.1用户界面实现 16 6.2 下载任务实现 20 6.2.1 下载任务类图 20 6.2.2 下载任务顺序图 20 6.2.3 下载任务具体实现 21 6.3 监控下载信息设计 25 6.3.1 监控下载信息类图 25 6.3.2 监控下载信息顺序图 26 6.3.3 监控下载信息实现 26 结 论 27 参考文献 28 致 谢 29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值