Java:使用多线程实现文件分区间下载

每日一讲

我不知道我应当成为一个什么样的人,但我知道我应当不成为什么样的人!

目录

每日一讲

1.多线程文件分区下载目的和原理

2.多线程文件下载的实现过程

2.1获取下载文件的大小

2.2计算每个线程需要下载的范围

2.3创建线程进行下载任务

2.4在线程内进行读写操作实现文件下载

3.文件合成

3.1创建新类MergeFile合并文件

3.2创建CountDownLatch类及调用MergeFile类

 3.3改善线程个数和下载地址写死问题

4.完整代码和运行结果

4.1运行结果

4.2完整代码


1.多线程文件分区下载目的和原理

目的:正常我们下载文件通常是一下到底,只使用一个线程进行下载。但是通常情况下使用多个线程下载的效率会更高,迅雷下载中也是推荐使用三个线程进行下载,当然这也取决于网络,网络不好,线程多也未必是好事。

原理:将一个文件分成多端进行下载,每个线程下载文件的一部分,就好比一个苹果咱们三个好基友一起吃。用这么一张图就能很好的了解了。

2.多线程文件下载的实现过程

实际上想要实现简单的多线程任务设计的技术不难,但也有许多,比如多线程、I/O流、参数传递、构造函数、类和对象等等。

文件链接:https://dldir1.qq.com/qqfile/qq/QQNT/Windows/QQ_9.9.7_240305_x64_01.exe

2.1获取下载文件的大小

要分区间下载我们就必然要文件的总大小,才能更好的分配嘛。

URL url =new URL(address);
HttpURLConnection httpURLConnection= (HttpURLConnection) url.openConnection();
long fileSize=httpURLConnection.getContentLengthLong();//获取要下载文件的大小
System.out.println("文件大小为:"+fileSize);

2.2计算每个线程需要下载的范围

既然大厨小林煮好一锅饭了,那么必然要合理的分配好每个小朋友要吃的饭咯。

long size=fileSize/threadNum;//每个线程下载的部分
//startPos:起始位置   endPos:结束位置
for(int i=0;i<threadNum;i++)
{
    long startPos= i* size;
    long endPos;
    if(i==threadNum-1){
        endPos=0;
    }else{
        endPos=startPos+size;
    }
    if(startPos!=0){
        startPos++;
    }
    System.out.println("第"+(i+1)+"个线程下载的区间为:"+startPos+"----"+endPos);
}

2.3创建线程进行下载任务

哥们,饭已经给你分完了,那接下来就是吃光这包分配的饭勒。

//每循环一次就启动一个线程
DownloadThread downloadThread=new DownloadThread(startPos,endPos,url,i+1,address,threadNum,countDownLatch);
downloadThread.start();
class DownloadThread extends Thread{
    long startPos;
    long endPos;
    URL url;
    int index;
    String address;
    int threadNum;
    CountDownLatch countDownLatch;
    //创建构造函数传递参数进来,(需要哪些参数就传哪些过来)
    public DownloadThread(long startPos,long endPos,URL url,int index,String address,int threadNum,CountDownLatch countDownLatch)
    {
        this.startPos=startPos;
        this.endPos=endPos;
        this.url=url;
        this.index=index;
        this.address=address;
        this.threadNum=threadNum;
        this.countDownLatch=countDownLatch;
    }
    public void run(){

    }
}

2.4在线程内进行读写操作实现文件下载

虽然说饭已经吃下去了,但是哥们得消化呀,光吃不拉早晚得成小胖呀,那么接下来我们就需要进行文件下载咯,先读后写,先读后写,先读后写--IO流秘诀可不能忘。

