多執行緒斷點下載原理

why發表於2018-03-19

建議讀者在看之前先獨立思考一番,以下為個人實現方案

思路圖

多執行緒斷點下載原理

編碼實現

JavaSE:

public class BreakpointDownloader {

    private static final int THREAD_COUNT = 3;

    private static final String LOCAL_SAVE_FILENAME = "setup.exe";

    private static volatile int runningThreadCount = THREAD_COUNT;

    public static void main(String[] args) {
        String remoteFilePath = "http://www.voidtools.com/Everything-1.4.1.895.x86-Setup.exe";
        try {
            // 1. 獲取當前伺服器上檔案的大小
            URL url = new URL(remoteFilePath);
            HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
            httpConn.setConnectTimeout(10000);
            httpConn.setRequestMethod("GET");
            if (httpConn.getResponseCode() != 200) {
                System.out.println("伺服器返回的狀態碼是 :" + httpConn.getResponseCode());
                return;
            }
            int remoteFileLength = httpConn.getContentLength();
            System.out.println("下載的檔案大小是:" + remoteFileLength);
            // 2. 建立一個和伺服器大小一致的本地檔案 用來存放下載的檔案
            // 隨機訪問檔案寫入
            // 其中第二個引數 中的mode rwd 和 rws 區別是:rws 要求對檔案的內容或後設資料的每個更新都同步寫入到底層儲存裝置 ,後設資料是指 檔案的一些摘要、作者、日期等資訊,我們暫用不到
            // bt毀硬碟,因為會實時更新到儲存裝置,如果不指定rwd 或者rws 預設會先寫入快取區 這樣就不保證斷電等一些硬體裝置異常資料不能及時儲存到硬碟中
            RandomAccessFile storeFileRaf = new RandomAccessFile(LOCAL_SAVE_FILENAME, "rwd");
            storeFileRaf.setLength(remoteFileLength);
            storeFileRaf.close();

            // 3. 確定每一個執行緒下載的block
            int averageBlockLen = remoteFileLength / THREAD_COUNT;
            // 4. 確定每一個執行緒下載的區間
            for (int i = 1; i <= THREAD_COUNT; i++) {
                int startIndex = (i - 1) * averageBlockLen;
                int endIndex = i * averageBlockLen - 1;
                if (i == THREAD_COUNT) {
                    // 最後一個執行緒特殊一點,剩餘的全部交付給他執行下載
                    endIndex = remoteFileLength;
                }
                System.out.println("執行緒" + i + "首次分配的 指標分別是:" + startIndex + "-------->" + endIndex);
                // 5. 開啟執行緒 下載檔案
                new DownloadFileThread(startIndex, endIndex, remoteFilePath, i + "").start();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private static class DownloadFileThread extends Thread {

        private int startIndex;
        private int endIndex;
        private String path;
        private String threadName;

        public DownloadFileThread(int startIndex, int endIndex, String path, String threadName) {
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.path = path;
            this.threadName = threadName;
        }

        @Override
        public void run() {
            URL url = null;
            try {
                url = new URL(path);
                HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
                httpConn.setConnectTimeout(10000);
                httpConn.setRequestMethod("GET");

                File recordTmpFile = new File(threadName + ".txt");
                if (recordTmpFile.exists() && recordTmpFile.length() > 0) {
                    // 說明上次下載到某一個位置就停止了,本次下載從下次的位置開始進行
                    FileInputStream fis = new FileInputStream(recordTmpFile);
                    byte[] buffer = new byte[1024];
                    int read = fis.read(buffer); // 讀取到buffer了
                    String res = new String(buffer, 0, read);
                    startIndex = Integer.parseInt(res);
                    fis.close();
                }

                System.out.println("執行緒" + threadName + "真實下載位置: statIndex:" + startIndex + "------->" + "endIndex = " + endIndex);

                // 確定當前這個執行緒下載指定檔案block ,非常重要
                httpConn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
                System.out.println("指定執行緒獲取server return code = " + httpConn.getResponseCode());
                // 從伺服器上獲取全部資源返回 200, 部分資源返回206
                if (httpConn.getResponseCode() / 200 != 1) {
                    return;
                }
                InputStream inputStream = httpConn.getInputStream();// 如果不指定Range , 這個獲取的輸入流肯定是整個檔案的輸入流
                // 輸入流寫入到指定位置
                RandomAccessFile targetSaveRaf = new RandomAccessFile(LOCAL_SAVE_FILENAME, "rwd");
                targetSaveRaf.seek(startIndex);
                byte[] buffer = new byte[1024];
                int readLen;
                int totalLen = 0;
                File recordFile = new File(threadName + ".txt");
                if (!recordFile.exists()) {
                    recordFile.createNewFile();
                }
                RandomAccessFile recordProgressRaf = null;
                while ((readLen = inputStream.read(buffer)) != -1) {
                    targetSaveRaf.write(buffer, 0, readLen);
                    totalLen += readLen;
                    // 每一次例項化一個 raf, 每一次都覆蓋上一次的值,保證實時更新到本地檔案中
                    recordProgressRaf = new RandomAccessFile(recordFile, "rwd");
                    recordProgressRaf.write(String.valueOf(startIndex + totalLen).getBytes());
                    recordProgressRaf.close();
                }
                inputStream.close();
                targetSaveRaf.close();
                System.out.println("執行緒" + threadName + "下載完畢!!!");
                runningThreadCount--;
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("執行緒" + threadName + "下載失敗: " + e.getMessage());
            } finally {
                if (runningThreadCount == 0) {
                    // 所有執行緒都下載完畢且成功了 刪除進度記憶檔案
                    for (int i = 1; i <= THREAD_COUNT; i++) {
                        File file = new File(i + ".txt");
                        boolean delete = file.delete();
                        if (delete) {
                            System.out.println("記錄檔案 " + i + ".txt 刪除成功");
                        } else {
                            System.out.println("記錄檔案 " + i + ".txt 刪除失敗");
                        }
                    }
                }
            }
        }
    }

}
複製程式碼

相關文章