多線程文件下載的服務器端及客戶端

多線程文件下載的服務器端及客戶端

本文需要引用httpClient包
httpClient下載地址
1 服務器端
簡要說明:
1.1 根據接收到的文件id,讀取服務器端的文件。
1.2 判斷request中的head是否有“Range”,如果有,代表需要支持斷點續傳功能,否則不需要。
1.3 根據Range的值判斷,如 (bytes=1000-)或(bytes=1000-2000)分別代表第1000字節以後的值,及1000到2000字節之間的值。
1.4 根據Range的值讀取文件中相應的字節返回。

/**
 * 下載文件(支持單點續傳下載)
 * 
 * @param request
 * @param fileId
 * @throws IOException
 */
@RequestMapping(value = "/file/downloadMultiThread/{fileId}", method = RequestMethod.GET)
public void download3(HttpServletRequest request, HttpServletResponse response, @PathVariable("fileId") int fileId)
        throws IOException {

    if (isTest) {
        Enumeration<String> enums = request.getHeaderNames();
        while (enums.hasMoreElements()) {
            String names = enums.nextElement();
            if (isTest) {
                System.out.println(names + ":[" + request.getHeader(names) + "]");
            }
        }
    }
    CmnTmpFile cmnTmpFile = cmnTmpFileSrvc.getCmnTmpFileByKeyId(fileId);
    if (cmnTmpFile == null) {
        return;
    }

    try {
        if (isTest) {
            log.info("請求下載的連接地址爲:[" + request.getRequestURL() + "]?[" + request.getQueryString() + "]");
        }
    } catch (IllegalArgumentException e) {
        log.error("請求下載的文件名參數爲空!");
        return;
    }

    File downloadFile = new File(cmnTmpFile.getFilePath());
    if (downloadFile.exists()) {
        if (downloadFile.isFile()) {
            if (downloadFile.length() > 0) {
            } else {
                log.info("請求下載的文件是一個空文件");
                return;
            }
            if (!downloadFile.canRead()) {
                log.info("請求下載的文件不是一個可讀的文件");
                return;
            } else {
            }
        } else {
            log.info("請求下載的文件是一個文件夾");
            return;
        }
    } else {
        log.info("請求下載的文件不存在!");
        return;
    }
    // 記錄文件大小
    long fileLength = downloadFile.length();
    // 記錄已下載文件大小
    long pastLength = 0;
    // 0:從頭開始的全文下載;
    // 1:從某字節開始的下載(bytes=1000-);
    // 2:從某字節開始到某字節結束的下載(bytes=1000-2000)
    int rangeSwitch = 0;
    // 記錄客戶端需要下載的字節段的最後一個字節偏移量(比如bytes=1000-2000,則這個值是爲2000)
    long toLength = 0;
    // 客戶端請求的字節總量
    long contentLength = 0;
    // 記錄客戶端傳來的形如“bytes=1000-”或者“bytes=1000-2000”的內容
    String rangeBytes = "";
    // 負責讀取數據
    RandomAccessFile raf = null;
    // 寫出數據
    OutputStream os = null;
    // 緩衝
    OutputStream out = null;
    // 暫存容器
    byte b[] = new byte[1024];

    if (request.getHeader("Range") != null) {
        // 客戶端請求的下載的文件塊的開始字節
        response.setStatus(javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT);
        if (isTest) {
            log.info("request.getHeader(\"Range\")=" + request.getHeader("Range"));
        }
        rangeBytes = request.getHeader("Range").replaceAll("bytes=", "");
        if (rangeBytes.indexOf('-') == rangeBytes.length() - 1) {
            // 如:bytes=1000-
            rangeSwitch = 1;
            rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
            pastLength = Long.parseLong(rangeBytes.trim());
            // 客戶端請求的是 1000之後的字節
            contentLength = fileLength - pastLength;
        } else {
            // 如:bytes=1000-2000
            rangeSwitch = 2;
            String temp0 = rangeBytes.substring(0, rangeBytes.indexOf('-'));
            String temp2 = rangeBytes.substring(rangeBytes.indexOf('-') + 1, rangeBytes.length());
            // bytes=1000-2000,從第1000個字節開始下載
            pastLength = Long.parseLong(temp0.trim());
            // bytes=1000-2000,到第2000個字節結束
            toLength = Long.parseLong(temp2);
            // 客戶端請求的是1000-2000之間的字節
            contentLength = toLength - pastLength;
        }
    } else {
        // 從開始進行下載,客戶端要求全文下載
        contentLength = fileLength;
    }

    /**
     * 如果設設置了Content -Length,則客戶端會自動進行多線程下載。如果不希望支持多線程,則不要設置這個參數。 響應的格式是:
     * Content - Length: [文件的總大小] - [客戶端請求的下載的文件塊的開始字節]
     * ServletActionContext.getResponse().setHeader("Content- Length", new
     * Long(file.length() - p).toString());
     */
    response.reset();
    // 告訴客戶端允許斷點續傳多線程連接下載,響應的格式是:Accept-Ranges: bytes
    response.setHeader("Accept-Ranges", "bytes");
    // 如果是第一次下,還沒有斷點續傳,狀態是默認的 200,無需顯式設置;響應的格式是:HTTP/1.1 200 OK

    if (pastLength != 0) {
        // 不是從最開始下載,響應的格式是:
        // Content-Range: bytes [文件塊的開始字節]-[文件的總大小 - 1]/[文件的總大小]
        if (isTest) {
            log.info("---------不是從開始進行下載!服務器即將開始斷點續傳...");
        }
        String contentRange = "";
        switch (rangeSwitch) {
        case 1:
            // 針對 bytes=1000- 的請求
            contentRange = new StringBuffer("bytes ").append(new Long(pastLength).toString()).append("-")
                    .append(new Long(fileLength - 1).toString()).append("/").append(new Long(fileLength).toString())
                    .toString();
            response.setHeader("Content-Range", contentRange);
            break;
        case 2:
            // 針對 bytes=1000-2000 的請求
            contentRange = rangeBytes + "/" + new Long(fileLength).toString();
            response.setHeader("Content-Range", contentRange);
            break;
        default:
            break;
        }
    } else {
        // 是從開始下載
        if (isTest) {
            log.info("---------是從開始進行下載!");
        }
    }

    try {
        response.addHeader("Content-Disposition", "attachment; filename=\"" + downloadFile.getName() + "\"");
        // 設置 MIME 類型.
        response.setContentType(CommonUtil.setContentType(downloadFile.getName()));
        response.addHeader("Content-Length", String.valueOf(contentLength));
        os = response.getOutputStream();
        out = new BufferedOutputStream(os);
        raf = new RandomAccessFile(downloadFile, "r");

        int readNum = 0;
        long readLength = 0;
        try {
            switch (rangeSwitch) {
            case 0:
                // 普通下載,或者從頭開始的下載,同1
            case 1:
                // 針對 bytes=1000- 的請求
                // 形如 bytes=1000- 的客戶端請求,跳過 1000 個字節
                raf.seek(pastLength);
                readNum = 0;
                while ((readNum = raf.read(b, 0, 1024)) != -1) {
                    out.write(b, 0, readNum);
                }
                break;
            case 2:
                // 針對 bytes=2000-3000 的請求
                // 形如 bytes=2000-3000 的客戶端請求,找到第 2000 個字節
                raf.seek(pastLength);
                readNum = 0;
                readLength = 0; // 記錄已讀字節數
                while (readLength <= contentLength - 1024) {
                    // 大部分字節在這裏讀取
                    readNum = raf.read(b, 0, 1024);
                    readLength += 1024;
                    out.write(b, 0, readNum);
                }
                if (readLength <= contentLength) {
                    // 餘下的不足 1024 個字節在這裏讀取
                    readNum = raf.read(b, 0, (int) (contentLength - readLength));
                    out.write(b, 0, readNum);
                }
                break;
            default:
                break;
            }
            out.flush();
            if (isTest) {
                log.info("-----------下載結束");
            }
        } catch (IOException ie) {
            /**
             * 在寫數據的時候, 對於 ClientAbortException 之類的異常,
             * 是因爲客戶端取消了下載,而服務器端繼續向瀏覽器寫入數據時,拋出這個異常,這個是正常的。
             */
            if (isTest) {
                log.info("向客戶端傳輸時出現IO異常,但此異常是允許的,有可能是客戶端取消了下載,導致此異常");
            }
        }
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    } finally {
        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                // 遠程主機或者本機強制關閉
                // log.error(e.getMessage(), e);
            } finally {
                out = null;
            }
        }
        if (raf != null) {
            try {
                raf.close();
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            } finally {
                raf = null;
            }
        }
    }
}
/**
 * 打包壓縮下載文件
 * 
 * @param response
 * @param fileId
 * @throws IOException
 */