//文件下载--先读后写
try {
    //这里用于断点续传,每次只请求文件的一部分
    URLConnection urlConnection = (HttpURLConnection) url.openConnection();
    if(index!=threadNum){
        urlConnection.setRequestProperty("Range","byte="+startPos+"-"+endPos);
    }else{
        urlConnection.setRequestProperty("Range","byte="+startPos);
    }
    InputStream inputStream = urlConnection.getInputStream();
    byte data[]=new byte[1024*8];
    int num=0;
    String fileName=address.substring(address.lastIndexOf("/")+1);
    FileOutputStream fileOutputStream=new FileOutputStream(fileName+"_0"+index);
    while((num=inputStream.read(data))!=-1){
        //开始写
        fileOutputStream.write(data,0,num);
    }
    fileOutputStream.close();
    inputStream.close();
    countDownLatch.countDown();
} catch (IOException e) {
    throw new RuntimeException(e);
}

 完整下载代码和运行结果

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
public class Test {
    public static void main(String[] args) throws IOException {
        String address="https://dldir1.qq.com/qqfile/qq/QQNT/Windows/QQ_9.9.7_240305_x64_01.exe";
        int threadNum=3;
        URL url=new URL(address);
        HttpURLConnection httpURLConnection= (HttpURLConnection)url.openConnection();
        long fileSize = httpURLConnection.getContentLengthLong();
        System.out.println("文件大小为:"+fileSize);
        //获取每个线程要下载的区间大小
        long size = fileSize/threadNum;
        for (int i = 0; i < threadNum; i++) {
            long startPos = i*size;
            long endPos;
            if(i==threadNum-1)
            {
                endPos=0;
            }else{
                endPos=startPos+size;
            }
            if(startPos!=0)
            {
                startPos++;
            }
            System.out.println("第"+(i+1)+"个线程下载的的区间为:"+startPos+"----"+endPos);
            //每循环一次启动一个线程
            TestDownloadThread testDownloadThread=new TestDownloadThread(startPos,endPos,url,(i+1),threadNum,address);
            testDownloadThread.start();
        }
    }
}
class TestDownloadThread extends Thread{
    long startPos;
    long endPos;
    URL url;
    int index;
    int threadNum;
    String address;
    public TestDownloadThread(long startPos,long endPos,URL url,int index,int threadNum,String address){
        this.startPos=startPos;
        this.endPos=endPos;
        this.url=url;
        this.index=index;
        this.threadNum=threadNum;
        this.address=address;
    }
    public void run(){
        //先读后写
        try {
            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
            if(index!=threadNum)
            {
                urlConnection.setRequestProperty("Range","byte="+startPos+"-"+endPos);
            }else{
                urlConnection.setRequestProperty("Range","byte="+startPos);
            }
            InputStream inputStream = urlConnection.getInputStream();
            byte data[]=new byte[1024*8];
            int num=0;
            String fileName=address.substring(address.lastIndexOf("/")+1);
            FileOutputStream fileOutputStream=new FileOutputStream(fileName+"_0"+index);
            while((num=inputStream.read(data))!=-1){
                //开始写
                fileOutputStream.write(data,0,num);
            }
            fileOutputStream.close();
            inputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

很好进行到这里你已经能够得到n个区间的下载文件了,已经成功了一小部分,但是这还远远不够,文件分段下载完了肯定是不能用的,所以我们还需要将这些小文件合并成一个能用的文件。

3.文件合成

文件合成实际上用到的主要技术还是IO流,接下来我们就进行文件的合成任务。

3.1创建新类MergeFile合并文件

在DownLoad类中我们已经下载好文件了,那么此时我们再新建一个文件类MergeFile进行合并文件。

public class MergeFile {
    //构造函数的特点:1.没有返回值 2.函数名字和类名一样
    String fileNames[];
    public MergeFile(String fileNames[]){
        this.fileNames=fileNames;
    }
    public void Merge() throws IOException {
        //具体的合并代码
        byte data[]=new byte[1024*8];
        int len=0;
        //用于获取文件名
        String fileName=fileNames[0].substring(0,fileNames[0].lastIndexOf("_"));
        FileOutputStream fileOutputStream=new FileOutputStream(fileName);
        for (int i = 0; i < fileNames.length; i++) {
            FileInputStream  fileInputStream=new FileInputStream(fileNames[i]);
            while((len=fileInputStream.read(data))!=-1){
                fileOutputStream.write(data,0,len);
            }
            fileInputStream.close();
        }
        fileOutputStream.close();
    }
}

3.2创建CountDownLatch类及调用MergeFile类

为了能够在DownLoad类中运行代码,实现文件合并,我们需要在原本的Download类中创建一个倒数计数器类CountDownLatch以及调用MergeFile类。

//倒数计数器
CountDownLatch countDownLatch=new CountDownLatch(threadNum);
//写在每个线程进行读写完成后位置
countDownLatch.countDown();
//调用MergeFile类
//新建一个文件名数组
String fileNames[]=new String[threadNum];
String fileName=address.substring(address.lastIndexOf("/")+1);
fileNames[i]= fileName+"_0"+(i+1);//添加到第2步for循环中给每个文件赋值
countDownLatch.await();//线程阻塞(等到所有线程任务都结束后才开始运行这句代码后面的代码)
//调用MergeFile类用来合并下载的文件(前面新建一个名字数组用来传参)
MergeFile mergeFile=new MergeFile(fileNames);
mergeFile.Merge();

 3.3改善线程个数和下载地址写死问题

为了不固定死下载的地址以及线程的个数,我们使用main方法的参数传递,相信很多人都没有用过这个功能。

String address=args[0];
int threadNum=Integer.parseInt(args[1]);

运行前需要设置配置(Edit Configurations)

 设置参数传递,中间留有空格

4.完整代码和运行结果

4.1运行结果

2

1

1

4.2完整代码

DownLoad类

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.CountDownLatch;
public class Download {
    public static void main(String[] args) throws IOException, InterruptedException {
        //为了不写死下载的地址以及线程的个数,使用main方法的参数传递
        String address=args[0];
        int threadNum=Integer.parseInt(args[1]);
//        String address="https://dldir1.qq.com/qqfile/qq/QQNT/Windows/QQ_9.9.7_240305_x64_01.exe";
//        int threadNum=3;
        //倒数计数器
        CountDownLatch countDownLatch=new CountDownLatch(threadNum);
        URL url =new URL(address);
        HttpURLConnection httpURLConnection= (HttpURLConnection) url.openConnection();
        long fileSize=httpURLConnection.getContentLengthLong();//获取要下载文件的大小
        System.out.println("文件大小为:"+fileSize);
        long size=fileSize/threadNum;//每个线程下载的部分
        //新建一个文件名数组
        String fileNames[]=new String[threadNum];
        String fileName=address.substring(address.lastIndexOf("/")+1);
        //startPos:起始位置   endPos:结束位置
        for(int i=0;i<threadNum;i++)
        {
            fileNames[i]= fileName+"_0"+(i+1);
            long startPos= i* size;
            long endPos;
            if(i==threadNum-1){
                endPos=0;
            }else{
                endPos=startPos+size;
            }
            if(startPos!=0){
                startPos++;
            }
            System.out.println("第"+(i+1)+"个线程下载的区间为:"+startPos+"----"+endPos);
            //每循环一次就启动一个线程
            DownloadThread downloadThread=new DownloadThread(startPos,endPos,url,i+1,address,threadNum,countDownLatch);
            downloadThread.start();
        }
        //线程阻塞(等到所有线程任务都结束后才开始运行这句代码后面的代码)
        countDownLatch.await();
        //调用MergeFile类用来合并下载的文件(前面新建一个名字数组用来传参)
        MergeFile mergeFile=new MergeFile(fileNames);
        mergeFile.Merge();
    }
}
class DownloadThread extends Thread{
    long startPos;
    long endPos;
    URL url;
    int index;
    String address;
    int threadNum;
    CountDownLatch countDownLatch;
    public DownloadThread(long startPos,long endPos,URL url,int index,String address,int threadNum,CountDownLatch countDownLatch){
        this.startPos=startPos;
        this.endPos=endPos;
        this.url=url;
        this.index=index;
        this.address=address;
        this.threadNum=threadNum;
        this.countDownLatch=countDownLatch;
    }
    public void run(){
        //先读后写
        try {
            URLConnection urlConnection = (HttpURLConnection) url.openConnection();
            if(index!=threadNum){
                urlConnection.setRequestProperty("Range","byte="+startPos+"-"+endPos);
            }else{
                urlConnection.setRequestProperty("Range","byte="+startPos);
            }
            InputStream inputStream = urlConnection.getInputStream();
            byte data[]=new byte[1024*8];
            int num=0;
            String fileName=address.substring(address.lastIndexOf("/")+1);
            FileOutputStream fileOutputStream=new FileOutputStream(fileName+"_0"+index);
            while((num=inputStream.read(data))!=-1){
                //开始写
                fileOutputStream.write(data,0,num);
            }
            fileOutputStream.close();
            inputStream.close();
            countDownLatch.countDown();
            System.out.println("第" + index + "个线程任务结束");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 MergeFile类

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class MergeFile {
    String fileNames[];
    public MergeFile(String fileNames[]){
        this.fileNames=fileNames;
    }
    public void Merge() throws IOException {
        //具体的合并代码
        byte data[]=new byte[1024*8];
        int len=0;
        //用于获取文件名
        String fileName=fileNames[0].substring(0,fileNames[0].lastIndexOf("_"));
        FileOutputStream fileOutputStream=new FileOutputStream(fileName);
        for (int i = 0; i < fileNames.length; i++) {
            FileInputStream  fileInputStream=new FileInputStream(fileNames[i]);
            while((len=fileInputStream.read(data))!=-1){
                fileOutputStream.write(data,0,len);
            }
            fileInputStream.close();
        }
        fileOutputStream.close();
    }
}

结果不难,过程不简单,学习必定要保持耐心,加油!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值