文档章节

Java使用HttpUrlConnection实现多线程断点下载

Ryane
 Ryane
发布于 2017/09/30 16:44
字数 1573
阅读 55
收藏 2

 相信很多同学在面试的时候,经常会被面试官问到这么一个问题:请问如何实现断点下载,即在文件未下载完成时,保存进度,在下次继续下载。要实现这个功能其实并不难,只要使用一个临时文件记录当前的下载进度,然后在下次下载时,从临时文件记录的进度开始下载,从而实现功能。

当你实现以上功能之后,面试官可能又会问:那能不能实现多线程的断点下载?这个问题的话,其实如果你解决了第一个问题之后,就很容易解决这个问题了,无非就是将文件大小分成几块,在不同线程下载不同的块段。而这个例子就是解决了以上两个问题。

以下是解决思路:

1. 根据线程数,将文件大小分成不同的块段。
2. 根据URL生成文件名,同时生成下载路径。
3. 生成每一个块段的起始位置、结束位置。
4. 创建多个线程,在每个线程里面做一下操作5-7的操作。
5. 建立连接,从临时文件里面读取上次下载的起始位置,在请求头里面设置下载范围(起始位置,结束位置)。
6. 打开下载的文件,将光标移动到起始位置,并往里面写数据,每写一次,往临时文件里面记录文件位置。
7. 在文件下载完成后,将临时文件删除。
 

本文只是简单提供解释思路,代码优化空间很大,细节不需要纠结,网络框架的话换做其他也是可以的,HttpUrlConnection是Java提供的最基础的网络框架之一,弄懂了这个,其他的同理。

完整的类在Github上面:https://github.com/ryanlijianchang/ResumeBrokenDownloads

 

import java.io.BufferedInputStream;  
import java.io.File;  
import java.io.FileInputStream;  
import java.io.RandomAccessFile;  
import java.net.HttpURLConnection;  
import java.net.URL;  
  
public class ResumeDownload {  
    public static final String DOWNLOAD_URL = "http://7xs0af.com1.z0.glb.clouddn.com/High-Wake.mp3";  
    public static final String DOWNLOAD_PARENT_PATH = "D:\\test_resume_download\\hi";  
    public static final int THREAD_COUNT = 3;  
  
    public static void main(String[] args) {  
        try {  
            // 获取到下载地址的连接  
            URL mUrl = new URL(DOWNLOAD_URL);  
            HttpURLConnection conn = (HttpURLConnection) mUrl.openConnection();  
            // 获取下载文件的大小  
            int fileLen = conn.getContentLength();  
            // 通过下载链接获取下载文件的文件名  
            String filePathUrl = conn.getURL().getFile();  
            String fileName = filePathUrl.substring(filePathUrl.lastIndexOf(File.separator) + 1);  
            // 生成下载路径  
            String fileDownloadPath = DOWNLOAD_PARENT_PATH + File.separator + fileName;  
            // 判断父路径是否存在,不存在就生成  
            File file = new File(fileDownloadPath);  
            if (!file.getParentFile().exists()) {  
                file.getParentFile().mkdirs();  
            }  
            // 关闭连接  
            conn.disconnect();  
  
            /** 
             * 以下为多线程下载,主要原理就是将文件大小均分多块(根据线程数) 每一个线程从不同的起始位置,下载相等大小的文件 主要通过 
             * HttpUrlConnection里面设置Range参数,设置每一个线程下载的范围 
             * setRequestProperty("Range", "bytes=" + startPos + "-" + endPos); 
             */  
  
            int blockSize = fileLen / THREAD_COUNT;  
            for (int threadId = 1; threadId <= THREAD_COUNT; threadId++) {  
                // 获取每一个线程下载的起始位置和结束位置  
                long startPos = (threadId - 1) * blockSize;  
                long endPos = threadId * blockSize - 1;  
                if (threadId == THREAD_COUNT) {  
                    endPos = fileLen;  
                }  
  
                // 然后通过再不同线程里面实现下载逻辑  
                // 具体实现在DownloadThread这个Runnable里面  
                new Thread(new DownLoadTask(threadId, startPos, endPos, fileDownloadPath, DOWNLOAD_URL)).start();  
            }  
  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
  
    }  
}  
  
/** 
 * 具体下载逻辑 
 *  
 * @author Administrator 
 * 
 */  
class DownLoadTask implements Runnable {  
    public static final String TEMP_NAME = "_tempfile";  
    private int threadId; // 当前线程id  
    private long startPos; // 下载的起始位置  
    private long endPos; // 下载的结束位置  
    private String fileDownloadPath; // 下载文件存放的文件位置  
    private String downloadUrl; // 下载链接  
  
