剛學(xué)了下多線程的下載,可能是初次接觸的原因吧,理解起來覺得稍微有點難。所以想寫一篇博客來記錄下,加深自己理解的同時,也希望能夠幫到一些剛接觸的小伙伴。由于涉及到網(wǎng)絡(luò)的傳輸,那么就會涉及到http協(xié)議。建議在讀本文之前您對http協(xié)議有一定的了解。
創(chuàng)新互聯(lián)建站自2013年創(chuàng)立以來,先為廣德等服務(wù)建站,廣德等地企業(yè),進行企業(yè)商務(wù)咨詢服務(wù)。為廣德企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
線程可以通俗的理解為下載的通道,一個線程就是文件下載的一個通道,多線程就是同時打開了多個通道對文件進行下載。當(dāng)服務(wù)器提供下載服務(wù)時,用戶之間共享帶寬,在優(yōu)先級相同的情況下,總服務(wù)器會對總下載線程進行平均分配。我們平時用的迅雷下載就是多線程下載。
1: 獲取目標(biāo)文件的大小(totalSize)
按照常識,我們在下載一個文件之前,通常情況下是要知道該文件的大小,這樣才好在本地留好足量的空間來存儲,免得出現(xiàn)還未下載完,存儲空間就爆了的情況。為了方便代碼的演示,本文在本地tomcat服務(wù)器的webapps/ROOT目錄下新建一個test.txt的文件,里面存儲了0123456789這10字節(jié)的數(shù)據(jù)。
2: 確定要開啟幾個線程(threadCount)
需要的文件在服務(wù)器上,那我們要開通幾個通道去下載呢?一般情況下這是由CPU去決定的,但是CPU開啟線程的數(shù)目也是有限的,不是想開幾個線程就開幾個線程。所開線程的最大數(shù)量=(CPU核數(shù)+1),例如你的CPU核數(shù)為4,那么電腦最多可以開啟5條線程。為了方便代碼演示,本文的threadCount=3
3: 計算平均每個線程需要下載多少個字節(jié)的數(shù)據(jù)(blockSize)
理想情況下多線程下載是按照平均分配原則的,即:單線程下載的字節(jié)數(shù)等于文件總大小除以開啟的線程總條數(shù),當(dāng)不能整除時,則最后開啟的線程將剩余的字節(jié)一起下載。例如:本文中的totalSize=10,threadCount=3,則前兩個開啟的線程下載3KB的數(shù)據(jù),第三個開啟的線程需要下載(3+1)KB的數(shù)據(jù)。
4:計算各個線程要下載的字節(jié)范圍。
平時我們做項目講究分工明確,同理多線程下載也需要明確各個下載的字節(jié)范圍,這樣才能將文件高效、快速、準(zhǔn)確的下載下來。即在下載過程中,各個線程都要明確自己的開始索引(startIndex)和結(jié)束索引(endIndex)。
從上圖我們可以總結(jié)出一個公式:
startIndex = threadId乘以blockSize;
endIndex = (threadId+1)乘以blockSize-1;
如果是最后一條線程,那么結(jié)束索引為:
endIndex = totalSize - 1;
5: 使用for循環(huán)開啟3個子線程
//每次循環(huán)啟動一條線程下載
for(int threadId=0; threadId<3;threadId++){
/**
* 計算各個線程要下載的字節(jié)范圍
*/
//開始索引
int startIndex = threadId * blockSize;
//結(jié)束索引
int endIndex = (threadId+1)* blockSize-1;
//如果是最后一條線程(因為最后一條線程可能會長一點)
if(threadId == (threadCount -1)){
endIndex = totalSize -1;
}
/**
* 啟動子線程下載
*/
new DownloadThread(threadId,startIndex,endIndex).start();
}
6:獲取各個線程的目標(biāo)文件的開始索引和結(jié)束索引的范圍。
告訴服務(wù)器,只要目標(biāo)段的數(shù)據(jù),這樣就需要通過Http協(xié)議的請求頭去設(shè)置(range:bytes=0-499 )
connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);
7:使用RandomAccessFile隨機文件訪問類。創(chuàng)建一個RandomAccessFile對象,將返回的字節(jié)流寫到文件指定的范圍
此處有個注意事項:讓RandomAccessFile對象寫字節(jié)流之前,需要移動RandomAccessFile對象到指定的位置開始寫。
raf.seek(startIndex);
以上就是多線程下載的大致步驟。代碼如下:
package com.example;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class DownloadTest {
private static final String path = "http://localhost:8080/test.txt";
public static void main(String[] args) throws Exception {
/**
* 1.獲取目標(biāo)文件的大小
*/
int totalSize = new URL(path).openConnection().getContentLength();
System.out.println("目標(biāo)文件的總大小為:"+totalSize+"B");
/**
*2. 確定開啟幾個線程
*開啟線程的總數(shù)=CPU核數(shù)+1;例如:CPU核數(shù)為4,則最多可開啟5條線程
*/
int availableProcessors = Runtime.getRuntime().availableProcessors();
System.out.println("CPU核數(shù)是:"+availableProcessors);
int threadCount = 3;
/**
* 3. 計算每個線程要下載多少個字節(jié)
*/
int blockSize = totalSize/threadCount;
//每次循環(huán)啟動一條線程下載
for(int threadId=0; threadId<3;threadId++){
/**
* 4.計算各個線程要下載的字節(jié)范圍
*/
//開始索引
int startIndex = threadId * blockSize;
//結(jié)束索引
int endIndex = (threadId+1)* blockSize-1;
//如果是最后一條線程(因為最后一條線程可能會長一點)
if(threadId == (threadCount -1)){
endIndex = totalSize -1;
}
/**
* 5.啟動子線程下載
*/
new DownloadThread(threadId,startIndex,endIndex).start();
}
}
//下載的線程類
private static class DownloadThread extends Thread{
private int threadId;
private int startIndex;
private int endIndex;
public DownloadThread(int threadId, int startIndex, int endIndex) {
super();
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public void run(){
System.out.println("第"+threadId+"條線程,下載索引:"+startIndex+"~"+endIndex);
//每條線程要去×××器拿取目標(biāo)段的數(shù)據(jù)
try {
//創(chuàng)建一個URL對象
URL url = new URL(path);
//開啟網(wǎng)絡(luò)連接
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
//添加配置
connection.setConnectTimeout(5000);
/**
* 6.獲取目標(biāo)文件的[startIndex,endIndex]范圍
*/
//告訴服務(wù)器,只要目標(biāo)段的數(shù)據(jù),這樣就需要通過Http協(xié)議的請求頭去設(shè)置(range:bytes=0-499 )
connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);
connection.connect();
//獲取響應(yīng)碼,注意,由于服務(wù)器返回的是文件的一部分,因此響應(yīng)碼不是200,而是206
int responseCode = connection.getResponseCode();
//判斷響應(yīng)碼的值是否為206
if (responseCode == 206) {
//拿到目標(biāo)段的數(shù)據(jù)
InputStream is = connection.getInputStream();
/**
* 7:創(chuàng)建一個RandomAccessFile對象,將返回的字節(jié)流寫到文件指定的范圍
*/
//獲取文件的信息
String fileName = getFileName(path);
//rw:表示創(chuàng)建的文件即可讀也可寫。
RandomAccessFile raf = new RandomAccessFile("d:/"+fileName, "rw");
/**
* 注意:讓raf寫字節(jié)流之前,需要移動raf到指定的位置開始寫
*/
raf.seek(startIndex);
//將字節(jié)流數(shù)據(jù)寫到file文件中
byte[] buffer = new byte[1024];
int len = 0;
while((len=is.read(buffer))!=-1){
raf.write(buffer, 0, len);
}
//關(guān)閉資源
is.close();
raf.close();
System.out.println("第 "+ threadId +"條線程下載完成 !");
} else {
System.out.println("下載失敗,響應(yīng)碼是:"+responseCode);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//獲取文件的名稱
private static String getFileName(String path){
//http://localhost:8080/test.txt
int index = path.lastIndexOf("/");
String fileName = path.substring(index+1);
return fileName ;
}
}
示例代碼運行結(jié)果如下:
目標(biāo)文件的總大小為:10B
CPU核數(shù)是:4
第0個線程,下載索引:0~2
第1個線程,下載索引:3~5
第2個線程,下載索引:6~9
第1個線程下載完成!
第2個線程下載完成!
第0個線程下載完成!
好了,本文寫到此為止。以上是我個人對多線程下載的初步理解,如有不妥之處,還望大家多多指點,感謝!讓我們共同學(xué)習(xí),一起進步。