返回 登录
0

Java多线程断点下载

最近比较闲,原本软件计划招20个人的,现在14个人的团队都是一半在打酱油,这跟全国经济形势有关,我们也没太大办法,所以最近是啥都看看。昨天晚上看了一个多线程断点下载,今天就用Java实现了一遍。
基本思路:
1、通过HttpURLConnection获取网络资源,得到资源大小等一些信息,
2、在本地创建一个和通过网络获取的资源同样大小的文件(目的是为了存放下载的资源)
3、分配每个线程下载文件的开始位置和结束位置(下载时记录每个线程下载的起始位置,方便停止后能继续下载)
4、开启线程去下载。

下面开始实现

private static int threadCount = 3;//开启3个线程
    private static int blockSize = 0;//每个线程下载的大小
    private static int runningTrheadCount = 0;//当前运行的线程数
    private static String path = "http://sw.bos.baidu.com/sw-search-sp/software/09d9bc67eab07/QQ_8.7.19075.0_setup.exe";
    private static String filename ="QQ_8.7.19075.0_setup.exe";
    /**
     * @param args
     */
    public static void main(String[] args) {

        try{
            //1.请求url地址获取服务端资源的大小
            URL url = new URL(path);
            HttpURLConnection openConnection = (HttpURLConnection) url.openConnection();
            openConnection.setRequestMethod("GET");
            openConnection.setConnectTimeout(5*1000);

            int code = openConnection.getResponseCode();
            if(code == 200){
                //获取资源的大小
                int filelength = openConnection.getContentLength();
                if(filelength==-1){
                    filelength=1024*1024*60;
                }
                System.out.println("filelength="+filelength);
                //2.在本地创建一个与服务端资源同样大小的一个文件(占位)
                RandomAccessFile randomAccessFile = new RandomAccessFile(filename, "rw");
                randomAccessFile.setLength(filelength);//设置随机访问文件的大小

                //3.要分配每个线程下载文件的开始位置和结束位置。
                blockSize = filelength/threadCount;//计算出每个线程理论下载大小
                for(int threadId =0 ;threadId < threadCount;threadId++){
                    int startIndex =  threadId * blockSize;//计算每个线程下载的开始位置
                    int endIndex = (threadId+1)*blockSize -1;//计算每个线程下载的结束位置
                    //如果是最后一个线程,结束位置需要单独计算
                    if(threadId == threadCount-1){
                        endIndex = filelength -1;
                    }

                    //4.开启线程去执行下载
                    new DownloadThread(threadId, startIndex, endIndex).start();


                }


            }


        }catch (Exception e) {
            e.printStackTrace();
        }

     }

上面代码就是按照基本思路来写的 首先通过请求url获取网络的资源,并设置为get请求,超时时间为5S 正常连接后获取资源的大小,这里要注意一下,在百度里面输入”qq下载“ 点立即下载
图片描述

得到的下载连接为https://www.baidu.com/link?url=V4gOxFgauy-GHJGTah_Os4obVwMcRqSdtCT3zL6Lxyzg5tMfIMqK7OW_WIr8cUasrc-h9fDypXCPa2EYE8e1242fYJshFLA1BYdPKIu2KWy&wd=&eqid=b7133a01000210550000000658008fce
用chrome带的抓包工具发现
图片描述

图片描述

会有三个连接而我们用直接点击下载得到的连接其中没有Content-Length这一项,因此当调用openConnection.getContentLength();时返回值为-1, 看网上都说设置setRequestProperty(“Accept-Encoding”, “identity”); 就可以了,可是试了下并没什么卵用 我不是搞网络的出身,所以搞了个简单的方法,用下面的地址获取资源。就是抓的包最下面一个的URL地址
http://sw.bos.baidu.com/sw-search-sp/software/84b5fcf50a3de/QQ_8.7.19083.0_setup.exe
现在可以正确的获取网络资源的大小了。
继续 创建一个RandomAccessFile类型的文件,之所以要用这个类创建文件,是为了为后面的断点续传做准备的,看它名字也知道它 它可以随机的开始写入文件的位置,这个类功能非常强大,这里只用了它的randomAccessFile.seek(lastPostion)方法。
接着 就该分配线程 并确定每个线程的开始和结束位置了 这个没什么难的,就是确定用几个线程去下载,每个线程下载多少,随你怎么分配了,只要他们连起来还是资源的大小,并且是连续的就行。
接下来就是开启线程去下载了。

public static class DownloadThread  extends Thread{


