支持断点续传Java多线程下载

mousefat

贡献于2012-04-01

字数:18346 关键词: Java开发 Java

支持断点续传java多线程下载 2008年07月01日 星期二 12:55 最近一个项目有个非B/S也非C/S架构的离线申报系统,这个系统涉及到版本更新。没做过多线程下载,所以看了一些例子,结果都比较让人失望,有个多线程下载的例子是启动两个线程下载两个文件,倒,当时差点打110了。还有些例子也不完整,完整的能下载,但下载的文件打不开,没办法,还是自己来了。 界面预览 先说下思路吧 主要类包括 DownInfo.java 线程下载信息类 DownloadFrame.java 下载界面 DownloadHis.java 下载历史记录,定时记录下载信息,为断点续传提供基础 DownloadMain.java 下载主线程 DownloadStat.java 下载信息统计类 DownThread.java 下载子线程 ResponseCode.java response响应编码 UpdateService.java 更新服务类,用于读取本地版本信息,服务器更新信息,该类需要自己实现 1 从客户端读取版本及服务器地址等信息 2 通过webservice将本地版本号与服务器版本号进行比对,如果服务器发布的版本号大于客户端版本号,则从服务器查找要更新的文件。 3 启动主线程,根据线程个数划分每个线程的下载范围。 4启动下载子线程开始下载,子线程将下载的数据量调用统计类进行累计。 4 通过下载统计类得到下载信息,包括下载速度、下载量、剩余时间估计、完成百分比等等。 5 通过下载信息存档类定时将下载信息写入磁盘,作为断点续传的基础。 源码如下: 1 从客户端读取版本及服务器地址等信息 2 通过webservice将本地版本号与服务器版本号进行比对,如果服务器发布的版本号大于客户端版本号,则从服务器查找要更新的文件。 3 启动主线程,根据线程个数划分每个线程的下载范围。 4启动下载子线程开始下载,子线程将下载的数据量调用统计类进行累计。 4 通过下载统计类得到下载信息,包括下载速度、下载量、剩余时间估计、完成百分比等等。 5 通过下载信息存档类定时将下载信息写入磁盘,作为断点续传的基础。 源码如下: 1 从客户端读取版本及服务器地址等信息 2 通过webservice将本地版本号与服务器版本号进行比对,如果服务器发布的版本号大于客户端版本号,则从服务器查找要更新的文件。 3 启动主线程,根据线程个数划分每个线程的下载范围。 4启动下载子线程开始下载,子线程将下载的数据量调用统计类进行累计。 4 通过下载统计类得到下载信息,包括下载速度、下载量、剩余时间估计、完成百分比等等。 5 通过下载信息存档类定时将下载信息写入磁盘,作为断点续传的基础。 源码如下: /** * 线程下载信息 * @author zean */ public class DownInfo { private String threadId;//线程id private BigDecimal startPos=new BigDecimal(0);//开始位置,随着下载增长 private BigDecimal endPos=new BigDecimal(0);//结束位置 public DownInfo(String threadId, long startPos, long endPos) { this.threadId=threadId; this.startPos = new BigDecimal(startPos); this.endPos = new BigDecimal(endPos); } public long getStartPos() { return startPos.longValue(); } public long getEndPos() { return endPos.longValue(); } public void updateStartPos(long size) { this.startPos =startPos.add(new BigDecimal(size)); } public String getThreadId() { return threadId; } public void setThreadId(String threadId) { this.threadId = threadId; } } Java实例:利用java多线程断点续传实践 2010-07-19 12:22 /** * author:annegu * date:2009-07-16 */ annegu做了一个简单的Http多线程的下载程序,来讨论一下多线程并发下载以及断点续传的问题。 这个程序的功能,就是可以分多个线程从目标地址上下载数据,每个线程负责下载一部分,并可以支持断点续传和超时重连。 下载的方法是download(),它接收两个参数,分别是要下载的页面的url和编码方式。在这个负责下载的方法中,主要分了三个步骤。第一步是用来设置断点续传时候的一些信息的,第二步就是主要的分多线程来下载了,最后是数据的合并。 1、多线程下载: /** *//** http://www.bt285.cn http://www.5a520.cn */ public String download(String urlStr, String charset) { this.charset = charset; long contentLength = 0; CountDownLatch latch = new CountDownLatch(threadNum); long[] startPos = new long[threadNum]; long endPos = 0; try { // 从url中获得下载的文件格式与名字 this.fileName = urlStr.substring(urlStr.lastIndexOf("/") + 1); this.url = new URL(urlStr); URLConnection con = url.openConnection(); setHeader(con); // 得到content的长度 contentLength = con.getContentLength(); // 把context分为threadNum段的话,每段的长度。 this.threadLength = contentLength / threadNum; // 第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,       则建立目标文件。在第4点中说明。 startPos = setThreadBreakpoint(fileDir, fileName, contentLength, startPos); //第二步,分多个线程下载文件 ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 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; } // 开启子线程,并执行。 ChildThread thread = new ChildThread(this, latch, i, startPos[i], endPos); childThreads[i] = thread; exec.execute(thread); } try { // 等待CountdownLatch信号为0,表示所有子线程都结束。 latch.await(); exec.shutdown(); // 第三步,把分段下载下来的临时文件中的内容写入目标文件中。在第3点中说明。 tempFileToTargetFile(childThreads); } catch (InterruptedException e) { e.printStackTrace(); } } 在ChildThread的构造方法中,除了设置一些从主线程中带来的id, 起始位置之外,就是新建了一个临时文件用来存放当前线程的下载数据。临时文件的命名规则是这样的:下载的目标文件名+”_”+线程编号。 现在让我们来看看从网络中读数据是怎么读的。我们通过URLConnection来获得一个http的连接。有些网站为了安全起见,会对请求的http连接进行过滤,因此为了伪装这个http的连接请求,我们给httpHeader穿一件伪装服。下面的setHeader方法展示了一些非常常用的典型的httpHeader的伪装方法。比较重要的有:Uer-Agent模拟从Ubuntu的firefox浏览器发出的请求;Referer模拟浏览器请求的前一个触发页面,例如从skycn站点来下载软件的话,Referer设置成skycn的首页域名就可以了;Range就是这个连接获取的流文件的起始区间。 private void setHeader(URLConnection con) { con.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3"); con.setRequestProperty("Accept-Language", "en-us,en;q=0.7,zh-cn;q=0.3"); con.setRequestProperty("Accept-Encoding", "aa"); con.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); con.setRequestProperty("Keep-Alive", "300"); con.setRequestProperty("Connection", "keep-alive"); con.setRequestProperty("If-Modified-Since", "Fri, 02 Jan 2009 17:00:05 GMT"); con.setRequestProperty("If-None-Match", "\"1261d8-4290-df64d224\""); con.setRequestProperty("Cache-Control", "max-age=0"); con.setRequestProperty("Referer", "http://http://www.bt285.cn"); } 另外,为了避免线程因为网络原因而阻塞,设置了ConnectTimeout和ReadTimeout,代码⑤、⑥处。setConnectTimeout设置的连接的超时时间,而setReadTimeout设置的是读取数据的超时时间,发生超时的话,就会抛出socketTimeout异常,两个方法的参数都是超时的毫秒数。 这里对超时的发生,采用的是等候一段时间重新连接的方法。整个获取网络连接并读取下载数据的过程都包含在一个循环之中(代码③处),如果发生了连接或者读取数据的超时,在抛出的异常里面就会sleep一定的时间(代码⑩处),然后continue,再次尝试获取连接并读取数据,这个时间可以通过setSleepSeconds()方法来设置。我们在迅雷等下载工具的使用中,经常可以看到状态栏会输出类似“连接超时,等待*秒后重试”的话,这个就是通过ConnectTimeout,ReadTimeout来实现的。 连接建立好之后,我们要检查一下返回响应的状态码。常见的Http Response Code有以下几种: a) 200 OK 一切正常,对GET和POST请求的应答文档跟在后面。 b) 206 Partial Content 客户发送了一个带有Range头的GET请求,服务器完成。 c) 404 Not Found 无法找到指定位置的资源。这也是一个常用的应答。 d) 414 Request URI Too Long URI太长。 e) 416 Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头。 f) 500 Internal Server Error 服务器遇到了意料不到的情况,不能完成客户的请求。 g) 503 Service Unavailable 服务器由于维护或者负载过重未能应答。例如,Servlet可能在数据库连接池已满的情况下返回503。 在这些状态里面,只有200与206才是我们需要的正确的状态。所以在代码⑥处,进行了状态码的判断,如果返回不符合要求的状态码,则结束线程,返回主线程并提示报错。 假设一切正常,下面我们就要考虑从网络中读数据了。正如我之前在分析mysql的数据库驱动中看的一样,网络中发送数据都是以数据包的形式来发送的,也就是说不管是客户端向服务器发出的请求数据,还是从服务器返回给客户端的响应数据,都会被拆分成若干个小型数据包在网络中传递,等数据包到达了目的地,网络接口会依据数据包的编号来组装它们,成为完整的比特数据。因此,我们可以想到在这里也是一样的,我们用inputStream的read方法来通过网卡从网络中读取数据,并不一定一次就能把所有的数据包都读完,所以我们要不断的循环来从inputStream中读取数据。Read方法有一个int型的返回值,表示每次从inputStream中读取的字节数,如果把这个inputStream中的数据读完了,那么就返回-1。 Read方法最多可以有三个参数,byte b[]是读取数据之后存放的目标数组,off标识了目标数组中存储的开始位置,len是想要读取的数据长度,这个长度必定不能大于b[]的长度。 public synchronized int read(byte b[], int off, int len); 我们的目标是要把目标地址的内容下载下来,现在分了5个线程来分段下载,那么这些分段下载的数据保存在哪里呢?如果把它们都保存在内存中是非常糟糕的做法,如果文件相当之大,例如是一个视频的话,难道把这么大的数据都放在内存中吗,这样的话,万一连接中断,那前面下载的东西就都没有了?我们当然要想办法及时的把下载的数据刷到磁盘上保存下来。当用bt下载视频的时候,通常都会有个临时文件,当视频完全下载结束之后,这个临时文件就会被删除,那么下次继续下载的时候,就会接着上次下载的点继续下载。所以我们的outputStream就是往这个临时文件来输出了。 OutputStream的write方法和上面InputStream的read方法有类似的参数,byte b[]是输出数据的来源,off标识了开始位置,len是数据长度。 public synchronized void write(byte b[], int off, int len) throws IOException; 在往临时文件的outputStream中写数据的时候,我会加上一个计数器,每满5000个比特就往文件中flush一下(代码⑦处)。 对于输出流的flush,有些要注意的地方,在程序中有三个地方调用了outputStream.flush()。第一个是在循环的读取网络数据并往outputStream中写入的时候,每满5000个byte就flush一下(代码⑦处);第二个是循环之后(代码⑧处),这时候正常的读取写入操作已经完成,但是outputStream中还有没有刷入磁盘的数据,所以要flush一下才能关闭连接;第三个就是在异常中的flush(代码⑨处),因为如果发生了连接超时或者读取数据超时的话,就会直接跑到catch的exception中去,这个时候outputStream中的数据如果不flush的话,重新连接的时候这部分数据就会丢失了。另外,当抛出异常,重新连接的时候,下载的起始位置也要重新设置(代码④处),count就是用来标识已经下载的字节数的,把count+startPosition就是新一次连接需要的下载起始位置了。 3、现在每个分段的下载线程都顺利结束了,也都创建了相应的临时文件,接下来在主线程中会对临时文件进行合并,并写入目标文件,最后删除临时文件。这部分很简单,就是一个对所有下载线程进行遍历的过程。这里outputStream也有两次flush,与上面类似,不再赘述。 /** *//**author by http://www.bt285.cn http://www.guihua.org */ private void tempFileToTargetFile(ChildThread[] childThreads) { try { BufferedOutputStream outputStream = new BufferedOutputStream( new FileOutputStream(fileDir + fileName)); // 遍历所有子线程创建的临时文件,按顺序把下载内容写入目标文件中 for (int i = 0; i < threadNum; i++) { if (statusError) { for (int k = 0; k < threadNum; k++) { if (childThreads[k].tempFile.length() == 0) childThreads[k].tempFile.delete(); } System.out.println("本次下载任务不成功,请重新设置线程数。"); break; } BufferedInputStream inputStream = new BufferedInputStream( new FileInputStream(childThreads[i].tempFile)); System.out.println("Now is file " + childThreads[i].id); int len = 0; int count = 0; byte[] b = new byte[1024]; while ((len = inputStream.read(b)) != -1) { count += len; outputStream.write(b, 0, len); if ((count % 5000) == 0) { outputStream.flush(); } // b = new byte[1024]; } inputStream.close(); // 删除临时文件 if (childThreads[i].status == ChildThread.STATUS_HAS_FINISHED) { childThreads[i].tempFile.delete(); } } outputStream.flush(); outputStream.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } 4、最后,说说断点续传,前面为了实现断点续传,在每个下载线程中都创建了一个临时文件,现在我们就要利用这个临时文件来设置断点的位置。由于临时文件的命名方式都是固定的,所以我们就专门找对应下载的目标文件的临时文件,临时文件中已经下载的字节数就是我们需要的断点位置。startPos是一个数组,存放了每个线程的已下载的字节数。 //第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。 private long[] setThreadBreakpoint(String fileDir2, String fileName2, long contentLength, long[] startPos) { File file = new File(fileDir + fileName); long localFileSize = file.length(); if (file.exists()) { System.out.println("file " + fileName + " has exists!"); // 下载的目标文件已存在,判断目标文件是否完整 if (localFileSize < contentLength) { System.out.println("Now download continue "); // 遍历目标文件的所有临时文件,设置断点的位置,即每个临时文件的长度 File tempFileDir = new File(fileDir); File[] files = tempFileDir.listFiles(); for (int k = 0; k < files.length; k++) { String tempFileName = files[k].getName(); // 临时文件的命名方式为:目标文件名+"_"+编号 if (tempFileName != null && files[k].length() > 0 && tempFileName.startsWith(fileName + "_")) { int fileLongNum = Integer.parseInt(tempFileName .substring(tempFileName.lastIndexOf("_") + 1, tempFileName.lastIndexOf("_") + 2)); // 为每个线程设置已下载的位置 startPos[fileLongNum] = files[k].length(); } } } } else { // 如果下载的目标文件不存在,则创建新文件 try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } return startPos; } 5、测试 public class DownloadStartup { private static final String encoding = "utf-8"; public static void main(String[] args) { DownloadTask downloadManager = new DownloadTask(); String urlStr =     "http://apache.freelamp.com/velocity/tools/1.4/velocity-tools-1.4.zip"; downloadManager.setSleepSeconds(5); downloadManager.download(urlStr, encoding); } } 首先来看最主要的步骤:多线程下载。 首先从url中提取目标文件的名称,并在对应的目录创建文件。然后取得要下载的文件大小,根据分成的下载线程数量平均分配每个线程需要下载的数据量,就是threadLength。然后就可以分多个线程来进行下载任务了。 在这个例子中,并没有直接显示的创建Thread对象,而是用Executor来管理Thread对象,并且用CachedThreadPool来创建的线程池,当然也可以用FixedThreadPool。CachedThreadPool在程序执行的过程中会创建与所需数量相同的线程,当程序回收旧线程的时候就停止创建新线程。FixedThreadPool可以预先新建参数给定个数的线程,这样就不用在创建任务的时候再来创建线程了,可以直接从线程池中取出已准备好的线程。下载线程的数量是通过一个全局变量threadNum来控制的,默认为5。 好了,这5个子线程已经通过Executor来创建了,下面它们就会各自为政,互不干涉的执行了。线程有两种实现方式:实现Runnable接口;继承Thread类。 ChildThread就是子线程,它作为DownloadTask的内部类,继承了Thread,它的构造方法需要5个参数,依次是一个对DownloadTask的引用,一个CountDownLatch,id(标识线程的id号),startPosition(下载内容的开始位置),endPosition(下载内容的结束位置)。 这个CountDownLatch是做什么用的呢? 现在我们整理一下思路,要实现分多个线程来下载数据的话,我们肯定还要把这多个线程下载下来的数据进行合。主线程必须等待所有的子线程都执行结束之后,才能把所有子线程的下载数据按照各自的id顺序进行合并。CountDownLatch就是来做这个工作的。 CountDownLatch用来同步主线程,强制主线程等待所有的子线程执行的下载操作完成。在主线程中,CountDownLatch对象被设置了一个初始计数器,就是子线程的个数5个,代码①处。在新建了5个子线程并开始执行之后,主线程用CountDownLatch的await()方法来阻塞主线程,直到这个计数器的值到达0,才会进行下面的操作,代码②处。 对每个子线程来说,在执行完下载指定区间与长度的数据之后,必须通过调用CountDownLatch的countDown()方法来把这个计数器减1。 2、在全面开启下载任务之后,主线程就开始阻塞,等待子线程执行完毕,所以下面我们来看一下具体的下载线程ChildThread。 /** *//** *author by http://www.5a520.cn http://www.feng123.com */ public class ChildThread extends Thread { public static final int STATUS_HASNOT_FINISHED = 0; public static final int STATUS_HAS_FINISHED = 1; public static final int STATUS_HTTPSTATUS_ERROR = 2; private DownloadTask task; private int id; private long startPosition; private long endPosition; private final CountDownLatch latch; private File tempFile = null; //线程状态码 private int status = ChildThread.STATUS_HASNOT_FINISHED; public ChildThread(DownloadTask task, CountDownLatch latch,    int id, long startPos, long endPos) { super(); this.task = task; this.id = id; this.startPosition = startPos; this.endPosition = endPos; this.latch = latch; try { tempFile = new File(this.task.fileDir + this.task.fileName + "_" + id); if(!tempFile.exists()){ tempFile.createNewFile(); } } catch (IOException e) { e.printStackTrace(); } } public void run() { System.out.println("Thread " + id + " run "); HttpURLConnection con = null; InputStream inputStream = null; BufferedOutputStream outputStream = null; int count = 0; long threadDownloadLength = endPosition - startPosition; try { outputStream = new BufferedOutputStream(new FileOutputStream        (tempFile.getPath(), true)); } catch (FileNotFoundException e2) { e2.printStackTrace(); } ③ for(;;){ ④ startPosition += count; try { //打开URLConnection con = (HttpURLConnection) task.url.openConnection(); setHeader(con); con.setAllowUserInteraction(true); //设置连接超时时间为10000ms ⑤ con.setConnectTimeout(10000); //设置读取数据超时时间为10000ms con.setReadTimeout(10000); if(startPosition < endPosition){ //设置下载数据的起止区间 con.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); System.out.println("Thread " + id + " startPosition is "                + startPosition); System.out.println("Thread " + id + " endPosition is "                + endPosition); //判断http status是否为HTTP/1.1 206 Partial Content或者200 OK //如果不是以上两种状态,把status改为STATUS_HTTPSTATUS_ERROR ⑥ if (con.getResponseCode() != HttpURLConnection.HTTP_OK && con.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) { System.out.println("Thread " + id + ": code = " + con.getResponseCode() + ", status = " + con.getResponseMessage()); status = ChildThread.STATUS_HTTPSTATUS_ERROR; this.task.statusError = true; outputStream.close(); con.disconnect(); System.out.println("Thread " + id + " finished."); latch.countDown(); break; } inputStream = con.getInputStream(); int len = 0; byte[] b = new byte[1024]; while ((len = inputStream.read(b)) != -1) { outputStream.write(b, 0, len); count += len; //每读满5000个byte,往磁盘上flush一下 if(count % 5000 == 0){ ⑦ outputStream.flush(); } } System.out.println("count is " + count); if(count >= threadDownloadLength){ hasFinished = true; } ⑧ outputStream.flush(); outputStream.close(); inputStream.close(); con.disconnect(); } System.out.println("Thread " + id + " finished."); latch.countDown(); break; } catch (IOException e) { try { ⑨ outputStream.flush(); ⑩ TimeUnit.SECONDS.sleep(getSleepSeconds()); } catch (InterruptedException e1) { e1.printStackTrace(); } catch (IOException e2) { e2.printStackTrace(); } continue; } } } } 这两天把原来写的多线程下载程序整理了一下,考虑到原来的都是散文件,使用起来也不方便,所以决定把其写JAR,这样,使用起来也方便。并且增加使用XML保存下载文件以便下次再次下载,也修正了原来的一些BUG,只要你的电脑允许,想同时有多少个下载就有多少个下载。        这里我有一个示例,因为这里用到了JDOM处理XML文件,本来是想把用到的JDOM也把包到downFile.jar(下载地址:http://dl2.csdn.net/down4/20070710/10102631756.rar,源程序下载地址:http://dl2.csdn.net/down4/20070710/10140804846.rar)(第一次修正后的jar:(又传不上来了))里面的,可是试了好多次、好几种方法都不行,所以你首先得把JDOM配好,然后再把我的downFile.jar也放在类路径里面,直接运行就可以看到效果,在保存文件目录下面会生成一个保存文件下载的XML文件,该文件不能够自动删除,可以删除也可以不删除,并不会影响到程序的运行,这个文件主要是方便于断点续传,下面是示例源程序,很简单的: import com.downfile.*; /* *请先配置好JDOM及downFile.jar *作者:http://blog.csdn.net/fenglibing *希望听到各位的见意,里面肯定还有很多的不足,如果需要源程序,请给我联系。 */ public class DownFileTest {     public DownFileTest() {     }     public static void main(String[] args) {         String urlFile; //网络地址         int threadNum; //文件下载线程数         String localAddress; //本地地址         urlFile = "http://www.netbox.cn:88/download/nbsetup.EXE";         //现在还只能够处理1到9个线程,后面的的版本再增加         threadNum = 9;         //注:路径的形式一定要写出下面的格式,特别的最好的路径称号不能缺         localAddress = "d:\\multiDownTest\\";//这个文件夹如果不存在,会自动创建                       //这里采用三个软件同时下载,当然你可以多下载,使用就是这么方便         DownFile downFile = new DownFile(urlFile, threadNum, localAddress);         downFile.startDownFile();                 urlFile = "http://dl.360safe.com/setup.exe";         DownFile downFile1 = new DownFile(urlFile, threadNum, localAddress);         downFile1.startDownFile();                  urlFile = "http://down.sandai.net/Thunder5.6.8.329.exe";         DownFile downFile2 = new DownFile(urlFile, threadNum, localAddress);         downFile2.startDownFile();     } } -------------------------------------------------- BUG修正: 抓出来的BUG,错误分析及解决方案: 1、发生错误,说是找不到文件 原因,肯定是在合并的时候,又在删除文件,并且删除文件比合并先完成,而不是合成完成后再删除文件 java.io.FileNotFoundException: d:\multiDownTest\tmp0\setup.exe.part27.tp (系统找不到指定的路径。) at java.io.FileOutputStream.open(Native Method) at java.io.FileOutputStream.(FileOutputStream.java:179) at java.io.FileOutputStream.(FileOutputStream.java:70) at com.downfile.GetFileThread.run(GetFileThread.java:148) at java.lang.Thread.run(Thread.java:595) 解决:原来的利用线程去检测临时文件夹中是否还有.tp临时文件,如果没有就执行合并及删除操作,      假设当前只有一个临时文件,而恰恰就在这个时假完成了,那么下面就没有临时文件了,那此      我就执行了合并删除操作,而我的程序去还要去删除那些根本没有产生的文件,所以就会报错,      解决方法:下面的“为什么找不到文件”      2、在程序运行的过程中,到生成的临时文件夹中去查看,如要求生成30个线程,可是在临时文件夹下面 只能够看到10多个左右的临时文件,表明线程并没有完成生成 有两个原因: 1)本身程序的问题,就是没有把线程抛出完,就却执行下一个操作,是还没有来得急产生需要数目的线程,    另外的线程在执行中 解决办法:如下“为什么找不到文件” 为什么找不到文件: 我想可能是这样的,因为线程在创建的时候,有可能只创建只创建了一个线程,就被其它的线程给抢走了, 而检查当前文件夹下面是否还有.tp文件就刚好在这个文件下载完成的时候去检测,那它就认为没有.tp文件了, 解决办法: 1):是把检测时间设长一点 2):一定要当前是二十个(如线程数为20)文件才能够说是完成了 3):两者同时使用,要保证当前文件夹下面文件的数目是我们设定的设定的线程数目才去检测是否已经完成    检测时间可不设那么长,因为这要拖延时间,这就会间接的产生超时错误    3、java.net.ConnectException: Connection timed out: connect,连接超时错误,如果一个线程    的操作时间过长,超过了默认的等待时间,那么线程就会报操作超时的错误,因为在当前程序中操作I/O的    时候比较多,为了尽量减少超时的可能性,我通过程序设定了超时的时间为15秒。当然,并不能够完全避免    超时的错误,只能够说尽量的避免,这还与个人电脑的配置及网络的带宽有一定的联系了。 4、出现数字格式化错误: java.lang.NumberFormatException: null at java.lang.Long.parseLong(Long.java:372) at java.lang.Long.parseLong(Long.java:461) at com.downfile.MultiThreadGetFile.init(MultiThreadGetFile.java:148) at com.downfile.MultiThreadGetFile.run(MultiThreadGetFile.java:196) at java.lang.Thread.run(Thread.java:595) 原因:在格式化取得文件长度为长整性时发生错误,一时有可能当前网络一时阻塞,没能够获得文件的长度,      就会出错,原来没有在这个方面处理一下,现在一直去读该文件去读,直到一定的次数后才放弃,这样      就保证不能够读到文件长度的机率大大减少,增强的程序的健状性。 注:该程序在运行的时候还是有可能出现连接超时的情况,如果出现这种情况,请再尝试调用程序,此时程序会自动进行断点续传,前提是没有更改下载线程数及文件存放位置。 断点续传Java版 2010-06-09 11:06 package test; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; //断点续传 public class DownLoad { public static void down(String URL, long nPos, String savePathAndFile) {    try {     URL url = new URL(URL);     HttpURLConnection httpConnection = (HttpURLConnection) url       .openConnection();     // 设置User-Agent     httpConnection.setRequestProperty("User-Agent", "NetFox");     // 设置断点续传的开始位置     httpConnection.setRequestProperty("RANGE", "bytes=" + nPos);     // 获得输入流     InputStream input = httpConnection.getInputStream();     RandomAccessFile oSavedFile = new RandomAccessFile(savePathAndFile,       "rw");     // 定位文件指针到nPos位置     oSavedFile.seek(nPos);     byte[] b = new byte[1024];     int nRead;     // 从输入流中读入字节流,然后写到文件中     while ((nRead = input.read(b, 0, 1024)) > 0) {      (oSavedFile).write(b, 0, nRead);     }     httpConnection.disconnect();    } catch (MalformedURLException e) {     e.printStackTrace();    } catch (IOException e) {     e.printStackTrace();    } } public static long getRemoteFileSize(String url) {    long size = 0;    try {     HttpURLConnection conn = (HttpURLConnection) (new URL(url))       .openConnection();     size = conn.getContentLength();     conn.disconnect();    } catch (Exception e) {     e.printStackTrace();    }    return size; } public static void main(String[] args) {    String url = "http://www.videosource.cgogo.com/media/0/16/8678/8678.flv";    String savePath = "F:\\";    String fileName = url.substring(url.lastIndexOf("/"));    String fileNam = fileName;    HttpURLConnection conn = null;    try {     conn = (HttpURLConnection) (new URL(url)).openConnection();    } catch (Exception e) {     e.printStackTrace();    }    File file = new File(savePath + fileName);    // 获得远程文件大小    long remoteFileSize = getRemoteFileSize(url);    System.out.println("远程文件大小=" + remoteFileSize);    int i = 0;    if (file.exists()) {     // 先看看是否是完整的,完整,换名字,跳出循环,不完整,继续下载     long localFileSize = file.length();     System.out.println("已有文件大小为:" + localFileSize);     if (localFileSize < remoteFileSize) {      System.out.println("文件续传");      down(url, localFileSize, savePath + fileName);     } else {      System.out.println("文件存在,重新下载");      do {       i++;       fileName = fileNam.substring(0, fileNam.indexOf(".")) + "("         + i + ")" + fileNam.substring(fileNam.indexOf("."));       file = new File(savePath + fileName);      } while (file.exists());      try {       file.createNewFile();      } catch (IOException e) {       e.printStackTrace();      }      down(url, 0, savePath + fileName);     }     // 下面表示文件存在,改名字    } else {     try {      file.createNewFile();      System.out.println("下载中...............");      down(url, 0, savePath + fileName);     } catch (IOException e) {      e.printStackTrace();     }    } } }

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ]
3 人已下载

下载文档

相关文档