    private String tempFilePath; // 记录进度的临时文件路径  
  
    public DownLoadTask(int threadId, long startPos, long endPos, String fileDownloadPath, String downloadUrl) {  
        super();  
        this.threadId = threadId;  
        this.startPos = startPos;  
        this.endPos = endPos;  
        this.fileDownloadPath = fileDownloadPath;  
        this.downloadUrl = downloadUrl;  
  
        this.tempFilePath = fileDownloadPath + TEMP_NAME + threadId;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 记录下载的开始时间  
            long startTime = System.currentTimeMillis();  
  
            URL mUrl = new URL(downloadUrl);  
  
            // 为了实现断点下载,在重新下载时从缓存文件里面获取下载的起始位置  
            if (getProgress(threadId) != 0) {  
                startPos = getProgress(threadId);  
            }  
  
            System.out.println("线程" + threadId + "继续下载,开始位置:" + startPos + "结束位置是:" + endPos);  
  
            // HttpUrlConnection的常规操作  
            // 要实现断点下载的话,必须要设置mConnection.setRequestProperty("Range", "bytes=" +  
            // startPos + "-" + endPos);  
            HttpURLConnection mConnection = (HttpURLConnection) mUrl.openConnection();  
            mConnection.setRequestMethod("POST");  
            mConnection.setReadTimeout(5000);  
            mConnection.setRequestProperty("Charset", "UTF-8");  
            mConnection.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);  
            mConnection.connect();  
  
            // 如果下载路径不存在的话,则创建文件路径  
            File file = new File(fileDownloadPath);  
            if (!file.getParentFile().exists()) {  
                file.getParentFile().mkdirs();  
            }  
  
            // 通过RandomAccessFile对要下载的文件进行读写  
            RandomAccessFile downloadFile = new RandomAccessFile(fileDownloadPath, "rw");  
            // 写的时候,将光标移到要下载的起始位置  
            downloadFile.seek(startPos);  
  
            BufferedInputStream bis = new BufferedInputStream(mConnection.getInputStream());  
            int size = 0; // 获取缓存区存放的字节大小  
            long len = 0; // 记录本次下载的大小,以便计算本次下载的起始位置移动到了哪里  
            byte[] buf = new byte[1024];  
            while ((size = bis.read(buf)) != -1) {  
                // 累加  
                len += size;  
                // 然后将缓冲区的内容写到下载文件中  
                downloadFile.write(buf, 0, size);  
                // 然后将下载的起始位置移动到已经下载完的末尾,写到缓存文件里面去  
                setProgress(threadId, startPos + len);  
            }  
  
            // 获取下载结束时间,输出  
            long curTime = System.currentTimeMillis();  
            System.out.println("线程" + threadId + "已经下载完成,耗时:" + (curTime - startTime) + "ms.");  
  
            // 关闭流、文件和连接  
            downloadFile.close();  
            mConnection.disconnect();  
            bis.close();  
  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
  