        private int threadId;
        private int startIndex;
        private int endIndex;
        private int lastPostion;
        public DownloadThread(int threadId,int startIndex,int endIndex){
            this.threadId = threadId;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
        }

        @Override
        public void run() {
            synchronized (DownloadThread.class) {

                runningTrheadCount = runningTrheadCount +1;//开启一线程,线程数加1
            }

            //分段请求网络连接,分段保存文件到本地
            try{
                URL url = new URL(path);
                HttpURLConnection openConnection = (HttpURLConnection) url.openConnection();
                openConnection.setRequestMethod("GET");
                openConnection.setConnectTimeout(5*1000);


                System.out.println("理论上下载:  线程:"+threadId+",开始位置:"+startIndex+";结束位置:"+endIndex);

                //读取上次下载结束的位置,本次从这个位置开始直接下载。
                File file2 = new File(threadId+".txt");
                if(file2.exists()){
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file2)));
                    String lastPostion_str = bufferedReader.readLine();
                    lastPostion = Integer.parseInt(lastPostion_str);//读取文件获取上次下载的位置

                    //设置分段下载的头信息。  Range:做分段数据请求用的。
                    openConnection.setRequestProperty("Range", "bytes="+lastPostion+"-"+endIndex);//bytes:0-500:请求服务器资源中0-500之间的字节信息  501-1000:
                    System.out.println("实际下载1:  线程:"+threadId+",开始位置:"+lastPostion+";结束位置:"+endIndex);
                    bufferedReader.close();
                }else{

                    lastPostion = startIndex;
                    //设置分段下载的头信息。  Range:做分段数据请求用的。
                    openConnection.setRequestProperty("Range", "bytes="+lastPostion+"-"+endIndex);//bytes:0-500:请求服务器资源中0-500之间的字节信息  501-1000:
                    System.out.println("实际下载2:  线程:"+threadId+",开始位置:"+lastPostion+";结束位置:"+endIndex);
                }



                System.out.println("getResponseCode"+openConnection.getResponseCode() );



                if(openConnection.getResponseCode() == 206){//200:请求全部资源成功, 206代表部分资源请求成功
                    InputStream inputStream = openConnection.getInputStream();
                    //请求成功将流写入本地文件中,已经创建的占位那个文件中

                    RandomAccessFile randomAccessFile = new RandomAccessFile(filename, "rw");
                    randomAccessFile.seek(lastPostion);//设置随机文件从哪个位置开始写。
                    //将流中的数据写入文件
                    byte[] buffer = new byte[1024];
                    int length = -1;
                    int total = 0;//记录本次线程下载的总大小

                    while((length= inputStream.read(buffer)) !=-1){
                        randomAccessFile.write(buffer, 0, length);

                        total = total+ length;
                        //去保存当前线程下载的位置,保存到文件中
                        int currentThreadPostion = lastPostion + total;//计算出当前线程本次下载的位置
                        //创建随机文件保存当前线程下载的位置
                        File file = new File(threadId+".txt");
                        RandomAccessFile accessfile = new RandomAccessFile(file, "rwd");
                        accessfile.write(String.valueOf(currentThreadPostion).getBytes());
                        accessfile.close();



                    }
                    //关闭相关的流信息
                    inputStream.close();
                    randomAccessFile.close();

                    System.out.println("线程:"+threadId+",下载完毕");



                    //当所有线程下载结束,删除存放下载位置的文件。
                    synchronized (DownloadThread.class) {
                        runningTrheadCount = runningTrheadCount -1;//标志着一个线程下载结束。
                        if(runningTrheadCount == 0 ){
                            System.out.println("所有线程下载完成");
                            for(int i =0 ;i< threadCount;i++){
                                File file = new File(i+".txt");
                                System.out.println(file.getAbsolutePath());
                                file.delete();
                            }
                        }

                    }


                }


            }catch (Exception e) {
                e.printStackTrace();
            }



            super.run();
        }

    }

上面代码主要做了4 件事
1、设置分段下载的头信息;
2、分段下载网络资源
3、当中断时把当前各个线程当前下载的位置分别保存到一个临时文件中
4、下载完成后把临时文件删除 上面代码中都给出了详细的注释

其中有一点要注意
openConnection.setRequestProperty(“Range”, “bytes=”+lastPostion+”-“+endIndex);
如果”bytes=格式不对的话会导致设置不成功,返回的将不是部分资源的返回码
另一个要说明的就是randomAccessFile.seek(startThread);是设置各个线程下载的开始位置
现在的开源项目xutils也可以实现多线程断点下载 不过还是附上demo吧。

学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:454297367 我们一起学Java!

评论