`
liangguanhui
  • 浏览: 111564 次
  • 性别: Icon_minigender_1
社区版块
存档分类
最新评论

【原创】一个简单的多线程、断点下载Java程序

阅读更多

因为公司不允许用fg之类的软件,所以就搞了这个东西来下载东西。程序比较简单,尚有多处地方没有优化。其实这种多线程下载的难点主要是下载任务的分配 下,打个比方,一个文件的某个部分应该给哪个线程下载?为了简单(另一方面是我不愿多想),所以分配算法也比较简单,直接分成一块块,然后每个线程下载一块。如果读者有留意Flashget之类的软件下载时的过程图的话,应该会发现它们的算法比这里的好很多。

这里我用HttpURLConnection下载,你也可以用HttpClient或者自己实现一个Http协议(不过貌似没有必要)

其次,你可能发现我这里效仿迅雷,一个任务生成两个文件,一个是任务描述文件,一个是真正的下载文件,而Flahget则是只有一个文件。在任务描述文件里,我把前4K用来做一些描述,然后之后的用于记录下载的过程。

另外,这里写文件没有实现缓存写之类的功能,不过那些功能做起来不难。

最后,希望你转载文章的时候,麻烦保留作者信息。(夏威夷雪人 or 书虫)

 

 

//这个是任务Bean

public class Task {



  private String downURL;



  private String saveFile;

  

  private int bufferSize = 64 * 1024;

  

  private int workerCount;

  

  private int sectionCount;

  

  private long contentLength;

  

  private long[] sectionsOffset;

  

  public static final int HEAD_SIZE = 4096;

  

  //读下载描述文件内容

  public synchronized void read(RandomAccessFile file) throws IOException {

    byte[] temp = new byte[HEAD_SIZE];

    file.seek(0);

    int readed = file.read(temp);

    if (readed != temp.length) {

      throw new RuntimeException();

    }

    ByteArrayInputStream bais = new ByteArrayInputStream(temp);

    DataInputStream dis = new DataInputStream(bais);

    downURL = dis.readUTF();

    saveFile = dis.readUTF();

    sectionCount = dis.readInt();

    contentLength = dis.readLong();

    

    sectionsOffset = new long[sectionCount];

    for (int i = 0; i < sectionCount; i++) {

      sectionsOffset[i] = file.readLong();

    }

  }

  

  //创建下载描述文件内容

  public synchronized void create(RandomAccessFile file) throws IOException {

    if (sectionCount != sectionsOffset.length) {

      throw new RuntimeException();

    }

    long len = HEAD_SIZE + 8 * sectionCount;

    file.setLength(len);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    DataOutputStream dos = new DataOutputStream(baos);

    dos.writeUTF(downURL);

    dos.writeUTF(saveFile);

    dos.writeInt(sectionCount);

    dos.writeLong(contentLength);

    byte[] src = baos.toByteArray();

    byte[] temp = new byte[HEAD_SIZE];

    System.arraycopy(src, 0, temp, 0, src.length);

    file.seek(0);

    file.write(temp);

    writeOffset(file);

  }

  

  //更新下载的过程

  public synchronized void writeOffset(RandomAccessFile file) throws IOException {

    if (sectionCount != sectionsOffset.length) {

      throw new RuntimeException();

    }

    file.seek(HEAD_SIZE);

    for (int i = 0; i < sectionsOffset.length; i++) {

      file.writeLong(sectionsOffset[i]);

    }

  }

  (下面是Getter、Setter)

}



//这个是下载主程序



public class TaskAssign {



  public void work(Task task) throws IOException {

    File file = new File(task.getSaveFile());

    if (file.exists()) {

      return;

    }

    //这个是记录是否下载成功。我这里也没有增加失败回复、重试之类的工作。

    final AtomicBoolean success = new AtomicBoolean(true);

    //任务描述文件

    File taskFile = new File(task.getSaveFile() + ".r_task");

    //真正下载的文件

    File saveFile = new File(task.getSaveFile() + ".r_save");

    boolean taskFileExist = taskFile.exists();

    RandomAccessFile taskRandomFile = null;

    RandomAccessFile downRandomFile = null;

    try {

      taskRandomFile = new RandomAccessFile(taskFile, "rw");

      downRandomFile = new RandomAccessFile(saveFile, "rw");

      long rtnLen = getContentLength(task.getDownURL());

      if (!taskFileExist) {

        //如果文件不存在,就初始化任务文件和下载文件

        task.setContentLength(rtnLen);

        initTaskFile(taskRandomFile, task);

        downRandomFile.setLength(rtnLen);

      } else {

        //任务文件存在就读取任务文件

        task.read(taskRandomFile);

        if (task.getContentLength() != rtnLen) {

          throw new RuntimeException();

        }

      }

      int secCount = task.getSectionCount();

      //分配线程去下载,这里用到线程池

      ExecutorService es = Executors.newFixedThreadPool(task.getWorkerCount());

      for (int i = 0; i < secCount; i++) {

        final int j = i;

        final Task t = task;

        final RandomAccessFile f1 = taskRandomFile;

        final RandomAccessFile f2 = downRandomFile;

        es.execute(new Runnable() {

          public void run() {

            try {

              down(f1, f2, t, j);

            } catch (IOException e) {

              success.set(false);

              e.printStackTrace(System.out);

            }

          }

        });

      }

      es.shutdown();

      try {

        es.awaitTermination(24 * 3600, TimeUnit.SECONDS);

      } catch (InterruptedException e) {

        e.printStackTrace();

      }

      taskRandomFile.close();

      taskRandomFile = null;

      downRandomFile.close();

      downRandomFile = null;

      //如果下载成功,去掉任务描述文件、帮下载文件改名

      if (success.get()) {

        taskFile.delete();

        saveFile.renameTo(file);

      }

    } finally {

      if (taskRandomFile != null) {

        taskRandomFile.close();

        taskRandomFile = null;

      }

      if (downRandomFile != null) {

        downRandomFile.close();

        downRandomFile = null;

      }

    }

  }

  

  public void down(RandomAccessFile taskRandomFile, RandomAccessFile downRandomFile, Task task, int sectionNo) throws IOException {

    //这里我用HttpURLConnection下载,你也可以用HttpClient或者自己实现一个Http协议(不过貌似没有必要)

    URL u = new URL(task.getDownURL());

    HttpURLConnection conn = (HttpURLConnection) u.openConnection();

    long start = task.getSectionsOffset()[sectionNo];

    long end = -1;

    //这里要注意一下,这里是计算当前块的长度

    if (sectionNo < task.getSectionCount() - 1) {

      long per = task.getContentLength() / task.getSectionCount();

      end = per * (sectionNo + 1);

    } else {

      end = task.getContentLength();

    }

    if (start >= end) {

      System.out.println("Section has finished before. " + sectionNo);

      return;

    }

    String range = "bytes=" + start + "-" + (end - 1);

    conn.setRequestProperty("Range", range);

    conn.setRequestProperty("User-Agent", "Ray-Downer");

    try {

      conn.connect();

      if (conn.getResponseCode() != 206) {

        throw new RuntimeException();

      }

      if (conn.getContentLength() != (end - start)) {

        throw new RuntimeException();

      }

      InputStream is = conn.getInputStream();

      byte[] temp = new byte[task.getBufferSize()];

      BufferedInputStream bis = new BufferedInputStream(is, temp.length);

      int readed = 0;

      while ((readed = bis.read(temp)) > 0) {

        long offset = task.getSectionsOffset()[sectionNo];

        synchronized (task) {

          //下载之后顺便更新描述文件,你可能会发现这里效率比较低,在一个线程同步里进行两次文件操作。你可以自己实现一个缓冲写。

          downRandomFile.seek(offset);

          downRandomFile.write(temp, 0, readed);

          offset += readed;

          task.getSectionsOffset()[sectionNo] = offset;

          task.writeOffset(taskRandomFile);

        }

      }

    } finally {

      conn.disconnect();

    }

    System.out.println("Section finished. " + sectionNo);

  }

  

  public void initTaskFile(RandomAccessFile taskRandomFile, Task task) throws IOException {

    int secCount = task.getSectionCount();

    long per = task.getContentLength() / secCount;

    long[] sectionsOffset = new long[secCount];

    for (int i = 0; i < secCount; i++) {

      sectionsOffset[i] = per * i;

    }

    task.setSectionsOffset(sectionsOffset);

    task.create(taskRandomFile);

  }

  

  public long getContentLength(String url) throws IOException {

    URL u = new URL(url);

    HttpURLConnection conn = (HttpURLConnection) u.openConnection();

    try {

      return conn.getContentLength();

    } finally {

      conn.disconnect();

    }

  }

}



//稍微测试一下。

public class Main {

  

  public static void main(String[] args) throws IOException {

    test3();

    System.out.println("\n\n===============\nFinished.");

  }

 

  public static void test1() throws IOException {

    Task task = new Task();

    task.setDownURL("http://61.152.235.21/qqfile/qq/2007iistable/QQ2007IIKB1.exe");

    task.setSaveFile("H:/Test2.exe");

    task.setSectionCount(200);

    task.setWorkerCount(100);

    task.setBufferSize(256 * 1024);

    TaskAssign ta = new TaskAssign();

    ta.work(task);

  }

  

  public static void test2() throws IOException {

    Task task = new Task();

    task.setDownURL("http://student1.scut.edu.cn:8880/manage/news/data/1208421861893.xls");

    task.setSaveFile("H:/Test3.xls");

    task.setSectionCount(5);

    task.setWorkerCount(1);

    task.setBufferSize(128 * 1024);

    TaskAssign ta = new TaskAssign();

    ta.work(task);

  }

  

  public static void test3() throws IOException {

    Task task = new Task();

    task.setDownURL("http://go.microsoft.com/fwlink/?linkid=57034");

    task.setSaveFile("H:/vc2005express.iso");

    task.setSectionCount(500);

    task.setWorkerCount(200);

    task.setBufferSize(128 * 1024);

    TaskAssign ta = new TaskAssign();

    ta.work(task);

  }

  

  public static void test4() throws IOException {

    Task task = new Task();

    task.setDownURL("http://down.sandai.net/Thunder5.7.9.472.exe");

    task.setSaveFile("H:/Thunder.exe");

    task.setSectionCount(30);

    task.setWorkerCount(30);

    task.setBufferSize(128 * 1024);

    TaskAssign ta = new TaskAssign();

    ta.work(task);

  }

}



 
分享到:
评论
3 楼 dava_gosling 2009-03-22  
dava_gosling 写道

为什么下载描述文件不用对象序列化?把下载信息装在一个Pojo类再序列化

我错了,对象序列化效率低。
2 楼 dava_gosling 2009-02-14  
为什么下载描述文件不用对象序列化?把下载信息装在一个Pojo类再序列化
1 楼 careprad 2008-05-30  
  不错

相关推荐

Global site tag (gtag.js) - Google Analytics