RxDownload2 源碼解析(三)

源碼解析,如需轉載,請註明做者:Yuloran (t.cn/EGU6c76)html

前言

造輪子者:Season_zlcjava

本文主要講述 RxDownload2 的多線程斷點下載技術服務器

斷點下載技術前提

服務器必須支持按 byte-range 下載,也就是支持 Range: bytes=xxx-xxx 請求頭。詳見 Http 協議 rfc2616 - Range多線程

下載範圍分割

很簡單,先讀取 Content-Length 響應頭,獲取文件大小,而後用文件大小除以線程數就可計算出每條線程的下載範圍。app

好比,假設文件大小是 100 bytes,下載線程數爲 3。由於 100 / 3 = 33,因此:框架

  • 線程 0 的下載範圍是 0 ~32[0 * 33 ~ (0 + 1) * 33 - 1]
  • 線程 1 的下載範圍是 33~65[1 * 33 ~ (1 + 1) * 33 - 1]
  • 線程 2 的下載範圍是 66~99[2 * 33 ~ 100 - 1]

上代碼:dom

  1. prepareDownload() [-> FileHelper.java]
public void prepareDownload(File lastModifyFile, File tempFile, File saveFile, long fileLength, String lastModify) throws IOException, ParseException {
        // 將響應頭中的上次修改時間轉爲 long 類型的 unix 時間戳,而後保存到文件中
        writeLastModify(lastModifyFile, lastModify);
        // 設置下載文件的大小、計算每條線程的下載範圍並保存到 tempFile 中
        prepareFile(tempFile, saveFile, fileLength);
    }
複製代碼
  1. prepareFile() [-> FileHelper.java]
private void prepareFile(File tempFile, File saveFile, long fileLength) throws IOException {
        RandomAccessFile rFile = null;
        RandomAccessFile rRecord = null;
        FileChannel channel = null;
        try {
            rFile = new RandomAccessFile(saveFile, ACCESS);
            rFile.setLength(fileLength);//設置下載文件的長度 

            rRecord = new RandomAccessFile(tempFile, ACCESS);
            // 下載範圍在文件中的記錄方式:|start|end|start|end|start|end|...
            // 數據類型是 long,long類型在 java 中佔 8 個字節,因此每一個線程的下載範圍都佔 16 字節
            // 因此 tempFile 的長度 RECORD_FILE_TOTAL_SIZE = 16 * 線程數
            rRecord.setLength(RECORD_FILE_TOTAL_SIZE); //設置指針記錄文件的大小

            // NIO 內存映射文件的方式讀寫二進制文件,速度更快
            channel = rRecord.getChannel();
            // 注意映射方式爲讀寫
            MappedByteBuffer buffer = channel.map(READ_WRITE, 0, RECORD_FILE_TOTAL_SIZE);

            long start;
            long end;
            // 計算並保存每條線程的下載範圍,計算方法同上面舉的例子
            int eachSize = (int) (fileLength / maxThreads);
            for (int i = 0; i < maxThreads; i++) {
                if (i == maxThreads - 1) {
                    start = i * eachSize;
                    end = fileLength - 1;
                } else {
                    start = i * eachSize;
                    end = (i + 1) * eachSize - 1;
                }
                buffer.putLong(start);
                buffer.putLong(end);
            }
        } finally {
            closeQuietly(channel);
            closeQuietly(rRecord);
            closeQuietly(rFile);
        }
    }
複製代碼

讀取下載範圍

很簡單,上面已經將每條線程的下載範圍保存到了 tempFile 中,只要再從 tempFile 中按位置讀出來就好了。工具

  1. readDownloadRange() [-> FileHelper.java]
public DownloadRange readDownloadRange(File tempFile, int i) throws IOException {
        RandomAccessFile record = null;
        FileChannel channel = null;
        try {
            // 入參 i 表示線程序號
            record = new RandomAccessFile(tempFile, ACCESS);
            channel = record.getChannel();
            MappedByteBuffer buffer = channel
                    .map(READ_WRITE, i * EACH_RECORD_SIZE, (i + 1) * EACH_RECORD_SIZE);
            long startByte = buffer.getLong();
            long endByte = buffer.getLong();
            return new DownloadRange(startByte, endByte);
        } finally {
            closeQuietly(channel);
            closeQuietly(record);
        }
    }
複製代碼

注意 MappedByteBuffer buffer = channel.map(READ_WRITE, i * EACH_RECORD_SIZE, (i + 1) * EACH_RECORD_SIZE); 這句代碼是有坑的,可是表現不出來,由於這裏的文件打開方式爲 READ_WRITE。要是改爲 READ_ONLY 就有致使讀取最後一條線程的下載範圍時拋出IllegalArgumentException(代碼靜態檢查工具 Fortify 提示要以合適的權限打開文件,我將其改成了 READ_ONLY ,發現了這一問題)。源碼分析

