java多线程下载

来源:互联网 发布:几种排序算法的比较 编辑:程序博客网 时间:2024/06/11 05:24

多线程下载介绍

  多线程下载技术是很常见的一种下载方案,这种方式充分利用了多线程的优势,在同一时间段内通过多个线程发起下载请求,将需要下载的数据分割成多个部分,每一个线程只负责下载其中一个部分,然后将下载后的数据组装成完整的数据文件,这样便大大加快了下载效率。常见的下载器,迅雷,QQ旋风等都采用了这种技术。

实现方案

原理很清楚,但是其中涉及到两个关键问题:

  1. 需要请求的数据如何分段。
  2. 分段下载的数据如何组装成完整的数据文件。

要解决这两个问题,需要掌握下面两个知识点。

Range范围请求

  Range,是在 HTTP/1.1里新增的一个 header field,它允许客户端实际上只请求文档的一部分,或者说某个范围。

  有了范围请求,HTTP 客户端可以通过请求曾获取失败的实体的一个范围(或者说一部分),来恢复下载该实体。当然这有一个前提,那就是从客户端上一次请求该实体到这次发出范围请求的时段内,该对象没有改变过。例如:

GET /bigfile.html HTTP/1.1Host: www.joes-hardware.comRange: bytes=4000-User-Agent: Mozilla/4.61 [en] (WinNT; I)

  上述请求中,客户端请求的是文档开头 4000 字节之后的部分(不必给出结尾字节数,因为请求方可能不知道文档的大小)。在客户端收到了开头的 4000 字节之后就失败的情况下,可以使用这种形式的范围请求。还可以用 Range 首部来请求多个范围(这些范围可以按任意顺序给出,也可以相互重叠)。例如,假设客户端同时连接到多个服务器,为了加速下载文档而从不同的服务器下载同一个文档的不同部分。对于客户端在一个请求内请求多个不同范围的情况,返回的响应也是单个实体,它有一个多部分主体及 Content-Type: multipart/byteranges 首部。

Range头域使用形式如下。例如:

表示头500个字节:bytes=0-499  表示第二个500字节:bytes=500-999  表示最后500个字节:bytes=-500  表示500字节以后的范围:bytes=500-  第一个和最后一个字节:bytes=0-0,-1  

  如果客户端发送的请求中Range这个值存在而且有效,则服务端只发回请求的那部分文件内容,响应的状态码变成206,表示Partial Content,并设置Content-Range。如果无效,则返回416状态码,表明Request Range Not Satisfiable如果不包含Range的请求头,则继续通过常规的方式响应。

  比如某文件的大小是 1000 字节,client 请求这个文件时用了 Range: bytes=0-500,那么 server 应该把这个文件开头的 501 个字节发回给 client,同时回应头要有如下内容:Content-Range: bytes 0-500/1000,并返回206状态码。

 并不是所有服务器都接受范围请求,但很多服务器可以。服务器可以通过在响应中包含 Accept-Ranges 首部的形式向客户端说明可以接受的范围请求。这个首部的值是计算范围的单位,通常是以字节计算的。

随机访问文件RandomAccessFile类

RandomAccessFile适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从一处转移到另一处,然后读取或修改记录。

随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。

RandomAccessFile虽然位于Java.io包中,但从RandomAccessFile类的层级结构来看,它并不是InputStream或者OutputStream继承层次结构中的一部分。除了实现了DataInput和DataOutput接口(DataInputStream和DataOutputStream也实现了这两个接口),它和这个两个继承层次结构没有任何关联。它甚至不适用InputStream和OutputStream类中已有的任何功能。它是一个完全独立的类,从头开始编写其所有的方法(大多数都是native的)。这么做是因为RandomAccessFile拥有和别的I/O类型本质不同的行为,我们可以通过它在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接派生自Object类。

本质上来说,RandomAccessFile的工作方式类似于把DataInputStream和DataOutStream组合起来使用,还添加了一些方法。

以下是一些比较重要的方法。

构造方法RandomAccessFile

public RandomAccessFile(File file,  String mode) throws FileNotFoundException

创建从中读取和向其中写入(可选)的随机访问文件流,该文件由 File 参数指定。将创建一个新的 FileDescriptor 对象来表示此文件的连接。 
mode 参数指定用以打开文件的访问模式。允许的值及其含意为:

  • r“——以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
  • rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
  • rws“—— 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
  • rwd“——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。

getFilePointer

public native long getFilePointer() throws IOException;

