支持断点续传java多线程下载Word格式.docx
- 文档编号:19185716
- 上传时间:2023-01-04
- 格式:DOCX
- 页数:24
- 大小:35.51KB
支持断点续传java多线程下载Word格式.docx
《支持断点续传java多线程下载Word格式.docx》由会员分享,可在线阅读,更多相关《支持断点续传java多线程下载Word格式.docx(24页珍藏版)》请在冰豆网上搜索。
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
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 支持 断点续传 java 多线程 下载