下載工具我想沒(méi)有幾個(gè)人不會(huì)用的吧,前段時(shí)間比較無(wú)聊,花了點(diǎn)時(shí)間用java寫(xiě)了個(gè)簡(jiǎn)單的http多線程下載程序,純粹是無(wú)聊才寫(xiě)的,只實(shí)現(xiàn)了幾個(gè)簡(jiǎn)單的功能,而且也沒(méi)寫(xiě)界面,今天正好也是一個(gè)無(wú)聊日,就拿來(lái)寫(xiě)篇文章,班門(mén)弄斧一下,覺(jué)得好給個(gè)掌聲,不好也不要噴,謝謝!
我實(shí)現(xiàn)的這個(gè)http下載工具功能很簡(jiǎn)單,就是一個(gè)多線程以及一個(gè)斷點(diǎn)恢復(fù),當(dāng)然下載是必不可少的。那么大概先整理一下要做的事情:
1、 連接資源服務(wù)器,獲取資源信息,創(chuàng)建文件
2、 切分資源,多線程下載
3、 斷點(diǎn)恢復(fù)功能
4、 下載速率統(tǒng)計(jì)
大概就這幾點(diǎn)吧,那么首先要做的就是連接資源并獲取資源信息,我這里使用了JavaSE自帶的URLConnection進(jìn)行資源連接,大致代碼如下:
1
String urlStr = “http://www.sourcelink.com/download/xxx”; //資源地址,隨便寫(xiě)的
2
3
URL url = new URL(urlStr); //創(chuàng)建URL
4
5
URLConnection con = url.openConnection(); //建立連接
6
7
contentLen = con.getContentLength(); //獲得資源長(zhǎng)度
8
9
File file = new File(filename); //根據(jù)filename創(chuàng)建一個(gè)下載文件,也會(huì)是我們最終下載所得的文件
10
很簡(jiǎn)單吧,沒(méi)錯(cuò)就是這么簡(jiǎn)單,第一步做完了,那么接下來(lái)要做第二步,切分資源,實(shí)現(xiàn)多線程。在上一步我們已經(jīng)獲得了資源的長(zhǎng)度contentLen,那么如何根據(jù)這個(gè)對(duì)資源進(jìn)行切分呢?假如我們要運(yùn)行十個(gè)線程,那么我們就先把contentLen處以10,獲得每塊的大小,然后在分別創(chuàng)建十個(gè)線程,每個(gè)線程負(fù)責(zé)其中一塊的寫(xiě)入,這就需要利用到RandomAccessFile這個(gè)類(lèi)了,這個(gè)類(lèi)提供了對(duì)文件的隨機(jī)訪問(wèn),可以指定向文件中的某一個(gè)位置進(jìn)行寫(xiě)入操作,大致代碼如下:
long subLen = contentLen / threadQut; //獲取每塊的大小

//創(chuàng)建十個(gè)線程,并啟動(dòng)線程

for (int i = 0; i < threadQut; i++)
{
DLThread thread = new DLThread(this, i + 1, subLen * i, subLen * (i + 1) - 1); //創(chuàng)建線程
dlThreads[i] = thread;
QSEngine.pool.execute(dlThreads[i]); //把線程交給線程池進(jìn)行管理
}

在這里使用到了DLThread這個(gè)類(lèi),我們先來(lái)看看這個(gè)類(lèi)的構(gòu)造方法的定義:
public DLThread(DLTask dlTask, int id, long startPos, long endPos)
第一個(gè)參數(shù)為一個(gè)DLTask,這個(gè)類(lèi)就代表一個(gè)下載任務(wù),里面主要保存這一個(gè)下載任務(wù)的信息,包括下載資源名,本地文件名等等的信息。第二個(gè)參數(shù)就是一個(gè)標(biāo)示線程的id,如果有10個(gè)線程,那么這個(gè)id就是從1到10,第三個(gè)參數(shù)startPos代表該線程從文件的哪個(gè)地方開(kāi)始寫(xiě)入,最后一個(gè)參數(shù)endPos代表寫(xiě)到哪里就結(jié)束。
我們?cè)賮?lái)看看,一個(gè)線程啟動(dòng)后,具體如何去下載,請(qǐng)看run方法:

