1. 程式人生 > >【HTML】Http分段下載詳解

【HTML】Http分段下載詳解

多線程 ces 數值 alt locks www. 支持 read rand

一.為什麽需要Http分段下載

在實際的業務開發中,大文件使用Http普通下載非常容易OOM(內存溢出)或是鏈接超時的錯誤,這種情況下應該就應該考慮使用Http的分段下載了。下面筆者為你介紹,Http協議如何實現分段下載。

二.Http協議的結構介紹

在正式開始之前,這裏筆者先介紹一下Http的報文結構。Http的報文結構是由狀態行、頭部、空行、主體組成。在這裏讀者需要註意,GET請求和POST得到的報文結構是不一樣的,GET請求無請求數據,而POST請求有請求數據。關於Http的詳細信息,讀者可以參考Http協議詳解,而我們接下來討論的Range就是請求頭部中的一個字段。

三.Http協議的頭部Range介紹
Range字段是Http1.1開始新增加的,Http1.1和傳統的Http1.0相比,最大的特點就是解決1.0中不能支持多請求的缺點。判斷一個WEB服務器是否支持分段下載可以通過查看 返回頭是否有Accept-Ranges:Byte 字段。分段下載分為兩種,一種就是一次請求一個字段,一種就是一次請求多個字段。關於超文本傳輸的報文信息,讀者可以通過filter、burp或是瀏覽器的控制臺中查看報文的信息。

(一)一次請求一個分段

下面看一下Range字段常用表示的寫法:
Range: bytes=0-1024 獲取最前面1025個字節
Range: bytes=-500 獲取最後500個字節

Range: bytes=1025- 獲取從1025開始到文件末尾所有的字節
Range: 0-0 獲取第一個字節
Range: -1 獲取最後一個字節
例如,在一個請求頭中有Range:byte=0-1024,那麽表示的意思就是請求數據的前1025個字節。
如果這個分段請求的返回碼是206,並且指示的分段範圍是0-1024,文件的總大小是7877,那麽在響應頭中的數據應該表示為:
Content-Range: bytes 0-1024/7877

(二)一次請求多個分段

多個分段和單個分段相差無幾,只需要在請求頭的分段中添加多個分段區域就可以了。

例如:在請求頭出現Range:byte:0-1024,2000-3000,表示的含義就是請求前1025個字節信息,和從2000到3000字節的信息。
如果分段請求的返回狀態碼是206,那麽Content-Range的返回值和一次請求單個分段一樣。


四.使用Java實現文件分段下載

接下來筆者結合Java實現一個多線程分段下載的功能。使用Java多線程實現實現這個功能的大致思想,使用多個線程負責文件的子模塊的下載,每個線程都要記錄好下載的結束位置,以便於作為下個線程下載的開始位置。直接上代碼:

多線程的下載類:

技術分享
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CountDownLatch;

public class MutiThreadDownLoad {
    /**
     * 同時下載的線程數
     */
    private int threadCount;
    /**
     * 服務器請求路徑
     */
    private String serverPath;
    /**
     * 本地路徑
     */
    private String localPath;
    /**
     * 線程計數同步輔助
     */
    private CountDownLatch latch;
 
    public MutiThreadDownLoad(int threadCount, String serverPath, String localPath, CountDownLatch latch) {
        this.threadCount = threadCount;
        this.serverPath = serverPath;
        this.localPath = localPath;
        this.latch = latch;
    }
 
    public void executeDownLoad() {
 
        try {
            URL url = new URL(serverPath);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5000);//設置超時時間
            conn.setRequestMethod("GET");//設置請求方式
            int code = conn.getResponseCode();
            if (code == 200) {
                //服務器返回的數據的長度,實際上就是文件的長度,單位是字節
                int length = conn.getContentLength();
                System.out.println("文件總長度:" + length + "字節(B)");
                RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");
                //指定創建的文件的長度
                raf.setLength(length);
                raf.close();
                //分割文件
                int blockSize = length / threadCount;
                for (int threadId = 1; threadId <= threadCount; threadId++) {
                    //第一個線程下載的開始位置
                    int startIndex = (threadId - 1) * blockSize;
                    int endIndex = startIndex + blockSize - 1;
                    if (threadId == threadCount) {
                        //最後一個線程下載的長度稍微長一點
                        endIndex = length;
                    }
                    System.out.println("線程" + threadId + "下載:" + startIndex + "字節~" + endIndex + "字節");
                    new DownLoadThread(threadId, startIndex, endIndex).start();
                }
 
            }
 
        } catch (Exception e) {
            e.printStackTrace();
        }
 
 
    }
 
 
    /**
     * 內部類用於實現下載
     */
    public class DownLoadThread extends Thread {
        /**
         * 線程ID
         */
        private int threadId;
        /**
         * 下載起始位置
         */
        private int startIndex;
        /**
         * 下載結束位置
         */
        private int endIndex;
 
        public DownLoadThread(int threadId, int startIndex, int endIndex) {
            this.threadId = threadId;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
        }
 
 
        @Override
        public void run() {
 
            try {
                System.out.println("線程" + threadId + "正在下載...");
                URL url = new URL(serverPath);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                //請求服務器下載部分的文件的指定位置
                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
                conn.setConnectTimeout(5000);
                int code = conn.getResponseCode();
 
                System.out.println("線程" + threadId + "請求返回code=" + code);
 
                InputStream is = conn.getInputStream();//返回資源
                RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");
                //隨機寫文件的時候從哪個位置開始寫
                raf.seek(startIndex);//定位文件
 
                int len = 0;
                byte[] buffer = new byte[1024];
                while ((len = is.read(buffer)) != -1) {
                    raf.write(buffer, 0, len);
                }
                is.close();
                raf.close();
                System.out.println("線程" + threadId + "下載完畢");
                //計數值減一
                latch.countDown();
 
            } catch (Exception e) {
                e.printStackTrace();
            }
 
        }
    }
}
MutiThreadDownLoad

接下來是測試文件:

技術分享
import java.util.concurrent.CountDownLatch;

public class ClientTest{
    public static void main(String[] args) {
 
        int threadSize = 4;
        String serverPath = "https://www.baidu.com";
        String localPath = "NewsReader.apk";
        CountDownLatch latch = new CountDownLatch(threadSize);
        MutiThreadDownLoad m = new MutiThreadDownLoad(threadSize, serverPath, localPath, latch);
        long startTime = System.currentTimeMillis();
        try {
            m.executeDownLoad();
            latch.await();//等待所有的線程執行完畢
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("全部下載結束,共耗時" + (endTime - startTime) / 1000 + "s");
    }
}
ClientTest

【HTML】Http分段下載詳解