张知临的专栏

架一座科研与工程的桥梁

基于Java多线程的下载器源码剖析(一)

本文实现了一个基于Java多线程的下载器,可提供的功能有:

1. 对文件使用多线程下载,并显示每时刻的下载速度。
2. 对多个下载进行管理,包括线程调度,内存管理等。

这篇文章的结构如下:首先讨论如何实现利用Java多线程对单个文件进行下载。然后讨论当系统中有多个文件下载,如何对这些下载进行管理。包括线程调度,内存管理等。

一:单个文件下载的管理

1. 单文件下载类层次

首先简要介绍一下单个文件下载管理的类层次:

来一张图来表示。



 

  1. 为需要下载的文件创建一个Download类,Download负责管理该文件下载时的线程管理、文件管理、当前速度计算等操作。
  2. 根据线程的数目tNum,将该文件分为tNum段,每段为一个DownloadBlock。在实际下载的过程中,并不是一次把所有的东西下载完,而是每次下载固定size的一段Di。所以每个DownloadBlock又会分成n段。
  3. 为每个DownloadBlock申请一个线程DownloadThread。其主要作用就是每次下载一段Di,并将其写入到文件中。


2. 单文件下载

对于单个下载,步骤如下

  1. 连接资源服务器,获取资源信息,创建文件
  2.  切分资源,为每个线程分配固定的下载区域。


1)封装下载的属性

在建立下载之前,我们把每一次下载进行抽象封装。

首先把URL、目标文件等封装在一个DownloadConfig类中。

其中包含了4个属性:

private URL url; //文件下载地址
private File file; //下载文件保存目标文件
private int nthread; //下载该文件需要的线程数
private int priority; //该下载的优先级

如下如所示:



2)连接资源服务器,获取资源信息,创建文件,并指定文件大小

length = config.getUrl().openConnection().getContentLength();
RandomAccessFile file = new RandomAccessFile(config.getFile(), "rw");
file.setLength(length);
file.close();

3)切分资源,为每个线程分配固定的下载区域,并将当前的下载加入到队列中

int size = length / config.getNthread();
for(int i = 0; i < config.getNthread(); i++){
	int start = i * size;
	int len;
	if(i == config.getNthread() - 1)
		len = length - start;
	else len = size;
//并将当前的下载加入到下载队列中
	addDownloadBlock(getDownloadBlock(start, len));
}

3)启动线程进行下载

下载的步骤如下:

   1. 创建缓存,创建连接。设置获取资源数据的范围,创建文件,并设置写入位置

//创建缓存
byte [] b;
if(block.getLength() < Constants.BYTES_READ)
	b = new byte[(int)block.getLength()];
else
	b = new byte[Constants.BYTES_READ];

//创建连接。设置获取资源数据的范围,从startPos到endPos
URLConnection con = null;
con.setRequestProperty("Range", "bytes=" + block.getStart() + "-" + block.getStart()+block.getLength()-1);
RandomAccessFile file = new RandomAccessFile(block.getDownload().getConfig().getFile(), "rw");//创建RandomAccessFile
file.seek(block.getStart()); //从startPos开始写入


  2. 如果当前block的length大于0,则从URL资源处获取固定大小的资源,并将其写入到文件中。

  3 .更新block块的start,以及length,如果length大于0,继续进行2,否则则表示当前block已经下载完毕,退出该线程。

InputStream in = block.getDownload().getConfig().getUrl().openStream();
int n;

//对该block内的文件进行下载,
while(count < block.getLength()){
	if (needSplit()) { // 检查该Block是否还需要分块(即当前block剩余的大小大于一次下载的量)
		long newLength = (block.getLength() - count) / 2;
		long newStart = block.getStart() + block.getLength() - newLength;
		DownloadBlock newBlock = block.getDownload().getDownloadBlock(newStart, newLength);
		block.setLength(block.getLength() - newLength);
		block.getDownload().addDownloadBlock(newBlock);
	}


	//写入文件
	n = in.read(b);
	if(n < 0){
		break;
	}else if(count + n > block.getLength()){
		file.write(b, 0, (int)(block.getLength() - count));
		count = block.getLength();
	}else {
		count += n;
		file.write(b, 0, n);
	}
	
	// set block count in download
	if(n > 0){
		//统计每个block中已经下载的段的个数,用于计算当前下载的速度。
		block.getDownload().setBlockCount(block.getStart(), count);
	}
}

in.close();
file.close();


二 . 当前文件下载速度与进度计算

如第一个图所表示的,每个Block中又分为了很多的段D1、D2、…Dn,因此当为了计算当前下载的速度,需要将下载的段D的数量统计出来,这里使用了一个ConcurrentHashMap<Long, Long>来保存每个block已经下载完成的段D的数目。其中key为每个block的start值,而value为该block已经下载完的段D。
在当前时刻,我们需要统计当前Download已经下载完成段D的数量,然后再和上一时刻的相比较,则可以得出当前的下载速度。具体代码见下:

class CheckSpeedTask extends TimerTask{
	
	private static final Log log = LogFactory.getLog(CheckSpeedTask.class);
	
	private Download download;
	private ConcurrentHashMap<Long, Long> blockCounts; 
	
	private long speed = 0; // Byte/S
	private long count = 0; // Total downloaded byte count
	private long lastCount = 0;
	private long time = 0; // Check time
	private long lastTime = 0;
	
	public CheckSpeedTask(Download download, long startTime, ConcurrentHashMap<Long, Long> blockCounts){
		this.download = download;
		this.lastTime = startTime;
		this.blockCounts = blockCounts;
	}
	
	@Override
	public void run() {
		try {	
			time = System.currentTimeMillis();
			count = 0;
//需要统计当前已经下载完成段D的数量。
			for(long c : blockCounts.values()){
				count += c;
			}
			speed = (count -lastCount)/((time - lastTime)/1000);
			log.debug(blockCounts.size() + " threads are downloading " + download + ", cuttent is " + speed + "Byte/S, " + (count * 1.0)/download.getLength()*100 + "% downloaded");
			download.setCount(count);
			download.setSpeed(speed);
			lastTime = time;
			lastCount = count;
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}		
		
	}
}

这样我们就可以在Thread类的run()函数中,计算当前下载的速度

while(activeThreads.size() > 0 || blockQueue.size() > 0){
	Thread.sleep(1000);
	checkSpeed();
}

上面的代码演示了如何使用Java多线程对单个文件进行下载,接下来我们继续讨论如何对多个下载进行调度、管理等



阅读更多
个人分类: Java多线程
想对作者说点什么? 我来说一句

java多线程简单下载

2011年04月30日 211KB 下载

基于Java的网络下载器的实现

2015年11月30日 1.03MB 下载

java 编写的http下载器 源代码

2015年07月10日 9KB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