    /** 
     * 从temp文件获取下载进度 
     *  
     * @param threadId 
     * @return 
     */  
    private long getProgress(int threadId) {  
        try {  
            File markFile = new File(tempFilePath);  
            if (!markFile.exists()) {  
                return 0;  
            }  
            FileInputStream fis = new FileInputStream(markFile);  
            BufferedInputStream bis = new BufferedInputStream(fis);  
            byte[] buf = new byte[1024];  
            String startPos = "";  
            int len = -1;  
            while ((len = bis.read(buf)) != -1) {  
                startPos += new String(buf, 0, len);  
            }  
  
            // 不关闭流的话,不能删除文件  
            fis.close();  
            bis.close();  
  
            return Long.parseLong(startPos);  
  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return 0;  
    }  
  
    /** 
     * 在temp文件记录下载进度 
     *  
     * @param threadId 
     * @param startPos 
     */  
    private void setProgress(int threadId, long startPos) {  
        try {  
            File markFile = new File(tempFilePath);  
            if (!markFile.getParentFile().exists()) {  
                markFile.getParentFile().mkdirs();  
            }  
              
            RandomAccessFile rr = new RandomAccessFile(markFile, "rw");// 存储下载标记的文件  
            String strStartPos = String.valueOf(startPos);  
            rr.write(strStartPos.getBytes(), 0, strStartPos.length());  
              
            rr.close();  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            // 当文件下载完成时,即开始位置和结束位置重合时,删除记录进度的缓存文件  
            if (startPos >= endPos) {  
                File markFile = new File(tempFilePath);  
                if (markFile.exists()) {  
                    System.out.println("markFile delete");  
                    markFile.delete();  
                }  
            }  
        }  
  
    }  
}  

 

© 著作权归作者所有

Ryane
粉丝 42
博文 22
码字总数 55318
作品 0
程序员
私信 提问
java如何拿到url啊

这里有一篇关于断点续传的文章 详细地址http://www.ibm.com/developerworks/cn/java/joy-down/index.html Java 实现断点续传的关键几点 (1) 用什么方法实现提交 RANGE: bytes=2000070-。 当然...

帅狗
2013/09/01
197
0
Java之多线程断点下载的实现

RandomAccessFile类: 此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组,光标或索引,称为文件指针;输入操作...

rootusers
2015/03/16
0
0
用Java实现HTTP断点续传(2)

(二)Java实现断点续传的关键几点 (1)用什么方法实现提交RANGE: bytes=2000070-。 当然用最原始的Socket是肯定能完成的,不过那样太费事了,其实Java的net包中提供了这种功能。代码如下: UR...

millzhang
2011/05/04
146
0
用Java实现HTTP断点续传(3)

(二)Java实现断点续传的关键几点 (1)用什么方法实现提交RANGE: bytes=2000070-。 当然用最原始的Socket是肯定能完成的,不过那样太费事了,其实Java的net包中提供了这种功能。代码如下: UR...

millzhang
2011/05/04
126
1
用 Java 实现断点续传 (HTTP)

断点续传的原理 其实断点续传的原理很简单,就是在 Http 的请求上和一般的下载有所不同而已。 打个比方,浏览器请求服务器上的一个文时,所发出的请求如下: 假设服务器域名为 wwww.sjtu.ed...

罪恶的花生
2012/06/22
259
0

没有更多内容

加载失败,请刷新页面

加载更多

The server time zone value 'EDT' is unrecognized or represents more than one time zone.

2019-10-14 18:07:43.714 ERROR 74363 --- [Druid-ConnectionPool-Create-1855026648] com.alibaba.druid.pool.DruidDataSource : create connection SQLException, url: jdbc:mysql://10.30......

yizhichao
17分钟前
6
0
html加载顺序以及影响页面二次渲染额的因素

本文转载于:专业的前端网站➱html加载顺序以及影响页面二次渲染额的因素 浏览器请求发往服务器以后,返回HTML页面,页面内容开始渲染,具体的执行顺序为: 1. 浏览器开始载入html代码,发现<...

前端老手
20分钟前
7
0
BeginnersBook JSP、JSTL、Servlet 教程

来源:ApacheCN BeginnersBook 翻译项目 译者:飞龙 协议:CC BY-NC-SA 4.0 贡献指南 本项目需要校对,欢迎大家提交 Pull Request。 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并...

ApacheCN_飞龙
32分钟前
5
0
Handler和内部类的正确用法

PS:本文摘抄自《Android高级进阶》,仅供学习使用   Android代码中涉及线程间通信的地方经常会使用Handler,典型的代码结构如下。 1 2 3 4 5 6 7 8 9 public class HandlerActivity exten...

shzwork
32分钟前
7
0
centos 7 安装mysql5.6rpm格式

centos 7 安装mysql5.6rpm格式 1查看是否安装了mysql rpm -qa|grep -i mysql 如果安装了请卸载:rpm -e --nodeps MySQL... 2、没有安装则进行如下操作 下载mysql rpm tar包 https://dev.mysq...

Archer99
33分钟前
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部