一個簡單的多線程、斷點下載Java程序

 

因爲公司不允許用fg之類的軟件,所以就搞了這個東西來下載東西。程序比較簡單,尚有多處地方沒有優化。其實這種多線程下載的難點主要是下載任務的分配 下,打個比方,一個文件的某個部分應該給哪個線程下載?爲了簡單(另一方面是我不願多想),所以分配算法也比較簡單,直接分成一塊塊,然後每個線程下載一塊。如果讀者有留意Flashget之類的軟件下載時的過程圖的話,應該會發現它們的算法比這裏的好很多。

這裏我用HttpURLConnection下載,你也可以用HttpClient或者自己實現一個Http協議(不過貌似沒有必要)

其次,你可能發現我這裏效仿迅雷,一個任務生成兩個文件,一個是任務描述文件,一個是真正的下載文件,而Flahget則是隻有一個文件。在任務描述文件裏,我把前4K用來做一些描述,然後之後的用於記錄下載的過程。

另外,這裏寫文件沒有實現緩存寫之類的功能,不過那些功能做起來不難。

最後,希望你轉載文章的時候,麻煩保留作者信息。(夏威夷雪人 or 書蟲)

 

 

Java代碼 複製代碼
  1. //這個是任務Bean  
  2.   
  3. public class Task {  
  4.   
  5.   
  6.   
  7.   private String downURL;  
  8.   
  9.   
  10.   
  11.   private String saveFile;  
  12.   
  13.     
  14.   
  15.   private int bufferSize = 64 * 1024;  
  16.   
  17.     
  18.   
  19.   private int workerCount;  
  20.   
  21.     
  22.   
  23.   private int sectionCount;  
  24.   
  25.     
  26.   
  27.   private long contentLength;  
  28.   
  29.     
  30.   
  31.   private long[] sectionsOffset;  
  32.   
  33.     
  34.   
  35.   public static final int HEAD_SIZE = 4096;  
  36.   
  37.     
  38.   
  39.   //讀下載描述文件內容  
  40.   
  41.   public synchronized void read(RandomAccessFile file) throws IOException {  
  42.   
  43.     byte[] temp = new byte[HEAD_SIZE];  
  44.   
  45.     file.seek(0);  
  46.   
  47.     int readed = file.read(temp);  
  48.   
  49.     if (readed != temp.length) {  
  50.   
  51.       throw new RuntimeException();  
  52.   
  53.     }  
  54.   
  55.     ByteArrayInputStream bais = new ByteArrayInputStream(temp);  
  56.   
  57.     DataInputStream dis = new DataInputStream(bais);  
  58.   
  59.     downURL = dis.readUTF();  
  60.   
  61.     saveFile = dis.readUTF();  
  62.   
  63.     sectionCount = dis.readInt();  
  64.   
  65.     contentLength = dis.readLong();  
  66.   
  67.       
  68.   
  69.     sectionsOffset = new long[sectionCount];  
  70.   
  71.     for (int i = 0; i < sectionCount; i++) {  
  72.   
  73.       sectionsOffset[i] = file.readLong();  
  74.   
  75.     }  
  76.   
  77.   }  
  78.   
  79.     
  80.   
  81.   //創建下載描述文件內容  
  82.   
  83.   public synchronized void create(RandomAccessFile file) throws IOException {  
  84.   
  85.     if (sectionCount != sectionsOffset.length) {  
  86.   
  87.       throw new RuntimeException();  
  88.   
  89.     }  
  90.   
  91.     long len = HEAD_SIZE + 8 * sectionCount;  
  92.   
  93.     file.setLength(len);  
  94.   
  95.     ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  96.   
  97.     DataOutputStream dos = new DataOutputStream(baos);  
  98.   
  99.     dos.writeUTF(downURL);  
  100.   
  101.     dos.writeUTF(saveFile);  
  102.   
  103.     dos.writeInt(sectionCount);  
  104.   
  105.     dos.writeLong(contentLength);  
  106.   
  107.     byte[] src = baos.toByteArray();  
  108.   
  109.     byte[] temp = new byte[HEAD_SIZE];  
  110.   
  111.     System.arraycopy(src, 0, temp, 0, src.length);  
  112.   
  113.     file.seek(0);  
  114.   
  115.     file.write(temp);  
  116.   
  117.     writeOffset(file);  
  118.   
  119.   }  
  120.   
  121.     
  122.   
  123.   //更新下載的過程  
  124.   
  125.   public synchronized void writeOffset(RandomAccessFile file) throws IOException {  
  126.   
  127.     if (sectionCount != sectionsOffset.length) {  
  128.   
  129.       throw new RuntimeException();  
  130.   
  131.     }  
  132.   
  133.     file.seek(HEAD_SIZE);  
  134.   
  135.     for (int i = 0; i < sectionsOffset.length; i++) {  
  136.   
  137.       file.writeLong(sectionsOffset[i]);  
  138.   
  139.     }  
  140.   
  141.   }  
  142.   
  143.   (下面是Getter、Setter)  
  144.   
  145. }  
  146.   
  147.   
  148.   
  149. //這個是下載主程序  
  150.   
  151.   
  152.   
  153. public class TaskAssign {  
  154.   
  155.   
  156.   
  157.   public void work(Task task) throws IOException {  
  158.   
  159.     File file = new File(task.getSaveFile());  
  160.   
  161.     if (file.exists()) {  
  162.   
  163.       return;  
  164.   
  165.     }  
  166.   
  167.     //這個是記錄是否下載成功。我這裏也沒有增加失敗回覆、重試之類的工作。  
  168.   
  169.     final AtomicBoolean success = new AtomicBoolean(true);  
  170.   
  171.     //任務描述文件  
  172.   
  173.     File taskFile = new File(task.getSaveFile() + ".r_task");  
  174.   
  175.     //真正下載文件  
  176.   
  177.     File saveFile = new File(task.getSaveFile() + ".r_save");  
  178.   
  179.     boolean taskFileExist = taskFile.exists();  
  180.   
  181.     RandomAccessFile taskRandomFile = null;  
  182.   
  183.     RandomAccessFile downRandomFile = null;  
  184.   
  185.     try {  
  186.   
  187.       taskRandomFile = new RandomAccessFile(taskFile, "rw");  
  188.   
  189.       downRandomFile = new RandomAccessFile(saveFile, "rw");  
  190.   
  191.       long rtnLen = getContentLength(task.getDownURL());  
  192.   
  193.       if (!taskFileExist) {  
  194.   
  195.         //如果文件不存在,就初始化任務文件下載文件  
  196.   
  197.         task.setContentLength(rtnLen);  
  198.   
  199.         initTaskFile(taskRandomFile, task);  
  200.   
  201.         downRandomFile.setLength(rtnLen);  
  202.   
  203.       } else {  
  204.   
  205.         //任務文件存在就讀取任務文件  
  206.   
  207.         task.read(taskRandomFile);  
  208.   
  209.         if (task.getContentLength() != rtnLen) {  
  210.   
  211.           throw new RuntimeException();  
  212.   
  213.         }  
  214.   
  215.       }  
  216.   
  217.       int secCount = task.getSectionCount();  
  218.   
  219.       //分配線程去下載,這裏用到線程池  
  220.   
  221.       ExecutorService es = Executors.newFixedThreadPool(task.getWorkerCount());  
  222.   
  223.       for (int i = 0; i < secCount; i++) {  
  224.   
  225.         final int j = i;  
  226.   
  227.         final Task t = task;  
  228.   
  229.         final RandomAccessFile f1 = taskRandomFile;  
  230.   
  231.         final RandomAccessFile f2 = downRandomFile;  
  232.   
  233.         es.execute(new Runnable() {  
  234.   
  235.           public void run() {  
  236.   
  237.             try {  
  238.   
  239.               down(f1, f2, t, j);  
  240.   
  241.             } catch (IOException e) {  
  242.   
  243.               success.set(false);  
  244.   
  245.               e.printStackTrace(System.out);  
  246.   
  247.             }  
  248.   
  249.           }  
  250.   
  251.         });  
  252.   
  253.       }  
  254.   
  255.       es.shutdown();  
  256.   
  257.       try {  
  258.   
  259.         es.awaitTermination(24 * 3600, TimeUnit.SECONDS);  
  260.   
  261.       } catch (InterruptedException e) {  
  262.   
  263.         e.printStackTrace();  
  264.   
  265.       }  
  266.   
  267.       taskRandomFile.close();  
  268.   
  269.       taskRandomFile = null;  
  270.   
  271.       downRandomFile.close();  
  272.   
  273.       downRandomFile = null;  
  274.   
  275.       //如果下載成功,去掉任務描述文件、幫下載文件改名  
  276.   
  277.       if (success.get()) {  
  278.   
  279.         taskFile.delete();  
  280.   
  281.         saveFile.renameTo(file);  
  282.   
  283.       }  
  284.   
  285.     } finally {  
  286.   
  287.       if (taskRandomFile != null) {  
  288.   
  289.         taskRandomFile.close();  
  290.   
  291.         taskRandomFile = null;  
  292.   
  293.       }  
  294.   
  295.       if (downRandomFile != null) {  
  296.   
  297.         downRandomFile.close();  
  298.   
  299.         downRandomFile = null;  
  300.   
  301.       }  
  302.   
  303.     }  
  304.   
  305.   }  
  306.   
  307.     
  308.   
  309.   public void down(RandomAccessFile taskRandomFile, RandomAccessFile downRandomFile, Task task, int sectionNo) throws IOException {  
  310.   
  311.     //這裏我用HttpURLConnection下載,你也可以用HttpClient或者自己實現一個Http協議(不過貌似沒有必要)  
  312.   
  313.     URL u = new URL(task.getDownURL());  
  314.   
  315.     HttpURLConnection conn = (HttpURLConnection) u.openConnection();  
  316.   
  317.     long start = task.getSectionsOffset()[sectionNo];  
  318.   
  319.     long end = -1;  
  320.   
  321.     //這裏要注意一下,這裏是計算當前塊的長度  
  322.   
  323.     if (sectionNo < task.getSectionCount() - 1) {  
  324.   
  325.       long per = task.getContentLength() / task.getSectionCount();  
  326.   
  327.       end = per * (sectionNo + 1);  
  328.   
  329.     } else {  
  330.   
  331.       end = task.getContentLength();  
  332.   
  333.     }  
  334.   
  335.     if (start >= end) {  
  336.   
  337.       System.out.println("Section has finished before. " + sectionNo);  
  338.   
  339.       return;  
  340.   
  341.     }  
  342.   
  343.     String range = "bytes=" + start + "-" + (end - 1);  
  344.   
  345.     conn.setRequestProperty("Range", range);  
  346.   
  347.     conn.setRequestProperty("User-Agent""Ray-Downer");  
  348.   
  349.     try {  
  350.   
  351.       conn.connect();  
  352.   
  353.       if (conn.getResponseCode() != 206) {  
  354.   
  355.         throw new RuntimeException();  
  356.   
  357.       }  
  358.   
  359.       if (conn.getContentLength() != (end - start)) {  
  360.   
  361.         throw new RuntimeException();  
  362.   
  363.       }  
  364.   
  365.       InputStream is = conn.getInputStream();  
  366.   
  367.       byte[] temp = new byte[task.getBufferSize()];  
  368.   
  369.       BufferedInputStream bis = new BufferedInputStream(is, temp.length);  
  370.   
  371.       int readed = 0;  
  372.   
  373.       while ((readed = bis.read(temp)) > 0) {  
  374.   
  375.         long offset = task.getSectionsOffset()[sectionNo];  
  376.   
  377.         synchronized (task) {  
  378.   
  379.           //下載之後順便更新描述文件,你可能會發現這裏效率比較低,在一個線程同步裏進行兩次文件操作。你可以自己實現一個緩衝寫。  
  380.   
  381.           downRandomFile.seek(offset);  
  382.   
  383.           downRandomFile.write(temp, 0, readed);  
  384.   
  385.           offset += readed;  
  386.   
  387.           task.getSectionsOffset()[sectionNo] = offset;  
  388.   
  389.           task.writeOffset(taskRandomFile);  
  390.   
  391.         }  
  392.   
  393.       }  
  394.   
  395.     } finally {  
  396.   
  397.       conn.disconnect();  
  398.   
  399.     }  
  400.   
  401.     System.out.println("Section finished. " + sectionNo);  
  402.   
  403.   }  
  404.   
  405.     
  406.   
  407.   public void initTaskFile(RandomAccessFile taskRandomFile, Task task) throws IOException {  
  408.   
  409.     int secCount = task.getSectionCount();  
  410.   
  411.     long per = task.getContentLength() / secCount;  
  412.   
  413.     long[] sectionsOffset = new long[secCount];  
  414.   
  415.     for (int i = 0; i < secCount; i++) {  
  416.   
  417.       sectionsOffset[i] = per * i;  
  418.   
  419.     }  
  420.   
  421.     task.setSectionsOffset(sectionsOffset);  
  422.   
  423.     task.create(taskRandomFile);  
  424.   
  425.   }  
  426.   
  427.     
  428.   
  429.   public long getContentLength(String url) throws IOException {  
  430.   
  431.     URL u = new URL(url);  
  432.   
  433.     HttpURLConnection conn = (HttpURLConnection) u.openConnection();  
  434.   
  435.     try {  
  436.   
  437.       return conn.getContentLength();  
  438.   
  439.     } finally {  
  440.   
  441.       conn.disconnect();  
  442.   
  443.     }  
  444.   
  445.   }  
  446.   
  447. }  
  448.   
  449.   
  450.   
  451. //稍微測試一下。  
  452.   
  453. public class Main {  
  454.   
  455.     
  456.   
  457.   public static void main(String[] args) throws IOException {  
  458.   
  459.     test3();  
  460.   
  461.     System.out.println("/n/n===============/nFinished.");  
  462.   
  463.   }  
  464.   
  465.    
  466.   
  467.   public static void test1() throws IOException {  
  468.   
  469.     Task task = new Task();  
  470.   
  471.     task.setDownURL("http://61.152.235.21/qqfile/qq/2007iistable/QQ2007IIKB1.exe");  
  472.   
  473.     task.setSaveFile("H:/Test2.exe");  
  474.   
  475.     task.setSectionCount(200);  
  476.   
  477.     task.setWorkerCount(100);  
  478.   
  479.     task.setBufferSize(256 * 1024);  
  480.   
  481.     TaskAssign ta = new TaskAssign();  
  482.   
  483.     ta.work(task);  
  484.   
  485.   }  
  486.   
  487.     
  488.   
  489.   public static void test2() throws IOException {  
  490.   
  491.     Task task = new Task();  
  492.   
  493.     task.setDownURL("http://student1.scut.edu.cn:8880/manage/news/data/1208421861893.xls");  
  494.   
  495.     task.setSaveFile("H:/Test3.xls");  
  496.   
  497.     task.setSectionCount(5);  
  498.   
  499.     task.setWorkerCount(1);  
  500.   
  501.     task.setBufferSize(128 * 1024);  
  502.   
  503.     TaskAssign ta = new TaskAssign();  
  504.   
  505.     ta.work(task);  
  506.   
  507.   }  
  508.   
  509.     
  510.   
  511.   public static void test3() throws IOException {  
  512.   
  513.     Task task = new Task();  
  514.   
  515.     task.setDownURL("http://go.microsoft.com/fwlink/?linkid=57034");  
  516.   
  517.     task.setSaveFile("H:/vc2005express.iso");  
  518.   
  519.     task.setSectionCount(500);  
  520.   
  521.     task.setWorkerCount(200);  
  522.   
  523.     task.setBufferSize(128 * 1024);  
  524.   
  525.     TaskAssign ta = new TaskAssign();  
  526.   
  527.     ta.work(task);  
  528.   
  529.   }  
  530.   
  531.     
  532.   
  533.   public static void test4() throws IOException {  
  534.   
  535.     Task task = new Task();  
  536.   
  537.     task.setDownURL("http://down.sandai.net/Thunder5.7.9.472.exe");  
  538.   
  539.     task.setSaveFile("H:/Thunder.exe");  
  540.   
  541.     task.setSectionCount(30);  
  542.   
  543.     task.setWorkerCount(30);  
  544.   
  545.     task.setBufferSize(128 * 1024);  
  546.   
  547.     TaskAssign ta = new TaskAssign();  
  548.   
  549.     ta.work(task);  
  550.   
  551.   }  
  552.   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章