public void run()
{
System.out.println("線程" + id + "啟動(dòng)
");
BufferedInputStream bis = null; //創(chuàng)建一個(gè)buff
RandomAccessFile fos = null;
byte[] buf = new byte[BUFFER_SIZE]; //緩沖區(qū)大小
URLConnection con = null;

try
{
con = url.openConnection(); //創(chuàng)建連接,這里會(huì)為每個(gè)線程都創(chuàng)建一個(gè)連接
con.setAllowUserInteraction(true);

if (isNewThread)
{
con.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);//設(shè)置獲取資源數(shù)據(jù)的范圍,從startPos到endPos
fos = new RandomAccessFile(file, "rw"); //創(chuàng)建RandomAccessFile
fos.seek(startPos); //從startPos開(kāi)始

} else
{
con.setRequestProperty("Range", "bytes=" + curPos + "-" + endPos);
fos = new RandomAccessFile(dlTask.getFile(), "rw");
fos.seek(curPos);
}
//下面一段向根據(jù)文件寫(xiě)入數(shù)據(jù),curPos為當(dāng)前寫(xiě)入的未知,這里會(huì)判斷是否小于endPos,
//如果超過(guò)endPos就代表該線程已經(jīng)執(zhí)行完畢
bis = new BufferedInputStream(con.getInputStream());

while (curPos < endPos)
{
int len = bis.read(buf, 0, BUFFER_SIZE);

if (len == -1)
{
break;
}
fos.write(buf, 0, len);
curPos = curPos + len;

if (curPos > endPos)
{
readByte += len - (curPos - endPos) + 1; //獲取正確讀取的字節(jié)數(shù)

} else
{
readByte += len;
}
}
System.out.println("線程" + id + "已經(jīng)下載完畢。");
this.finished = true;
bis.close();
fos.close();

} catch (IOException ex)
{
ex.printStackTrace();
throw new RuntimeException(ex);
}
}

上面的代碼就是根據(jù)startPos和endPos對(duì)文件機(jī)型寫(xiě)操作,每個(gè)線程都有自己獨(dú)立的一個(gè)資源塊,從startPos到endPos。上面的方式就是線程下載的核心,多線程搞定后,接下來(lái)就是實(shí)現(xiàn)斷點(diǎn)恢復(fù)的功能,其實(shí)斷點(diǎn)恢復(fù)無(wú)非就是記錄下每個(gè)線程完成到哪個(gè)未知,在這里我就是使用curPos進(jìn)行的記錄,大家在上面的代碼就應(yīng)該可以看到,我會(huì)記錄下每個(gè)線程的curPos,然后在線程重新啟動(dòng)的時(shí)候,就把curPos當(dāng)成是startPos,而endPost則不變即可,大家有沒(méi)注意到run方法里有一段這樣的代碼:

if (isNewThread)
{ //判斷是否斷點(diǎn),如果true,代表是一個(gè)新的下載線程,而不是斷點(diǎn)恢復(fù)
con.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);//設(shè)置獲取資源數(shù)據(jù)的范圍,從startPos到endPos
fos = new RandomAccessFile(file, "rw"); //創(chuàng)建RandomAccessFile
fos.seek(startPos); //從startPos開(kāi)始

} else
{
con.setRequestProperty("Range", "bytes=" + curPos + "-" + endPos);//使用curPos替代startPos,其他都和新創(chuàng)建一個(gè)是一樣的。
fos = new RandomAccessFile(dlTask.getFile(), "rw");
fos.seek(curPos);
}

上面就是斷點(diǎn)恢復(fù)的做法了,和新創(chuàng)建一個(gè)線程沒(méi)什么不同,只是startPos不一樣罷了,其他都一樣,不過(guò)僅僅有這個(gè)還不夠,因?yàn)槿绻绦蜿P(guān)閉的話,這些信息又是如何保存呢?例如文件名啊,每個(gè)線程的curPos啊等等,大家在使用下載軟件的時(shí)候,相信都會(huì)發(fā)現(xiàn)在軟件沒(méi)下載完的時(shí)候,在目錄下會(huì)有兩個(gè)臨時(shí)文件,而其中一個(gè)就是用來(lái)保存下載任務(wù)的信息的,如果沒(méi)有這些信息,程序是不知道該如何恢復(fù)下載進(jìn)度的。而我這里又如何實(shí)現(xiàn)的呢?我這個(gè)人比較懶,又不想再創(chuàng)建一個(gè)文件來(lái)保存信息,然后自己又要讀取信息創(chuàng)建對(duì)象,那太麻煩了,所以我想到了java提供序列化機(jī)制,我的想法就是直接把整個(gè)DLTask的對(duì)象序列化到硬盤(pán)上,上面說(shuō)過(guò)DLTask這個(gè)類(lèi)就是用來(lái)保存每個(gè)任務(wù)的信息的,所以我只要在需要恢復(fù)的時(shí)候,反序列化這個(gè)對(duì)象,就可以很容易的實(shí)現(xiàn)了斷點(diǎn)功能,我們來(lái)看看這個(gè)對(duì)象保存的信息:

