多线程下载的基本原理和用法

刚学了下多线程的下载,可能是初次接触的原因吧,理解起来觉得稍微有点难。所以想写一篇博客来记录下,加深自己理解的同时,也希望能够帮到一些刚接触的小伙伴。由于涉及到网络的传输,那么就会涉及到http协议。建议在读本文之前您对http协议有一定的了解。

创新互联建站自2013年创立以来,先为广德等服务建站,广德等地企业,进行企业商务咨询服务。为广德企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。

线程可以通俗的理解为下载的通道,一个线程就是文件下载的一个通道,多线程就是同时打开了多个通道对文件进行下载。当服务器提供下载服务时,用户之间共享带宽,在优先级相同的情况下,总服务器会对总下载线程进行平均分配。我们平时用的迅雷下载就是多线程下载。

多线程的下载大致可以分为如下几个步骤:

1: 获取目标文件的大小(totalSize)

按照常识,我们在下载一个文件之前,通常情况下是要知道该文件的大小,这样才好在本地留好足量的空间来存储,免得出现还未下载完,存储空间就爆了的情况。为了方便代码的演示,本文在本地tomcat服务器的webapps/ROOT目录下新建一个test.txt的文件,里面存储了0123456789这10字节的数据。

2: 确定要开启几个线程(threadCount)

需要的文件在服务器上,那我们要开通几个通道去下载呢?一般情况下这是由CPU去决定的,但是CPU开启线程的数目也是有限的,不是想开几个线程就开几个线程。所开线程的最大数量=(CPU核数+1),例如你的CPU核数为4,那么电脑最多可以开启5条线程。为了方便代码演示,本文的threadCount=3

3: 计算平均每个线程需要下载多少个字节的数据(blockSize)

理想情况下多线程下载是按照平均分配原则的,即:单线程下载的字节数等于文件总大小除以开启的线程总条数,当不能整除时,则最后开启的线程将剩余的字节一起下载。例如:本文中的totalSize=10,threadCount=3,则前两个开启的线程下载3KB的数据,第三个开启的线程需要下载(3+1)KB的数据。

4:计算各个线程要下载的字节范围。

平时我们做项目讲究分工明确,同理多线程下载也需要明确各个下载的字节范围,这样才能将文件高效、快速、准确的下载下来。即在下载过程中,各个线程都要明确自己的开始索引(startIndex)和结束索引(endIndex)。

多线程下载的基本原理和用法
从上图我们可以总结出一个公式:
startIndex = threadId乘以blockSize;
endIndex = (threadId+1)乘以blockSize-1;
如果是最后一条线程,那么结束索引为:
endIndex = totalSize - 1;

5: 使用for循环开启3个子线程

    //每次循环启动一条线程下载
    for(int threadId=0; threadId<3;threadId++){
        /**
         * 计算各个线程要下载的字节范围
         */
        //开始索引
        int startIndex = threadId * blockSize;
        //结束索引
        int endIndex = (threadId+1)* blockSize-1;
        //如果是最后一条线程(因为最后一条线程可能会长一点)
        if(threadId == (threadCount -1)){
            endIndex = totalSize -1;
        }
        /**
         * 启动子线程下载
         */
        new DownloadThread(threadId,startIndex,endIndex).start();
    }

6:获取各个线程的目标文件的开始索引和结束索引的范围。

告诉服务器,只要目标段的数据,这样就需要通过Http协议的请求头去设置(range:bytes=0-499 )

connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);

7:使用RandomAccessFile随机文件访问类。创建一个RandomAccessFile对象,将返回的字节流写到文件指定的范围

此处有个注意事项:让RandomAccessFile对象写字节流之前,需要移动RandomAccessFile对象到指定的位置开始写。

    raf.seek(startIndex);

以上就是多线程下载的大致步骤。代码如下:

