zoukankan      html  css  js  c++  java
  • 通过HTTP协议实现多线程下载

    1. 基本原理,每条线程从文件不同的位置开始下载,最后合并出完整的数据。 


    2. 使用多线程下载的好处 
        下载速度快。为什么呢?很好理解,以往我是一条线程在服务器上下载。也就是说,对应在服务器上,有一个我的下载线程存在。 
        这时候肯定不只我一个人在下载,服务器上肯定同时存在多条下载线程,在下载服务器资源。对于 CPU 来说,不可能实现并发执行。 
        CPU 会公平的为这些线程划分时间片,轮流执行,a线程十毫秒 , b线程十毫秒... 
        假设运用了本文这种手法,意味着我的下载应用,可以同时使用服务器端的任意多条线程同时下载(理论上). 
        假设这个线程数目是 50 条,本应用就将更多的得到服务器 CPU 的照顾超过 50 倍. 
        但是总归会受本地网络速度的限制。 

    3. 每条线程要负责下载的数据长度可以用 “下载数据的总长度” 除以 “参与下载的线程总数” 来计算。但是要考虑到不能整除的情况。 
        假设有 5 条线程参与下载,那么计算公式应该为 : 
                int block = 数据总长度%线程数 == 0? 10/3 : 10/3+1; (不能整除,则加一) 

    4. 和数据库分页查询类型。每条线程需要知道自己从数据的什么位置开始下载,下载到什么位置为止。 
       首先,为每一个线程配备一个 id , id 从零开始,为 0 1 2 3... 
       开始位置:线程 id 乘以每条线程负责下载的数据长度. 
       结束位置:下一个线程开始位置的前一个位置。 
       如: 
          int startPosition =  线程id * 每条线程下载的数据长度 
          int endPosition = (线程id + 1) * 每条线程下载的数据长度 -1; 
            
    5. HTTP 协议的 Range 头可以指定从文件的什么位置开始下载,下载到什么位置结束。单位为 1byte 
       Range:bytes=2097152-4194304 表示从文件的 2M 的位置开始下载,下载到 4M 处结束 
       假如 Range 指定要读取到 文件的 5104389 的字节数位置,但是下载的文件本身只有 4104389 个长度。那么下载操作自动会在 4104389 处停止。 
       因此不会下载到多余的无效数据. 

    6. 另一个难题是如何按顺序将数据写往本地文件。因为,线程是同步执行的,它们同时在往本地目标文件写入数据。
       而线程于线程之间写入的数据并没有按照下载数据本身的顺序。若按照普通的 OutputStream 的写入方式,最后的本地下载文件将失真。 
       于是我们将用到下面这个类: 
         java.io.RandomAccessFile 
         因为此类同时实现了 DataOutput 和 DataInput 的方法。使他们同时具有写入和读取功能。 
         这个类仿佛存在一个类似文件指针的东西,可以随意执行文件的任意一个位置开始读写. 
         因此此类的实例支持对随机访问文件的读取和写入. 
         
         例如: 
           

    Java代码  收藏代码
    1. File file = new File("1.txt");  
    2.         RandomAccessFile accessFile = new RandomAccessFile(file,"rwd");  
    3.         accessFile.setLength(1024);  

        
        虽然,执行完这段代码后,我们还没有向目标文件 "1.txt" 写入任何数据。但是如果此时查看其大小,已经为 1kb 了。这是我们自己设置的大小。 
        这个操作类似于向这个文件存储了一个大型的 byte 数组。这个数组将这个文件撑到指定大小。等待被填满。 
        既然是这样,好处就在于,我们可以通过 “索引” 随机访问此文件系统的某个部分。 
        例如,可能这个文件大小为 500 
        那么,我的业务需求可能需要第一次 从 300 位置开始写数据,写到 350 为止。 
        第二次,我又从 50 开始写数据,写到 100 为止。 
        总之,我不是 “一次性” 的 “按顺序” 的将这个文件写完。 
        那么,RandomAccessFile 可以支持这种操作。 
         
         API 
          void setLength(long newLength) 
              Sets the length of this file. (设置文件的预计大小) 
          void seek(long pos) 
              Sets the file-pointer offset, measured from the beginning of this file, at which the next read or write occurs. 
              假设为这个方法传入 1028 这个参数,表示,将从文件的 1028 位置开始写入。 
          void write(byte[] b, int off, int len) 
              Writes len bytes from the specified byte array starting at offset off to this file. 
          write(byte[] b) 
              Writes b.length bytes from the specified byte array to this file, starting at the current file pointer. 
          void writeUTF(String str) 
              Writes a string to the file using modified UTF-8 encoding in a machine-independent manner. 
           String readLine() 
              Reads the next line of text from this file. 

          实验代码: 
         

    Java代码  收藏代码
    1. public static void main(String[] args) throws Exception {  
    2.   
    3.  File file = new File("1.txt");   
    4.   
    5. RandomAccessFile accessFile = new RandomAccessFile(file,"rwd");  
    6.   
    7.  /* 设置文件为 3 个字节大小 */  
    8.   
    9.  accessFile.setLength(3);  
    10.   
    11.  /* 向第二个位置写入 '2' */  
    12.   
    13.  accessFile.seek(1);  
    14.   
    15.  accessFile.write("2".getBytes());  
    16.   
    17.  /* 向第一个位置写入 '1' */   
    18.   
    19. accessFile.seek(0); accessFile.write("1".getBytes());   
    20.   
    21. /* 向第三个位置写入 '3' */  
    22.   
    23.  accessFile.seek(2);   
    24.   
    25. accessFile.write("3".getBytes()); accessFile.close();   
    26.   
    27. // 期待文件的内容为 :123  
    28.   
    29.  }   



        
        以上实验成功,虽然我们写入字符串的顺序为 "2"、"1"、"3",但是因为设置了文件偏移量的关系,文件最终保存的数据为 : 123 
        另一个疑问,写完这三个数据,文件的大小已经为 3 个字节大小了。已经撑满了写入的数据,那么我们继续往里面放数据会有什么效果? 
        
        /* 向超出大小的第四个字节位置写入数据 */ 
        accessFile.seek(3); 
        accessFile.write("400".getBytes()); 
        
        以上代码无论 seek 方法指定的文件指针偏移量以及存入的数据,都已经超出了最开始为文件设定的 3 个字节的大小。 
        按照我的猜测,至少 “accessFile.seek(3)” 位置会抛出 "ArrayIndexOutOfBoundsException" 异常,表示下标越界。 
        而,单独执行 "accessFile.write("400".getBytes())" 应该可以成功。因为这个需求属于合理的,应该有执行它的机制。 
        实验结果是两句代码都是成功的。貌似是说明,文件隐含的大型的字节数组,可以自动撑大。 
        
        但是要注意的问题是,必须要保证所设定的文件大小的每一个位置都具有合法的数据,至少不能为空。 
        例如: 
            /* 向第三个位置写入 '3'  */ 
            accessFile.seek(2); 
            accessFile.write("3".getBytes()); 
            
            accessFile.seek(5); 
            accessFile.write("400".getBytes()); 
        那么结合之前的代码,最后的结果为: 
            123口口400 
        在空白的两个位置处出现了乱码。这是理所应当的。 
        
        另外,假设我们为文件指定了一百个长度: 
            accessFile.setLength(100); 
        而,实际上,我们只为其前五个位置设置了值。那么理所当然的是,文件保存的数据,最后会缀上 95 个乱码。 
        
    7. 准备工作应该十分充分了。接下来上代码。 


       

    Java代码  收藏代码
    1. import java.io.File;    
    2. import java.io.IOException;    
    3. import java.io.InputStream;    
    4. import java.io.RandomAccessFile;    
    5. import java.net.HttpURLConnection;    
    6. import java.net.URL;    
    7. /**  
    8.  * 多线程方式文件下载  
    9.  */    
    10. public class MulThreadDownload {    
    11.     /* 下载的URL */    
    12.     private URL downloadUrl;    
    13.     /* 用于保存的本地文件 */    
    14.     private File localFile;    
    15.     /* 没条线程下载的数据长度 */    
    16.     private int block;    
    17.     public static void main(String[] args) {    
    18.         /* 可以为网络上任意合法下载地址 */    
    19.         String downPath = "http://192.168.1.102:8080/myvideoweb/down.avi";    
    20.         MulThreadDownload threadDownload = new MulThreadDownload();    
    21.         /* 开 10 条线程下载下载 */    
    22.         try {    
    23.             threadDownload.download(downPath, 10);    
    24.         } catch (Exception e) {    
    25.             e.printStackTrace();    
    26.         }    
    27.     }    
    28.     /**  
    29.      * 多线程文件下载  
    30.      *   
    31.      * @param path 下载地址  
    32.      * @param threadCount 线程数  
    33.      */    
    34.     public void download(String path, int threadCount) throws Exception {    
    35.         downloadUrl = new URL(path);    
    36.         HttpURLConnection conn = (HttpURLConnection) downloadUrl    
    37.                 .openConnection();    
    38.         /* 设置 GET 请求方式 */    
    39.         conn.setRequestMethod("GET");    
    40.         /* 设置响应时间超时为 5 秒 */    
    41.         conn.setConnectTimeout(5 * 1000);    
    42.         /* 获取本地文件名 */    
    43.         String filename = parseFilename(path);    
    44.         /* 获取下载文件的总大小 */    
    45.         int dataLen = conn.getContentLength();    
    46.         if (dataLen < 0) {    
    47.             System.out.println("获取数据失败");    
    48.             return;    
    49.         }    
    50.         /* 创建本地目标文件,并设置其大小为准备下载文件的总大小 */    
    51.         localFile = new File(filename);    
    52.         RandomAccessFile accessFile = new RandomAccessFile(localFile, "rwd");    
    53.         /* 这时候,其实本地目录下,已经创建好了一个大小为下载文件的总大小的文件 */    
    54.         accessFile.setLength(dataLen);    
    55.         accessFile.close();    
    56.         /* 计算每条线程要下载的数据大小 */    
    57.         block = dataLen % threadCount == 0 ? dataLen / threadCount : dataLen / threadCount + 1;    
    58.         /* 启动线程下载文件 */    
    59.         for (int i = 0; i < threadCount; i++) {    
    60.             new DownloadThread(i).start();    
    61.         }    
    62.     }    
    63.     /**  
    64.      * 解析文件  
    65.      */    
    66.     private String parseFilename(String path) {    
    67.         return path.substring(path.lastIndexOf("/") + 1);    
    68.     }    
    69.     /**  
    70.      * 内部类: 文件下载线程类  
    71.      */    
    72.     private final class DownloadThread extends Thread {    
    73.         /* 线程 id */    
    74.         private int threadid;    
    75.         /* 开始下载的位置 */    
    76.         private int startPosition;    
    77.         /* 结束下载的位置 */    
    78.         private int endPosition;    
    79.         /**  
    80.          * 新建一个下载线程  
    81.          * @param threadid 线程 id  
    82.          */    
    83.         public DownloadThread(int threadid) {    
    84.             this.threadid = threadid;    
    85.             startPosition = threadid * block;    
    86.             endPosition = (threadid + 1) * block - 1;    
    87.         }    
    88.         @Override    
    89.         public void run() {    
    90.             System.out.println("线程 '" + threadid + "'启动下载..");    
    91.                 
    92.             RandomAccessFile accessFile = null;    
    93.             try {    
    94.                 /* 设置从本地文件的什么位置开始写入数据 ,"rwd" 表示对文件具有读写删权限 */    
    95.                 accessFile = new RandomAccessFile(localFile, "rwd");    
    96.                 accessFile.seek(startPosition);    
    97.                 HttpURLConnection conn = (HttpURLConnection) downloadUrl.openConnection();    
    98.                 conn.setRequestMethod("GET");    
    99.                 conn.setReadTimeout(5 * 1000);    
    100.                 /* 为 HTTP 设置 Range 属性,可以指定服务器返回数据的范围 */    
    101.                 conn.setRequestProperty("Range", "bytes=" + startPosition + "-"    
    102.                         + endPosition);    
    103.                 /* 将数据写往本地文件 */    
    104.                 writeTo(accessFile, conn);    
    105.                     
    106.                 System.out.println("线程 '" + threadid + "'完成下载");    
    107.             } catch (IOException e) {    
    108.                 e.printStackTrace();    
    109.             } finally {    
    110.                 try {    
    111.                     if(accessFile != null) {    
    112.                         accessFile.close();    
    113.                     }    
    114.                  } catch (IOException ex) {    
    115.                      ex.printStackTrace();    
    116.                  }    
    117.             }    
    118.         }    
    119.         /**  
    120.          * 将下载数据写往本地文件  
    121.          */    
    122.         private void writeTo(RandomAccessFile accessFile,    
    123.                 HttpURLConnection conn){    
    124.             InputStream is = null;    
    125.             try {    
    126.                 is = conn.getInputStream();    
    127.                 byte[] buffer = new byte[1024];    
    128.                 int len = -1;    
    129.                 while ((len = is.read(buffer)) != -1) {    
    130.                     accessFile.write(buffer, 0, len);    
    131.                 }    
    132.             } catch (IOException e) {    
    133.                 e.printStackTrace();    
    134.             } finally {    
    135.                 try {    
    136.                     if(is != null) {    
    137.                         is.close();    
    138.                     }     
    139.                 } catch (Exception ex) {    
    140.                     ex.printStackTrace();    
    141.                 }    
    142.             }    
    143.         }    
    144.     }    
    145. }    
  • 相关阅读:
    async/await使用深入详解
    尴尬的事情又发生Newtonsoft.Json vs Protobuf.net
    在dotnet core下去中心化访问HTTP服务集群
    条件随机场CRF(一)从随机场到线性链条件随机场
    用hmmlearn学习隐马尔科夫模型HMM
    隐马尔科夫模型HMM(四)维特比算法解码隐藏状态序列
    隐马尔科夫模型HMM(三)鲍姆-韦尔奇算法求解HMM参数
    隐马尔科夫模型HMM(二)前向后向算法评估观察序列概率
    隐马尔科夫模型HMM(一)HMM模型
    EM算法原理总结
  • 原文地址:https://www.cnblogs.com/bigben0123/p/4527413.html
Copyright © 2011-2022 走看看