本文實現一個支援多線程並發下載、斷點續傳、超時重連的 HTTP 下載器,核心技術點包括 HttpURLConnection Range 請求、ExecutorService 線程池、CountDownLatch 同步、臨時文件分段存儲與合併。
public String download(String urlStr, String charset) {
// 1. 解析文件名、建立連接、獲取 contentLength
// 2. 計算每線程平均長度 threadLength = contentLength / threadNum
// 3. 斷點分析:startPos = setThreadBreakpoint(...)
// 4. 建立線程池
ExecutorService exec = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
startPos[i] += threadLength * i; // 全局起始位移
long endPos = (i == threadNum - 1) ? contentLength : threadLength * (i + 1) - 1;
ChildThread t = new ChildThread(this, latch, i, startPos[i], endPos);
childThreads[i] = t;
exec.execute(t);
}
latch.await(); // 阻塞等待所有線程完成
exec.shutdown();
tempFileToTargetFile(childThreads); // 合併文件
}
public class ChildThread extends Thread {
// 構造器:建立臨時文件 fileName_" + id
public void run() {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile, true));
long downloaded = 0;
for (;;) { // 無限重試循環
startPosition += downloaded; // 斷點續傳核心:基於已下載量調整起始位置
HttpURLConnection con = (HttpURLConnection) url.openConnection();
setHeader(con); // 偽裝 User-Agent、Referer 等
con.setConnectTimeout(10000);
con.setReadTimeout(10000);
con.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition);
// 狀態碼檢查:僅接受 200/206
if (con.getResponseCode() != HTTP_OK && con.getResponseCode() != HTTP_PARTIAL) {
// 錯誤處理、countDown、break
}
InputStream in = con.getInputStream();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
downloaded += len;
if (downloaded % 5000 == 0) out.flush(); // 定期持久化
}
if (downloaded >= (endPosition - startPosition + 1)) break; // 完成
out.flush(); out.close(); in.close(); con.disconnect();
latch.countDown(); break;
} catch (IOException e) {
out.flush(); // 異常時也要持久化已下載數據
TimeUnit.SECONDS.sleep(getSleepSeconds()); // 等待重試
}
}
}
}
| 技術點 | 實現要點 |
|---|---|
| Range 請求 | con.setRequestProperty("Range", "bytes=start-end"),支援 206 Partial Content |
| 偽裝 Header | User-Agent 模擬 Firefox/Ubuntu,Referer 指定來源頁,繞過防盜鏈 |
| 超時控制 | setConnectTimeout / setReadTimeout 10s,觸發 SocketTimeoutException 進入重試 |
| 狀態碼校驗 | 僅接受 200 OK、206 Partial Content,其他視為錯誤終止線程 |
| 定期 Flush | 每 5KB 刷盤,異常捕獲塊同步 Flush,確保斷點數據不丟失 |
| 斷點持久化 | 臨時文件命名 filename_id,文件大小即為已下載位移 |
| 線程同步 | CountDownLatch 初始計數 = 線程數,每線程完成 countDown(),主線程 await() |
private void tempFileToTargetFile(ChildThread[] threads) {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile));
for (int i = 0; i < threadNum; i++) {
BufferedInputStream in = new BufferedInputStream(new FileInputStream(threads[i].tempFile));
byte[] buf = new byte[1024];
int len, count = 0;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
if (++count % 5000 == 0) out.flush();
}
in.close();
if (threads[i].status == STATUS_HAS_FINISHED) threads[i].tempFile.delete();
}
out.flush(); out.close();
}
private long[] setThreadBreakpoint(String dir, String fileName, long contentLength, long[] startPos) {
File target = new File(dir, fileName);
if (target.exists() && target.length() < contentLength) { // 未完成,續傳
File[] temps = new File(dir).listFiles();
for (File f : temps) {
if (f.getName().startsWith(fileName + "_") && f.length() > 0) {
int idx = Integer.parseInt(f.getName().substring(f.getName().lastIndexOf("_") + 1));
startPos[idx] = f.length(); // 文件大小 = 已下載位移
}
}
} else if (!target.exists()) {
target.createNewFile(); // 新任務
}
return startPos;
}
public class DownloadStartup {
public static void main(String[] args) {
DownloadTask dm = new DownloadTask();
dm.setSleepSeconds(5); // 重試間隔 5s
dm.download("https://apache.freelamp.com/velocity/tools/1.4/velocity-tools-1.4.zip", "utf-8");
}
}
此實現展示了 Java NIO.2 之前時代,利用 BIO + 多線程 + Range 請求構建健壯下載器的經典模式。