package com.example;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class DownloadTest {
private static final String path = "http://localhost:8080/test.txt";
public static void main(String[] args) throws Exception {
    /**
     * 1.获取目标文件的大小
     */
    int totalSize = new URL(path).openConnection().getContentLength();
    System.out.println("目标文件的总大小为:"+totalSize+"B");
    /**
     *2. 确定开启几个线程
     *开启线程的总数=CPU核数+1;例如:CPU核数为4,则最多可开启5条线程
     */
    int availableProcessors = Runtime.getRuntime().availableProcessors();
    System.out.println("CPU核数是:"+availableProcessors);
    int threadCount = 3;

    /**
     * 3. 计算每个线程要下载多少个字节
     */
    int blockSize = totalSize/threadCount;

    //每次循环启动一条线程下载
    for(int threadId=0; threadId<3;threadId++){
        /**
         * 4.计算各个线程要下载的字节范围
         */
        //开始索引
        int startIndex = threadId * blockSize;
        //结束索引
        int endIndex = (threadId+1)* blockSize-1;
        //如果是最后一条线程(因为最后一条线程可能会长一点)
        if(threadId == (threadCount -1)){
            endIndex = totalSize -1;
        }
        /**
         * 5.启动子线程下载
         */
        new DownloadThread(threadId,startIndex,endIndex).start();
    }
}

//下载的线程类
private static class DownloadThread extends Thread{
    private int threadId;
    private int startIndex;
    private int endIndex;
    public DownloadThread(int threadId, int startIndex, int endIndex) {
        super();
        this.threadId = threadId;
        this.startIndex = startIndex;
        this.endIndex = endIndex;
    }

    @Override
    public void run(){
        System.out.println("第"+threadId+"条线程,下载索引:"+startIndex+"~"+endIndex);
        //每条线程要去×××器拿取目标段的数据
        try {
            //创建一个URL对象
            URL url = new URL(path);
            //开启网络连接
            HttpURLConnection connection = (HttpURLConnection)url.openConnection();
            //添加配置
            connection.setConnectTimeout(5000);
            /**
             * 6.获取目标文件的[startIndex,endIndex]范围
             */
            //告诉服务器,只要目标段的数据,这样就需要通过Http协议的请求头去设置(range:bytes=0-499 )
            connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);
            connection.connect();
            //获取响应码,注意,由于服务器返回的是文件的一部分,因此响应码不是200,而是206
            int responseCode = connection.getResponseCode();
            //判断响应码的值是否为206
            if (responseCode == 206) {
                //拿到目标段的数据
                InputStream is = connection.getInputStream();
                /**
                 * 7:创建一个RandomAccessFile对象,将返回的字节流写到文件指定的范围
                 */
                //获取文件的信息
                String fileName = getFileName(path);
                //rw:表示创建的文件即可读也可写。
                RandomAccessFile raf = new RandomAccessFile("d:/"+fileName, "rw");
                /**
                 * 注意:让raf写字节流之前,需要移动raf到指定的位置开始写
                 */
                raf.seek(startIndex);
                //将字节流数据写到file文件中
                byte[] buffer = new byte[1024];
                int len = 0;
                while((len=is.read(buffer))!=-1){
                    raf.write(buffer, 0, len);
                }
                //关闭资源
                is.close();
                raf.close();
                System.out.println("第 "+ threadId +"条线程下载完成 !");

            } else {
                System.out.println("下载失败,响应码是:"+responseCode);
            }

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

//获取文件的名称
private static String getFileName(String path){
    //http://localhost:8080/test.txt
    int index = path.lastIndexOf("/");
    String fileName = path.substring(index+1);
    return fileName ;
    }
}

示例代码运行结果如下:

目标文件的总大小为:10B
CPU核数是:4
第0个线程,下载索引:0~2
第1个线程,下载索引:3~5
第2个线程,下载索引:6~9
第1个线程下载完成!
第2个线程下载完成!
第0个线程下载完成!


好了,本文写到此为止。以上是我个人对多线程下载的初步理解,如有不妥之处,还望大家多多指点,感谢!让我们共同学习,一起进步。


网页题目:多线程下载的基本原理和用法
分享网址:http://hbruida.cn/article/pjshse.html