public class DLTask extends Thread implements Serializable
{

private static final long serialVersionUID = 126148287461276024L;
private final static int MAX_DLTHREAD_QUT = 10; //最大下載線程數(shù)量

/** *//**
* 下載臨時(shí)文件后綴,下載完成后將自動(dòng)被刪除
*/
public final static String FILE_POSTFIX = ".tmp";
private URL url;
private File file;
private String filename;
private int id;
private int Level;
private int threadQut; //下載線程數(shù)量,用戶可定制
private int contentLen; //下載文件長(zhǎng)度
private long completedTot; //當(dāng)前下載完成總數(shù)
private int costTime; //下載時(shí)間計(jì)數(shù),記錄下載耗費(fèi)的時(shí)間
private String curPercent; //下載百分比
private boolean isNewTask; //是否新建下載任務(wù),可能是斷點(diǎn)續(xù)傳任務(wù)
private DLThread[] dlThreads; //保存當(dāng)前任務(wù)的線程

transient private DLListener listener; //當(dāng)前任務(wù)的監(jiān)聽(tīng)器,用于即時(shí)獲取相關(guān)下載信息

如上代碼,這個(gè)對(duì)象實(shí)現(xiàn)了Serializable接口,保存了任務(wù)的所有信息,還包括有每個(gè)線程對(duì)象dlThreads,這樣子就可以很容易做到斷點(diǎn)的恢復(fù)了,讓我重新寫(xiě)一個(gè)文件保存這些信息,然后在恢復(fù)的時(shí)候再根據(jù)這些信息創(chuàng)建一個(gè)對(duì)象,那簡(jiǎn)直是要我的命。這里創(chuàng)建了一個(gè)方法,用于斷點(diǎn)恢復(fù)用:

private void resumeTask()
{
listener = new DLListener(this);
file = new File(filename);

for (int i = 0; i < threadQut; i++)
{
dlThreads[i].setDlTask(this);
QSEngine.pool.execute(dlThreads[i]);
}
QSEngine.pool.execute(listener);
}

實(shí)際上就是減少了先連接資源,然后進(jìn)行切分資源的代碼,因?yàn)檫@些信息已經(jīng)都被保存在DLTask的對(duì)象下了。
看到上面的代碼,不知道大家注意到有一個(gè)對(duì)象DLListener沒(méi)有,這個(gè)對(duì)象實(shí)際上就是用于監(jiān)聽(tīng)整個(gè)任務(wù)的信息的,這里我主要用于兩個(gè)目的,一個(gè)是定時(shí)的對(duì)DLTask進(jìn)行序列化,保存任務(wù)信息,用于斷點(diǎn)恢復(fù),一個(gè)就是進(jìn)行下載速率的統(tǒng)計(jì),平均多長(zhǎng)時(shí)間進(jìn)行一個(gè)統(tǒng)計(jì)。我們先來(lái)看下它的代碼,這個(gè)類(lèi)也是一個(gè)單獨(dú)的線程:

public void run()
{

int i = 0;
BigDecimal completeTot = null; //完成的百分比
long start = System.currentTimeMillis(); //當(dāng)前時(shí)間,用于記錄開(kāi)始統(tǒng)計(jì)時(shí)間
long end = start;


while (!dlTask.isComplete())
{ //整個(gè)任務(wù)是否完成,沒(méi)有完成則繼續(xù)循環(huán)
i++;
String percent = dlTask.getCurPercent(); //獲取當(dāng)前的完成百分?jǐn)?shù)

completeTot = new BigDecimal(dlTask.getCompletedTot()); //獲取當(dāng)前完成的總字節(jié)數(shù)

//獲得當(dāng)前時(shí)間,然后與start時(shí)間比較,如果不一樣,利用當(dāng)前完成的總數(shù)除以所使用的時(shí)間,獲得一個(gè)平均下載速度
end = System.currentTimeMillis();

if (end - start != 0)
{
BigDecimal pos = new BigDecimal(((end - start) / 1000) * 1024);
System.out.println("Speed :"
+ completeTot
.divide(pos, 0, BigDecimal.ROUND_HALF_EVEN)
+ "k/s " + percent + "% completed. ");
}
recoder.record(); //將任務(wù)信息記錄到硬盤(pán)

try
{
sleep(3000);

} catch (InterruptedException ex)
{
ex.printStackTrace();
throw new RuntimeException(ex);
}

}
//以下是下載完成后打印整個(gè)下載任務(wù)的信息
int costTime =+ (int)((System.currentTimeMillis() - start) / 1000);
dlTask.setCostTime(costTime);
String time = QSDownUtils.changeSecToHMS(costTime);
dlTask.getFile().renameTo(new File(dlTask.getFilename()));
System.out.println("Download finished. " + time);
}

這個(gè)方法中的recoder.record()方法的調(diào)用就是用于序列化任務(wù)對(duì)象,其他的代碼均為統(tǒng)計(jì)信息用的,具體可看注釋?zhuān)?/span>record該方法的代碼如下:

public void record()
{
ObjectOutputStream out = null;

try
{
out = new ObjectOutputStream(new FileOutputStream(dlTask.getFilename() + ".tsk"));
out.writeObject(dlTask);
out.close();

} catch (IOException ex)
{
ex.printStackTrace();
throw new RuntimeException(ex);

} finally
{

try
{
out.close();

} catch (IOException ex)
{
ex.printStackTrace();
throw new RuntimeException(ex);
}
}

}


到這里,大致的代碼都完成了,不過(guò)以上的代碼都是部分片段,只是作為一個(gè)參考給大家看下,而且由于本人水平有限,代碼很多地方都沒(méi)有經(jīng)過(guò)過(guò)多的考慮,沒(méi)有經(jīng)過(guò)優(yōu)化,僅僅只是自?shī)首詷?lè),所以可能有很多地方都寫(xiě)的很爛,這個(gè)程序也缺乏很多功能,連界面都沒(méi)有,所以整個(gè)程序的代碼就不上傳了,免得丟人,呵呵。希望對(duì)有興趣的朋友盡到一點(diǎn)幫助吧。