支持断点续传java多线程下载Word格式.docx

上传人:b****6 文档编号:19185716 上传时间:2023-01-04 格式:DOCX 页数:24 大小:35.51KB
下载 相关 举报
支持断点续传java多线程下载Word格式.docx_第1页
第1页 / 共24页
支持断点续传java多线程下载Word格式.docx_第2页
第2页 / 共24页
支持断点续传java多线程下载Word格式.docx_第3页
第3页 / 共24页
支持断点续传java多线程下载Word格式.docx_第4页
第4页 / 共24页
支持断点续传java多线程下载Word格式.docx_第5页
第5页 / 共24页
点击查看更多>>
下载资源
资源描述

支持断点续传java多线程下载Word格式.docx

《支持断点续传java多线程下载Word格式.docx》由会员分享,可在线阅读,更多相关《支持断点续传java多线程下载Word格式.docx(24页珍藏版)》请在冰豆网上搜索。

支持断点续传java多线程下载Word格式.docx

publicclassDownInfo

{

privateStringthreadId;

//线程id

privateBigDecimalstartPos=newBigDecimal(0);

//开始位置,随着下载增长

privateBigDecimalendPos=newBigDecimal(0);

//结束位置

publicDownInfo(StringthreadId,longstartPos,longendPos)

{

this.threadId=threadId;

this.startPos=newBigDecimal(startPos);

this.endPos=newBigDecimal(endPos);

}

publiclonggetStartPos()

returnstartPos.longValue();

publiclonggetEndPos()

returnendPos.longValue();

publicvoidupdateStartPos(longsize)

this.startPos=startPos.add(newBigDecimal(size));

publicStringgetThreadId()

returnthreadId;

publicvoidsetThreadId(StringthreadId)

this.threadId=threadId;

}

Java实例:

利用java多线程断点续传实践

2010-07-1912:

22

*author:

annegu

*date:

2009-07-16

annegu做了一个简单的Http多线程的下载程序,来讨论一下多线程并发下载以及断点续传的问题。

这个程序的功能,就是可以分多个线程从目标地址上下载数据,每个线程负责下载一部分,并可以支持断点续传和超时重连。

下载的方法是download(),它接收两个参数,分别是要下载的页面的url和编码方式。

在这个负责下载的方法中,主要分了三个步骤。

第一步是用来设置断点续传时候的一些信息的,第二步就是主要的分多线程来下载了,最后是数据的合并。

1、多线程下载:

/***//**

publicStringdownload(StringurlStr,Stringcharset){

this.charset=charset;

longcontentLength=0;

CountDownLatchlatch=newCountDownLatch(threadNum);

long[]startPos=newlong[threadNum];

longendPos=0;

try{

//从url中获得下载的文件格式与名字

this.fileName=urlStr.substring(urlStr.lastIndexOf("

/"

)+1);

this.url=newURL(urlStr);

URLConnectioncon=url.openConnection();

setHeader(con);

//得到content的长度

contentLength=con.getContentLength();

//把context分为threadNum段的话,每段的长度。

this.threadLength=contentLength/threadNum;

//第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,

      则建立目标文件。

在第4点中说明。

startPos=setThreadBreakpoint(fileDir,fileName,contentLength,startPos);

//第二步,分多个线程下载文件

ExecutorServiceexec=Executors.newCachedThreadPool();

for(inti=0;

i<

threadNum;

i++){

//创建子线程来负责下载数据,每段数据的起始位置为

       (threadLength*i+已下载长度)

startPos[i]+=threadLength*i;

/**//*设置子线程的终止位置,非最后一个线程即为(threadLength*(i+1)-1)

最后一个线程的终止位置即为下载内容的长度*/

if(i==threadNum-1){

endPos=contentLength;

}else{

endPos=threadLength*(i+1)-1;

}

//开启子线程,并执行。

ChildThreadthread=newChildThread(this,latch,i,startPos[i],endPos);

childThreads[i]=thread;

exec.execute(thread);

//等待CountdownLatch信号为0,表示所有子线程都结束。

latch.await();

exec.shutdown();

//第三步,把分段下载下来的临时文件中的内容写入目标文件中。

在第3点中说明。