@RequestMapping(value = "/file/downLoadZipFile/{fileId}", method = RequestMethod.GET)
public void downLoadZipFile(HttpServletResponse response, @PathVariable("fileId") int fileId) throws IOException {
    CmnTmpFile cmnTmpFile = cmnTmpFileSrvc.getCmnTmpFileByKeyId(fileId);
    if (cmnTmpFile == null) {
        return;
    }
    File file = new File(cmnTmpFile.getFilePath());
    response.setContentType("APPLICATION/OCTET-STREAM");
    response.setHeader("Content-Disposition", "attachment; filename=" + cmnTmpFile.getFileName() + ".zip");
    ZipOutputStream out = new ZipOutputStream(response.getOutputStream());
    try {
        long start = System.currentTimeMillis();
        if (isTest) {
            log.info("----------開始下載文件,文件長度[" + file.length() + "]");
        }
        // 壓縮輸出文件
        // ZipUtils.doCompress(cmnTmpFile.getFilePath(), out);
        ZipUtils.doCompress(file, out);
        response.flushBuffer();
        if (isTest) {
            System.out.println("耗時:[" + (System.currentTimeMillis() - start) + "]ms");
            log.info("----------壓縮下載文件完成");
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        out.close();
    }
}

/**
 * 下載文件(常規單線程下載)
 * 
 * @param request
 * @param fileId
 * @throws IOException
 */
@RequestMapping(value = "/file/download/{fileId}", method = RequestMethod.GET)
public void download(HttpServletRequest request, HttpServletResponse response, @PathVariable("fileId") int fileId)
        throws IOException {
    // 測試Header
    if (isTest) {
        Enumeration<String> enums = request.getHeaderNames();
        while (enums.hasMoreElements()) {
            String names = enums.nextElement();
            if (isTest) {
                System.out.println(names + ":[" + request.getHeader(names) + "]");
            }
        }
    }
    CmnTmpFile cmnTmpFile = cmnTmpFileSrvc.getCmnTmpFileByKeyId(fileId);
    if (cmnTmpFile == null) {
        return;
    }
    File file = new File(cmnTmpFile.getFilePath());
    // HttpHeaders headers = new HttpHeaders();
    // headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    // headers.setContentDispositionFormData("attachment",
    // cmnTmpFile.getFileName());
    try {
        long start = System.currentTimeMillis();
        if (isTest) {
            log.info("----------開始下載文件,文件長度[" + file.length() + "]");
        }
        if (file.exists()) {
            String filename = file.getName();
            InputStream fis = new BufferedInputStream(new FileInputStream(file));
            response.reset();
            response.setContentType("application/x-download");
            response.addHeader("Content-Disposition",
                    "attachment;filename=" + new String(filename.getBytes(), "iso-8859-1"));
            response.addHeader("Content-Length", "" + file.length());
            OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
            response.setContentType("application/octet-stream");
            byte[] buffer = new byte[1024 * 1024 * 4];
            int i = -1;
            while ((i = fis.read(buffer)) != -1) {
                toClient.write(buffer, 0, i);
            }
            fis.close();
            toClient.flush();
            toClient.close();
            try {
                response.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (isTest) {
            System.out.println("耗時:[" + (System.currentTimeMillis() - start) + "]ms");
            log.info("----------下載文件完成");
        }
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}

壓縮應用類代碼如下:

package cn.xaele.utils.zip;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 文件下載工具類
 */
public class ZipUtils {

    public static void doCompress(String srcFile, String zipFile) throws Exception {
        doCompress(new File(srcFile), new File(zipFile));
    }

    /**
     * 文件壓縮
     * 
     * @param srcFile 目錄或者單個文件
     * @param destFile 壓縮後的ZIP文件
     */
    public static void doCompress(File srcFile, File destFile) throws Exception {
        ZipOutputStream out = new ZipOutputStream(new FileOutputStream(destFile));
        if (srcFile.isDirectory()) {
            File[] files = srcFile.listFiles();
            for (File file : files) {
                doCompress(file, out);
            }
        } else {
            doCompress(srcFile, out);
        }
    }

    public static void doCompress(String pathname, ZipOutputStream out) throws IOException {
        doCompress(new File(pathname), out);
    }

    public static void doCompress(File file, ZipOutputStream out) throws IOException {
        if (file.exists()) {
            byte[] buffer = new byte[1024];
            FileInputStream fis = new FileInputStream(file);
            out.putNextEntry(new ZipEntry(file.getName()));
            int len = 0;
            // 讀取文件的內容,打包到zip文件
            while ((len = fis.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
            out.flush();
            out.closeEntry();
            fis.close();
        }
    }
}

2 客戶端
簡要說明:
2.1 客戶端要下載一個文件, 先請求服務器,然後服務器將文件傳送給客戶端,最後客戶端保存到本地, 完成下載過程。
2.2 多線程下載是在客戶端開啓多個線程同時下載,每個線程只負責下載文件其中的一部分,在客戶端進行文件組裝,當所有線程下載完的時候,文件下載完畢。 (注意:並非線程越多下載越快, 與網絡環境有很大的關係;在同等的網絡環境下,多線程下載速度要高於單線程;多線程下載佔用資源比單線程多,相當於用資源換速度)。
2.3 下載前,首先要獲取要下載文件的大小,然後用RandomAccessFile 類在客戶端磁盤創建一個同樣大小的文件,下載後的數據寫入這個文件。
2.4 在每個線程分配的下載任務中包含待下載文件取值的開始和結束位置,然後啓動下載線程下載數據。
2.5 判斷有沒有保存上次下載的臨時文件,如果有,判斷是否需要刪除或重新啓動下載線程;
2.6 在啓動線程下載的時候保存下載文件的位置信息,下載完畢後刪除當前線程產生的臨時文件。

package cn.xaele.utils.file;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * 多線程下載文件,經測試4G以上的文件也可正常下載
 */
public class DownLoadLargeFile {
    private static final Logger log = LogManager.getLogger(DownLoadLargeFile.class);
    // 測試標記
    private static boolean isTest = true;

    private CloseableHttpClient httpClient;

    public static void main(String[] args) {
        long starttimes = System.currentTimeMillis();
        DownLoadLargeFile app = new DownLoadLargeFile();

        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        // 設置整個連接池最大連接數 20
        cm.setMaxTotal(20);
        app.httpClient = HttpClients.custom().setConnectionManager(cm).build();
        try {
            app.doDownload("http://mirrors.hust.edu.cn/apache//httpcomponents/httpclient/binary/httpcomponents-client-4.5.3-bin.zip", "d:/doctohtml/");
            // app.doDownload("d:/doctohtml/httpcomponents-client-4.5.3-bin.zip.cfg");
            app.httpClient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println((System.currentTimeMillis() - starttimes) + "ms");
        cm.shutdown();
        cm.close();
        cm = null;
    }

    /**
     * 啓動多個線程下載文件
     * 
     * @param remoteUrl
     * @param localPath
     * @throws IOException
     */
    public void doDownload(String remoteUrl, String localPath) throws IOException {
        FileCfg fileCfg = new FileCfg(localPath, remoteUrl);
        if (fileCfg.getDnldStatus() == 0) {
            download(fileCfg);
        } else {
            System.out.println("解析錯誤,無法下載");
        }
    }

    /**
     * 讀取配置文件並按照其內容啓動多個線程下載文件未下載完畢的部分
     * 
     * @param cfgFile
     * @throws IOException
     */
    public void doDownload(String cfgFile) throws IOException {
        FileCfg fileCfg = new FileCfg(cfgFile);
        if (fileCfg.getDnldStatus() == 0) {
            download(fileCfg);
        } else {
            System.out.println("解析錯誤,無法下載");
        }
    }

    /**
     * 根據配置文件下載文件
     * 
     * @param fileCfg
     * @throws IOException
     */
    public void download(FileCfg fileCfg) throws IOException {
        if (fileCfg.getDnldStatus() == 0) {
            if (fileCfg.getFilePartList() != null && fileCfg.getFilePartList().size() > 0) {
                CountDownLatch end = new CountDownLatch(fileCfg.getFilePartList().size());
                ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
                boolean hasDownloadPart = false;
                for (FilePart filePart : fileCfg.getFilePartList()) {
                    if (!filePart.isFinish()) {
                        hasDownloadPart = true;
                        // 僅下載未完成的文件片段
                        DownloadThread downloadThread = new DownloadThread(filePart, end, httpClient);
                        cachedThreadPool.execute(downloadThread);
                    }
                }
                if (hasDownloadPart) {
                    try {
                        end.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 下載完成後清除臨時文件信息
                fileCfg.clearFile();
                log.debug("下載完成!{} ", fileCfg.getDnldTmpFile());
            } else {
                System.out.println("沒有需要下載的內容");
            }
        } else {
            System.out.println("解析錯誤,無法下載");
        }
    }

    public static void callback(FilePart filePart) {
        if (isTest) {
            System.out.println(">>>子線程執行之後的值是:" + filePart.toString());
        }
        // 保存線程執行結果
        File newFile = new File(filePart.getPartCfgfileName());

        try {
            // byte,char 1字節
            // short 2字節
            // int 4字節
            // long 8字節
            // Boolean 1字節
            RandomAccessFile raf = new RandomAccessFile(newFile, "rws");
            raf.seek(filePart.getThreadId() * 21 + 20);
            raf.writeBoolean(filePart.isFinish());
            raf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 待下載文件任務信息
    public class FileCfg {

        // 待下載的文件鏈接
        private String url = null;
        // 待下載的文件鏈接
        private String fileName = null;
        // 待下載的文件長度
        private long fileSize = 0l;
        // 每個線程下載的字節數
        private int unitSize = 1024000;
        // 下載狀態
        private short dnldStatus = -1;
        // 下載路徑
        private String dnldPath = "d:/download";

        private List<FilePart> filePartList = null;

        public FileCfg(String cfgFile) {
            try {
                // 讀取配置文件
                DataInputStream is = new DataInputStream(new BufferedInputStream(new FileInputStream(cfgFile)));
                this.url = is.readUTF();
                this.fileName = is.readUTF();
                this.dnldPath = is.readUTF();
                this.fileSize = is.readLong();
                this.unitSize = is.readInt();
                this.dnldStatus = is.readShort();

                // 下載片段數
                int partSize = is.readInt();
                is.close();
                if (isTest) {
                    System.out.println("FileCfg--->" + toString());
                }

                boolean reDownload = false;
                File downloadFile = new File(getDnldTmpFile());
                if (!downloadFile.exists() || !downloadFile.isFile()) {
                    // 重新下載文件
                    RandomAccessFile raf;
                    try {
                        raf = new RandomAccessFile(getDnldTmpFile(), "rw");
                        raf.setLength(fileSize);
                        raf.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    reDownload = true;
                }

                // 讀取文件下載片段信息
                filePartList = new ArrayList<>(partSize);
                is = new DataInputStream(new BufferedInputStream(new FileInputStream(cfgFile + ".part")));
                for (int i = 0; i < partSize; i++) {
                    FilePart part = new FilePart(getUrl(), getDnldTmpFile(), getDnldPartFile(), is.readInt(),
                            is.readLong(), is.readLong());
                    boolean finish = is.readBoolean();
                    if (!reDownload) {
                        part.setFinish(finish);
                    }
                    if (isTest) {
                        System.out.println(i + "--->" + part.toString());
                    }
                    filePartList.add(part);
                }
                is.close();
            } catch (IOException e) {

            }
        }

        public FileCfg(String dnldPath, String url) throws IOException {
            this.url = url;
            this.dnldPath = dnldPath;

            HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
            httpConnection.setRequestMethod("HEAD");
            int responseCode = httpConnection.getResponseCode();
            if (responseCode >= 400) {
                System.out.println("Web服務器響應錯誤!");
                return;
            }

            String lengthField = httpConnection.getHeaderField("Content-Length");
            if (lengthField != null && isLong(lengthField)) {
                System.out.println("文件大小:[" + lengthField + "]");
                this.fileSize = Long.parseLong(lengthField);
            }

            String nameField = httpConnection.getHeaderField("Content-Disposition");
            if (nameField != null) {
                String mark = "filename=\"";
                if (isTest) {
                    System.out.println("字符串:[" + nameField + "]");
                }
                int idx = nameField.indexOf(mark);
                this.fileName = nameField.substring(idx + mark.length(), nameField.length() - 1);
                // 如果沒有解析到文件名稱,則從url中獲取
                if (this.fileName == null || this.fileName.length() < 1) {
                    this.fileName = url.substring(url.lastIndexOf("/") + 1, url.length()).replace("%20", " ");
                }
                if (isTest) {
                    System.out.println("文件名稱:[" + fileName + "]");
                }
            }

            if (isTest) {
                // 讀取所有的Head信息
                httpConnection.getContentLength();
                httpConnection.getContentLengthLong();
                httpConnection.getHeaderFields();
                for (Iterator<String> iter = httpConnection.getHeaderFields().keySet().iterator(); iter.hasNext();) {
                    String key = iter.next();
                    System.out.println("[" + key + "][" + httpConnection.getHeaderField(key) + "]");
                }
            }

            calFileInfo();
        }

        public FileCfg(String dnldPath, String url, String fileName, long fileSize) {
            this.url = url;
            this.fileName = fileName;
            this.fileSize = fileSize;
            this.dnldPath = dnldPath;
            calFileInfo();
        }

        /**
         * 判斷指定的字符串是否可轉爲Long型數據
         * 
         * @param str
         * @return
         */
        public boolean isLong(String str) {
            if (str == null || str.trim().length() < 1) {
                return false;
            }
            Pattern pattern = Pattern.compile("[0-9]*");
            return pattern.matcher(str).matches();
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getFileName() {
            return fileName;
        }

        public void setFileName(String fileName) {
            this.fileName = fileName;
        }

        public long getFileSize() {
            return fileSize;
        }

        public void setFileSize(long fileSize) {
            this.fileSize = fileSize;
        }

        public int getUnitSize() {
            return unitSize;
        }

        public void setUnitSize(int unitSize) {
            this.unitSize = unitSize;
        }

        public short getDnldStatus() {
            return dnldStatus;
        }

        public void setDnldStatus(short dnldStatus) {
            this.dnldStatus = dnldStatus;
        }

        public String getDnldPath() {
            return dnldPath;
        }

        public void setDnldPath(String dnldPath) {
            this.dnldPath = dnldPath;
        }

        public List<FilePart> getFilePartList() {
            return filePartList;
        }

        public void setFilePartList(List<FilePart> filePartList) {
            this.filePartList = filePartList;
        }

        public String getDnldCfgFile() {
            return dnldPath + "/" + fileName + ".cfg";
        }

        public String getDnldPartFile() {
            return dnldPath + "/" + fileName + ".cfg.part";
        }

        public String getDnldTmpFile() {
            return dnldPath + "/" + fileName + ".tmp";
        }

        public String getDnldFile() {
            return dnldPath + "/" + fileName;
        }

        public void calFileInfo() {
            // 計算文件片段數量
            if (fileSize < 1) {
                // 沒有需要下載的文件
                dnldStatus = -2;
                return;
            }
            long filePartSize = (fileSize - 1) / unitSize + 1;
            if (filePartSize > Integer.MAX_VALUE) {
                // 文件過大,不能下載
                dnldStatus = -10;
                return;
            }

            // 構建文件片段列表
            filePartList = new ArrayList<>((int) filePartSize);
            for (int i = 0; i < filePartSize; i++) {
                long offset = i * unitSize;
                long length = unitSize;
                // 讀取數量超過文件大小
                if ((offset + length) > this.fileSize) {
                    length = this.fileSize - offset;
                }
                FilePart part = new FilePart(getUrl(), getDnldTmpFile(), getDnldPartFile(), i, offset, length);
                if (isTest) {
                    System.out.println(i + "--->" + part.toString());
                }
                filePartList.add(part);
            }
            dnldStatus = 0;

            // 構建完成,保存信息到文檔
            writeFile();

            // 檢查下載文件是否存在
            File newFile = new File(fileName);
            if (!newFile.exists() || !newFile.isFile()) {
                // 文件不存在,則重新創建,如存在,則保持原狀,用於斷點續傳
                RandomAccessFile raf;
                try {
                    raf = new RandomAccessFile(newFile, "rw");
                    raf.setLength(fileSize);
                    raf.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 將下載信息保存到文件中,以便斷點續傳
         */
        public void writeFile() {
            // 文件下載信息
            String dnldFile = getDnldCfgFile();
            // 文件片段信息
            String dnldPartFile = getDnldPartFile();

            try {
                // 保存文件下載信息到文件
                DataOutputStream os = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dnldFile)));
                os.writeUTF(url);
                os.writeUTF(fileName);
                os.writeUTF(dnldPath);
                os.writeLong(fileSize);
                os.writeInt(unitSize);
                os.writeShort(dnldStatus);
                os.writeInt(this.filePartList.size());
                os.flush();
                os.close();

                // 保存文件片段信息到文件
                os = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dnldPartFile)));
                for (int i = 0, p = filePartList.size(); i < p; i++) {
                    FilePart part = filePartList.get(i);
                    os.writeInt(part.getThreadId());
                    os.writeLong(part.getOffset());
                    os.writeLong(part.getLength());
                    os.writeBoolean(part.isFinish());
                    os.flush();
                }
                os.close();

                // 生成文件,並指定大小(與待下載的文件相同)
                File saveFile = new File(getDnldTmpFile());
                RandomAccessFile raf = new RandomAccessFile(saveFile, "rw");
                raf.setLength(fileSize);
                raf.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void clearFile() {
            // 文件下載信息
            String dnldFile = getDnldCfgFile();
            // 文件片段信息
            String dnldPartFile = getDnldPartFile();

            File file = new File(dnldFile);
            // 如果文件路徑所對應的文件存在,並且是一個文件,則直接刪除
            if (file.exists() && file.isFile()) {
                if (file.delete()) {
                    System.out.println("刪除文件" + dnldFile + "成功!");
                } else {
                    System.out.println("刪除文件" + dnldFile + "失敗!");
                }
            } else {
                System.out.println("刪除文件失敗:" + dnldFile + "不存在!");
            }

            file = new File(dnldPartFile);
            // 如果文件路徑所對應的文件存在,並且是一個文件,則直接刪除
            if (file.exists() && file.isFile()) {
                if (file.delete()) {
                    System.out.println("刪除文件" + dnldPartFile + "成功!");
                } else {
                    System.out.println("刪除文件" + dnldPartFile + "失敗!");
                }
            } else {
                System.out.println("刪除文件失敗:" + dnldPartFile + "不存在!");
            }

            // 下載完成後的臨時文件名改爲正式名稱
            File oldFile = new File(getDnldTmpFile());
            File newFile = new File(getDnldFile());
            if (oldFile.exists()) {
                // 重命名文件存在
                if (!newFile.exists()) {
                    oldFile.renameTo(newFile);
                } else {
                    // 若在該目錄下已經有一個文件和新文件名相同,則不允許重命名
                    System.out.println(newFile + "已經存在!");
                }
            }
        }

        public String toString() {
            return "isTest[" + isTest + "]url[" + url + "]fileName[" + fileName + "]fileSize[" + fileSize + "]unitSize["
                    + unitSize + "]dnldStatus[" + dnldStatus + "]dnldPath[" + dnldPath + "]filePartList["
                    + ((filePartList != null) ? filePartList.size() : 0) + "]";
        }
    }

    /**
     * 文件片段信息
     */
    public class FilePart {

        // 待下載的文件
        private String url = null;
        // 本地文件名
        private String fileName = null;
        // 本地文件名
        private String partCfgfileName = null;
        // 當前第幾個線程
        private int threadId = 0;
        // 偏移量
        private long offset = 0;
        // 分配給本線程的下載字節數
        private long length = 0;
        // 監聽本線程下載是否完成
        private boolean finish = false;

        public FilePart(String url, String fileName, String partCfgfileName, int threadId, long offset, long length) {
            this.url = url;
            this.fileName = fileName;
            this.partCfgfileName = partCfgfileName;
            this.threadId = threadId;
            this.offset = offset;
            this.length = length;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getFileName() {
            return fileName;
        }

        public void setFileName(String fileName) {
            this.fileName = fileName;
        }

        public String getPartCfgfileName() {
            return partCfgfileName;
        }

        public void setPartCfgfileName(String partCfgfileName) {
            this.partCfgfileName = partCfgfileName;
        }

        public int getThreadId() {
            return threadId;
        }

        public void setThreadId(int threadId) {
            this.threadId = threadId;
        }

        public long getOffset() {
            return offset;
        }

        public void setOffset(long offset) {
            this.offset = offset;
        }

        public long getLength() {
            return length;
        }

        public void setLength(long length) {
            this.length = length;
        }

        public boolean isFinish() {
            return finish;
        }

        public void setFinish(boolean finish) {
            this.finish = finish;
        }

        public String toString() {
            return "threadId[" + threadId + "]offset[" + offset + "]length[" + length + "]finish[" + finish + "]";
        }

    }

    /**
     * 文件下載線程
     */
    public class DownloadThread extends Thread {

        // // 待下載的文件
        // private String url = null;
        // // 本地文件名
        // private String fileName = null;
        // 待下載文件片段
        private FilePart filePart = null;
        // 通知服務器文件的取值範圍
        private String rangeStr = "";
        // 同步工具類,允許一個或多個線程一直等待,直到其他線程的操作執行完後再執行
        private CountDownLatch end;
        // http客戶端
        private CloseableHttpClient httpClient;
        // 上下文
        private HttpContext context;

        /**
         * @param url
         * @param file
         * @param part
         * @param end
         * @param hc
         */
        public DownloadThread(FilePart part, CountDownLatch end, CloseableHttpClient hc) {
            this.filePart = part;
            this.end = end;
            this.httpClient = hc;
            this.context = new BasicHttpContext();
            this.rangeStr = "bytes=" + filePart.getOffset() + "-" + (filePart.getOffset() + filePart.getLength());
            if (isTest) {
                System.out.println("rangeStr[" + rangeStr + "]");
                System.out.println("偏移量=" + filePart.getOffset() + ";字節數=" + filePart.getLength());
            }
        }

        public void run() {
            try {
                HttpGet httpGet = new HttpGet(filePart.getUrl());
                httpGet.addHeader("Range", rangeStr);
                CloseableHttpResponse response = httpClient.execute(httpGet, context);
                BufferedInputStream bis = new BufferedInputStream(response.getEntity().getContent());

                byte[] buff = new byte[1024];
                int bytesRead;
                File newFile = new File(filePart.getFileName());
                // Rws模式就是同步模式,每write修改一個byte,立馬寫到磁盤。當然性能差點兒,適合小的文件,debug模式,或者需要安全性高的時候。
                // Rwd模式跟個rws基礎的一樣,不過只對“文件的內容”同步更新到磁盤,而不對metadata同步更新。
                // 默認情形下(rw模式下),是使用buffer的,只有cache滿的或者使用RandomAccessFile.close()關閉流的時候兒才真正的寫到文件。
                // 這個會有兩個問題:
                // 1.調試麻煩的--->使用write方法修改byte的時候兒,只修改到個內存內,還沒到個文件,不能使用notepad++工具立即看見修改效果。
                // 2.當系統halt的時候兒,不能寫到文件,安全性稍微差點兒。
                RandomAccessFile raf = new RandomAccessFile(newFile, "rws");

                long offset = filePart.getOffset();
                while ((bytesRead = bis.read(buff, 0, buff.length)) != -1) {
                    raf.seek(offset);
                    raf.write(buff, 0, bytesRead);
                    offset += bytesRead;
                }
                raf.close();
                bis.close();
                // 下載線程執行完畢
                filePart.setFinish(true);
                // 調用回調函數告訴主進程該線程已執行完畢
                DownLoadLargeFile.callback(filePart);
            } catch (ClientProtocolException e) {
                log.error("文件下載異常信息:{}", ExceptionUtils.getStackTrace(e));
            } catch (IOException e) {
                log.error("文件下載異常信息:{}", ExceptionUtils.getStackTrace(e));
            } finally {
                end.countDown();
                if (isTest) {
                    log.info("剩餘線程[" + end.getCount() + "]繼續執行!");
                }
            }
        }
    }

}

多線程下載客戶端代碼下載

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