返回此文件中的当前偏移量,以字节为单位。

length

public native long length() throws IOException;

返回此文件的长度。

setLength

public native void setLength(long newLength)               throws IOException

设置此文件的长度。

seek

public native void seek(long pos)          throws IOException

设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。

write

public void write(byte[] b,int off,int len) throws IOException

将 len 个字节从指定 byte 数组写入到此文件,并从偏移量 off 处开始。

RandomAccessFile类特殊之处在于支持搜寻方法,并且只适用于文件,这种随机访问特性,为多线程下载提供了文件分段写的支持。

需要注意的是,在RandomAccessFile的大多函数均是native的,在JDK1.4之后,RandomAccessFile大多数功能由nio存储映射文件所取代。所谓存储映射文件,简单来说 是由一个文件到一块内存的映射。内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当做非常大的数组来访问。

了解了上面两个知识点,下面看一下多线程下载的具体实现。

import java.io.BufferedReader;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;import java.io.InputStreamReader;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.security.acl.LastOwnerException;import javax.print.attribute.standard.Finishings;public class Main {//此处下载路径可以更改static String path = "http://172.28.21.98:8080/droid4.exe";//开启的线程个数static int threadCount = 3;static int finishedThread = 0;public static void main(String[] args) {//发送http请求,拿到目标文件长度try {URL url = new URL(path);HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setRequestMethod("GET");conn.setConnectTimeout(8000);conn.setReadTimeout(8000);if(conn.getResponseCode() == 200){//获取长度int length = conn.getContentLength();//创建临时文件File file = new File(getNameFromPath(path));RandomAccessFile raf = new RandomAccessFile(file, "rwd");//设置临时文件大小与目标文件一致raf.setLength(length);raf.close();//计算每个线程下载区间int size = length / threadCount;for (int id = 0; id < threadCount; id++) {//计算每个线程下载的开始位置和结束位置int startIndex = id * size;int endIndex = (id + 1) * size - 1;if(id == threadCount - 1){endIndex = length - 1;}System.out.println("线程" + id + "下载的区间:" + startIndex + " ~ " + endIndex);new DownLoadThread(id, startIndex, endIndex).start();}}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}public static String getNameFromPath(String path){int index = path.lastIndexOf("/");return path.substring(index + 1);}}class DownLoadThread extends Thread{int threadId;int startIndex;int endIndex;public DownLoadThread(int threadId, int startIndex, int endIndex) {super();this.threadId = threadId;this.startIndex = startIndex;this.endIndex = endIndex;}@Overridepublic void run() {try {File fileProgress = new File(threadId + ".txt");int lastProgress = 0;if(fileProgress.exists()){//读取进度临时文件中的内容FileInputStream fis = new FileInputStream(fileProgress);BufferedReader br = new BufferedReader(new InputStreamReader(fis));//得到上一次下载进度lastProgress = Integer.parseInt(br.readLine());//改变下载的开始位置,上一次下过的,这次就不请求了startIndex += lastProgress;fis.close();}//发送http请求,请求要下载的数据URL url = new URL(Main.path);HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setRequestMethod("GET");conn.setConnectTimeout(8000);conn.setReadTimeout(8000);//设置请求数据的区间conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);//请求部分数据,成功的响应码是206if(conn.getResponseCode() == 206){InputStream is = conn.getInputStream();byte[] b = new byte[1024];int len = 0;//当前线程下载的总进度int total = lastProgress;File file = new File(Main.getNameFromPath(Main.path));RandomAccessFile raf = new RandomAccessFile(file, "rwd");//设置写入的开始位置raf.seek(startIndex);while((len = is.read(b)) != -1){raf.write(b, 0, len);total += len;System.out.println("线程" + threadId + "下载了:" + total);//创建一个进度临时文件,保存下载进度RandomAccessFile rafProgress = new RandomAccessFile(fileProgress, "rwd");//每次下载1024个字节,就,就马上把1024写入进度临时文件rafProgress.write((total + "").getBytes());rafProgress.close();}raf.close();System.out.println("线程" + threadId + "下载完毕------------------");//3条线程全部下载完毕,才去删除进度临时文件Main.finishedThread++;synchronized (Main.path) {if(Main.finishedThread == Main.threadCount){for (int i = 0; i < Main.threadCount; i++) {File f = new File(i + ".txt");System.out.println(f.getAbsolutePath());f.delete();}Main.finishedThread = 0;}}}}catch (Exception e) {e.printStackTrace();}}}


原创粉丝点击