tempFileToTargetFile(childThreads);

}catch(InterruptedExceptione){

e.printStackTrace();

在ChildThread的构造方法中,除了设置一些从主线程中带来的id,起始位置之外,就是新建了一个临时文件用来存放当前线程的下载数据。

临时文件的命名规则是这样的:

下载的目标文件名+”_”+线程编号。

现在让我们来看看从网络中读数据是怎么读的。

我们通过URLConnection来获得一个http的连接。

有些网站为了安全起见,会对请求的http连接进行过滤,因此为了伪装这个http的连接请求,我们给httpHeader穿一件伪装服。

下面的setHeader方法展示了一些非常常用的典型的httpHeader的伪装方法。

比较重要的有:

Uer-Agent模拟从Ubuntu的firefox浏览器发出的请求;

Referer模拟浏览器请求的前一个触发页面,例如从skycn站点来下载软件的话,Referer设置成skycn的首页域名就可以了;

Range就是这个连接获取的流文件的起始区间。

privatevoidsetHeader(URLConnectioncon){

con.setRequestProperty("

User-Agent"

"

Mozilla/5.0(X11;

U;

Linuxi686;

en-US;

rv:

1.9.0.3)Gecko/2008092510Ubuntu/8.04(hardy)Firefox/3.0.3"

);

Accept-Language"

en-us,en;

q=0.7,zh-cn;

q=0.3"

Accept-Encoding"

aa"

Accept-Charset"

ISO-8859-1,utf-8;

q=0.7,*;

q=0.7"

Keep-Alive"

300"

Connection"

keep-alive"

If-Modified-Since"

Fri,02Jan200917:

00:

05GMT"

If-None-Match"

\"

1261d8-4290-df64d224\"

"

Cache-Control"

max-age=0"

Referer"

http:

//"

另外,为了避免线程因为网络原因而阻塞,设置了ConnectTimeout和ReadTimeout,代码⑤、⑥处。

setConnectTimeout设置的连接的超时时间,而setReadTimeout设置的是读取数据的超时时间,发生超时的话,就会抛出socketTimeout异常,两个方法的参数都是超时的毫秒数。

这里对超时的发生,采用的是等候一段时间重新连接的方法。

整个获取网络连接并读取下载数据的过程都包含在一个循环之中(代码③处),如果发生了连接或者读取数据的超时,在抛出的异常里面就会sleep一定的时间(代码⑩处),然后continue,再次尝试获取连接并读取数据,这个时间可以通过setSleepSeconds()方法来设置。

我们在迅雷等下载工具的使用中,经常可以看到状态栏会输出类似“连接超时,等待*秒后重试”的话,这个就是通过ConnectTimeout,ReadTimeout来实现的。

连接建立好之后,我们要检查一下返回响应的状态码。

常见的HttpResponseCode有以下几种:

a)200OK一切正常,对GET和POST请求的应答文档跟在后面。

b)206PartialContent客户发送了一个带有Range头的GET请求,服务器完成。

c)404NotFound无法找到指定位置的资源。

这也是一个常用的应答。

d)414RequestURITooLongURI太长。

e)416RequestedRangeNotSatisfiable服务器不能满足客户在请求中指定的Range头。

f)500InternalServerError服务器遇到了意料不到的情况,不能完成客户的请求。

g)503ServiceUnavailable服务器由于维护或者负载过重未能应答。

例如,Servlet可能在数据库连接池已满的情况下返回503。

在这些状态里面,只有200与206才是我们需要的正确的状态。

所以在代码⑥处,进行了状态码的判断,如果返回不符合要求的状态码,则结束线程,返回主线程并提示报错。

假设一切正常,下面我们就要考虑从网络中读数据了。

正如我之前在分析mysql的数据库驱动中看的一样,网络中发送数据都是以数据包的形式来发送的,也就是说不管是客户端向服务器发出的请求数据,还是从服务器返回给客户端的响应数据,都会被拆分成若干个小型数据包在网络中传递,等数据包到达了目的地,网络接口会依据数据包的编号来组装它们,成为完整的比特数据。

因此,我们可以想到在这里也是一样的,我们用inputStream的read方法来通过网卡从网络中读取数据,并不一定一次就能把所有的数据包都读完,所以我们要不断的循环来从inputStream中读取数据。

Read方法有一个int型的返回值,表示每次从inputStream中读取的字节数,如果把这个inputStream中的数据读完了,那么就返回-1。

Read方法最多可以有三个参数,byteb[]是读取数据之后存放的目标数组,off标识了目标数组中存储的开始位置,len是想要读取的数据长度,这个长度必定不能大于b[]的长度。

publicsynchronizedintread(byteb[],intoff,intlen);

我们的目标是要把目标地址的内容下载下来,现在分了5个线程来分段下载,那么这些分段下载的数据保存在哪里呢?

如果把它们都保存在内存中是非常糟糕的做法,如果文件相当之大,例如是一个视频的话,难道把这么大的数据都放在内存中吗,这样的话,万一连接中断,那前面下载的东西就都没有了?