錯誤緣由:map() 方法的最後一個參數表示要映射的字節數,以只讀方式打開時,若參數大小超過了文件剩餘可讀字節數,就會拋出 IllegalArgumentException。而以讀寫方式打開文件時,會自動擴展文件長度,因此不會拋出異常。post

由於每段下載範圍的長度都是 EACH_RECORD_SIZE = 16 bytes,因此,上述代碼應修改成: MappedByteBuffer buffer = channel.map(READ_WRITE, i * EACH_RECORD_SIZE, EACH_RECORD_SIZE);

Intellij IDEA 示例代碼

本身寫了個示例代碼,測試了一下:

RandomAccessFile file = new RandomAccessFile("temp.txt", "rw");
        file.setLength(48);
        FileChannel channel = file.getChannel();
        MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 48);
        for (int i = 0; i < 3; i++) {
            if (i == 2) {
                buffer.putLong(i * 33).putLong(99);
            } else {
                buffer.putLong(i * 33).putLong((i + 1) * 33 - 1);
            }
        }
        channel.close();

        RandomAccessFile file1 = new RandomAccessFile("temp.txt", "r");
        FileChannel channel1 = file1.getChannel();
        for (int i = 0; i < 3; i++) {
            MappedByteBuffer buffer1 = channel1.map(FileChannel.MapMode.READ_ONLY, i * 16, 16);
            System.out.println(String.format("long1: %d", buffer1.getLong()));
            System.out.println(String.format("long2: %d", buffer1.getLong()));
        }
        channel1.close();
複製代碼

Notepad++ 裝個十六進制查看器,查看生成的 temp.txt 中的內容是否和咱們代碼寫的同樣:

temp.txt view in HEX

上面是十六進制,換算成十進制就是上面示例代碼寫的內容。

寫下載文件

很簡單,利用 RandomAccessFile 可從任意位置讀寫的屬性,分別將每條線程下載的數據寫到同一個文件的不一樣位置。

  1. saveFile() [-> FileHelper.java]
public void saveFile(FlowableEmitter<DownloadStatus> emitter, int i, File tempFile, File saveFile, ResponseBody response) {

        RandomAccessFile record = null;
        FileChannel recordChannel = null;
        RandomAccessFile save = null;
        FileChannel saveChannel = null;
        InputStream inStream = null;
        try {
            try {
                // 1.映射 tempFile 到內存中
                record = new RandomAccessFile(tempFile, ACCESS);
                recordChannel = record.getChannel();
                MappedByteBuffer recordBuffer = recordChannel
                        .map(READ_WRITE, 0, RECORD_FILE_TOTAL_SIZE);

                // i 表明線程序號,startIndex 表明該線程下載範圍的 start 字段在文件中的指針位置
                int startIndex = i * EACH_RECORD_SIZE;
                // start 表示該線程的起始下載位置
                long start = recordBuffer.getLong(startIndex);

                 // 新建一個下載狀態對象,用於發射下載進度
                DownloadStatus status = new DownloadStatus();
                // totalSize 表明文件總大小,也能夠從 saveFile 中讀出
                long totalSize = recordBuffer.getLong(RECORD_FILE_TOTAL_SIZE - 8) + 1;
                status.setTotalSize(totalSize);

                int readLen;
                byte[] buffer = new byte[2048];
                inStream = response.byteStream();

                save = new RandomAccessFile(saveFile, ACCESS);
                saveChannel = save.getChannel();

                while ((readLen = inStream.read(buffer)) != -1 && !emitter.isCancelled()) {
                    MappedByteBuffer saveBuffer = saveChannel.map(READ_WRITE, start, readLen);
                    saveBuffer.put(buffer, 0, readLen);

                    // 成功下載一段數據後,將已下載位置寫回 start 字段
                    start += readLen;
                    recordBuffer.putLong(startIndex, start);

                    // 計算已下載字節數 = 文件長度 - 每條線程剩餘未下載字節數
                    status.setDownloadSize(totalSize - getResidue(recordBuffer));
                    // 發射下載進度
                    emitter.onNext(status);
                }
                // 發射下載完成
                emitter.onComplete();
            } finally {
                closeQuietly(record);
                closeQuietly(recordChannel);
                closeQuietly(save);
                closeQuietly(saveChannel);
                closeQuietly(inStream);
                closeQuietly(response);
            }
        } catch (IOException e) {
            emitter.onError(e);
        }
    }
複製代碼

總結

  • 下載流程就不分析了,只要熟練使用下圖所示兩個快捷鍵,什麼源碼分析都是手到擒來:

  • RxDownload2 源碼解析系列至此結束,雖然框架比較簡單,可是仍是有不少值得學習的東西。尤爲是做者對 RxJava2 的使用,能夠說很是之六了。他寫的十篇 Rxjava2 教程也很是的通俗易懂,感興趣的能夠看一看。

RxDownload2 系列文章:

相關文章
相關標籤/搜索