我们当然要想办法及时的把下载的数据刷到磁盘上保存下来。

当用bt下载视频的时候,通常都会有个临时文件,当视频完全下载结束之后,这个临时文件就会被删除,那么下次继续下载的时候,就会接着上次下载的点继续下载。

所以我们的outputStream就是往这个临时文件来输出了。

OutputStream的write方法和上面InputStream的read方法有类似的参数,byteb[]是输出数据的来源,off标识了开始位置,len是数据长度。

publicsynchronizedvoidwrite(byteb[],intoff,intlen)throwsIOException;

在往临时文件的outputStream中写数据的时候,我会加上一个计数器,每满5000个比特就往文件中flush一下(代码⑦处)。

对于输出流的flush,有些要注意的地方,在程序中有三个地方调用了outputStream.flush()。

第一个是在循环的读取网络数据并往outputStream中写入的时候,每满5000个byte就flush一下(代码⑦处);

第二个是循环之后(代码⑧处),这时候正常的读取写入操作已经完成,但是outputStream中还有没有刷入磁盘的数据,所以要flush一下才能关闭连接;

第三个就是在异常中的flush(代码⑨处),因为如果发生了连接超时或者读取数据超时的话,就会直接跑到catch的exception中去,这个时候outputStream中的数据如果不flush的话,重新连接的时候这部分数据就会丢失了。

另外,当抛出异常,重新连接的时候,下载的起始位置也要重新设置(代码④处),count就是用来标识已经下载的字节数的,把count+startPosition就是新一次连接需要的下载起始位置了。

3、现在每个分段的下载线程都顺利结束了,也都创建了相应的临时文件,接下来在主线程中会对临时文件进行合并,并写入目标文件,最后删除临时文件。

这部分很简单,就是一个对所有下载线程进行遍历的过程。

这里outputStream也有两次flush,与上面类似,不再赘述。

/***//**authorbyhttp:

//www.guihua.org*/

privatevoidtempFileToTargetFile(ChildThread[]childThreads){

BufferedOutputStreamoutputStream=newBufferedOutputStream(

newFileOutputStream(fileDir+fileName));

//遍历所有子线程创建的临时文件,按顺序把下载内容写入目标文件中

if(statusError){

for(intk=0;

k<

k++){

if(childThreads[k].tempFile.length()==0)

childThreads[k].tempFile.delete();

System.out.println("

本次下载任务不成功,请重新设置线程数。

break;

BufferedInputStreaminputStream=newBufferedInputStream(

newFileInputStream(childThreads[i].tempFile));

Nowisfile"

+childThreads[i].id);

intlen=0;

intcount=0;

byte[]b=newbyte[1024];

while((len=inputStream.read(b))!

=-1){

count+=len;

outputStream.write(b,0,len);

if((count%5000)==0){

outputStream.flush();

//b=newbyte[1024];

inputStream.close();

//删除临时文件

if(childThreads[i].status==ChildThread.STATUS_HAS_FINISHED){

childThreads[i].tempFile.delete();

outputStream.close();

}catch(FileNotFoundExceptione){

}catch(IOExceptione){

4、最后,说说断点续传,前面为了实现断点续传,在每个下载线程中都创建了一个临时文件,现在我们就要利用这个临时文件来设置断点的位置。

由于临时文件的命名方式都是固定的,所以我们就专门找对应下载的目标文件的临时文件,临时文件中已经下载的字节数就是我们需要的断点位置。

startPos是一个数组,存放了每个线程的已下载的字节数。

//第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。

privatelong[]setThreadBreakpoint(StringfileDir2,StringfileName2,

longcontentLength,long[]startPos){

Filefile=newFile(fileDir+fileName);

longlocalFileSize=file.length();

if(file.exists()){

file"

+fileName+"

hasexists!

//下载的目标文件已存在,判断目标文件是否完整

if(localFileSize<

contentLength){

Nowdownloadcontinue"

//遍历目标文件的所有临时文件,设置断点的位置,即每个临时文件的长度

FiletempFileDir=newFile(fileDir);

File[]files=tempFileDir.listFiles();

files.length;

StringtempFileName=files[k].getName();

//临时文件的命名方式为:

目标文件名+"

_"

+编号

if(tempFileName!

=null&

&

files[k].length()>

0

&

tempFileName.startsWith(fileName+"

)){

intfileLongNum=Integer.parseInt(tempFileName

.substring(tempFileName.lastIndexOf("

)+1,

tempFileName.lastIndexOf("

)+2));

//为每个线程设置已下载的位置

startPos[fileLongNum]=files[k].length();

}els

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 表格模板 > 合同协议

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1