Java編寫的斷點續傳的一個Demo示例
最近在研究Java的網路程式設計。在學習了基礎知識和原理之後,覺得可以搞一個斷點續傳的Demo示例。分享記錄一下。
涉及到的Java知識點:URL,HttpURLConnection,RandomAccessFile類,HTTP協議的基礎知識。
Demo示例的大概邏輯:對於一個Web資源,比如一個圖片或者其他可以通過GET請求訪問到的檔案,當然不僅限於說到的這幾種,我們可以用Java程式分段獲取該資源的內容,然後在本地將獲取到的分段內容組裝為原始的檔案。如果之前獲取過該Web資源的部分內容,當再次呼叫Java程式的時候,我們可以獲取到剩下部分的所有內容,然後再將該檔案組裝為原始的檔案。
程式碼如下:
package net.url; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.TimeUnit; /** * 對於Web資源的斷點續傳 */ public class BreakpointResume { /** * 斷點續傳時每段的位元組數,建議不要設定得太大 */ private static final long FRAGMENT_SIZE = 2048L; /** * 本地檔案儲存目錄 */ private static final String LOCAL_PATH = "C:\\Users\\frank\\Desktop"; /** * args的第一個引數指示Web資源的URL地址,協議型別限定為HTTP。required<br> * 第二個引數指示本地檔案的絕對路徑 + 檔名。optional * * @param args */ public static void main(String[] args) throws Exception { if (args.length != 1 && args.length != 2) { System.out.println("Usage[1]:java BreakpointResume url localFile"); System.out.println("or Usage[2]:java BreakpointResume url"); return; } String localFile = null; String url = args[0]; if (args.length == 2) { localFile = args[1]; } // 1. 校驗URL和本地檔案的格式 String checkURLMsg = checkURLFormat(url); if (checkURLMsg != null) { System.err.println(checkURLMsg); return; } String checkLocalFileMsg = checkLocalFileFormat(localFile); if (checkLocalFileMsg != null) { System.err.println(checkLocalFileMsg); return; } // 2. 計算Range的範圍 long startRange, endRange; if (localFile == null) { startRange = 0L; endRange = FRAGMENT_SIZE; } else { startRange = new File(localFile).length(); endRange = startRange + FRAGMENT_SIZE; } // 3. 訪問網路資源然後分段下載 downloadPartially(url, localFile, startRange, endRange); } /** * 分段下載。<br> * <p>假設要下載的資源為http://img.article.pchome.net/00/22/70/23/pic_lib/wm/03.jpg,分段下載的思路如下:</p> * <p>1) 設定請求屬性Range</p> * <p>2) 連線遠端Web資源</p> * <p>3) 校驗響應中的狀態行,如果不是200或者206,就停止傳輸</p> * <p>4) 檢視.tmp檔案(03.jpg.tmp)是否已經存在,如果不存在,就新建該檔案</p> * <p>5) 將響應內容寫入.tmp檔案</p> * <p>6) 如果Web資源已經全部傳輸完了,將.tmp檔案的字尾去掉,還原為檔案本來的字尾和格式,然後結束while迴圈</p> * <p>7) 如果Web資源沒有傳輸完,計算下一次傳輸的Range的範圍</p> * * @param url * @param localFile * @param startRange * @param endRange * @throws MalformedURLException 應該不會丟擲該異常,因為已經限定了只能用HTTP協議訪問Web資源,並且進行了校驗 */ private static void downloadPartially(String url, String localFile, long startRange, long endRange) throws MalformedURLException { long startTime = System.currentTimeMillis(); URL resource = new URL(url); // 加入num是用來模擬Web資源傳輸了一部分,然後第二次傳輸時,從上次結束的部分開始獲取 int num = 0; while (true) { if (++num == 10) { // break; } HttpURLConnection conn = null; InputStream in = null; RandomAccessFile raf = null; try { conn = (HttpURLConnection) resource.openConnection(); // 1) 設定請求屬性Range conn.setRequestProperty("Range", "bytes=" + startRange + "-" + (endRange == -1L ? "" : endRange)); // 2) 連線遠端Web資源 conn.connect(); // 3) 校驗狀態行,如果不是成功或者部分內容,就停止傳輸 String statusLine = conn.getHeaderField(null);// 狀態行 System.out.println("statusLine=" + statusLine); if (!statusLine.contains("200") && !statusLine.contains("206")) { throw new Exception("獲取Web資源[" + url + "]時,響應狀態不是200或者206"); } // 獲取資源長度 String cr = conn.getHeaderField("Content-Range"); if (cr == null || "".equals(cr.trim())) { throw new Exception("獲取Web資源[" + url + "]時,響應資訊中Content-Range為null"); } System.out.println("Content-Range=" + cr); cr = cr.replace("[", "").replace("]", "").replace("bytes", "").trim(); // 解析響應訊息頭中Content-Range欄位的值 long resourceStartPos = Long.parseLong(cr.substring(0, cr.indexOf("-"))); long resourceEndPos = Long.parseLong(cr.substring(cr.indexOf("-") + 1, cr.indexOf("/"))); long resourceTotalLength = Long.parseLong(cr.substring(cr.indexOf("/") + 1)); System.out.println("resourceStartPos=" + resourceStartPos + ", resourceEndPos=" + resourceEndPos + ", resourceTotalLength=" + resourceTotalLength); // 將相應內容讀取到buf中 byte[] buf = new byte[(int) (resourceEndPos - resourceStartPos)]; in = conn.getInputStream(); in.read(buf); // 4) 檢視.tmp檔案是否已經存在,如果不存在,就新建該檔案 if (localFile == null) { localFile = LOCAL_PATH + File.separator + url.substring(url.lastIndexOf("/") + 1) + ".tmp"; } System.out.println("localFile=" + localFile); File f = new File(localFile); if (!f.exists()) {// .tmp檔案不存在,使用OutputStream手動建立該檔案 OutputStream os = new FileOutputStream(f); try {os.close();} catch (Exception e) {} } /* * 如果不新增下面一行程式碼,多次執行該類的話,下載到本地的檔案會出問題。 * 加上下面一行程式碼的話,會極大地減小出問題的概率,但並不能絕對避免出問題。 * 個人初步懷疑,是因為RandomAccessFile在建立以及關閉時,都需要呼叫native * 方法請求分配資源或者釋放資源,這種JNI呼叫實際上效率並不高。在本例中的 * while迴圈中頻繁呼叫RandomAccessFile的建立和關閉方法,可能會在分配 * 資源或者釋放資源時並不徹底,然後通過RandomAccessFile去寫的時候就會 * 出問題。加上一個短暫的休眠時間,是為了讓JNI有充分的時間能夠正確地 * 分配資源以及正確地釋放資源。10ms對於計算機系統應該算是一個比較長的 * 時間間隔了 */ TimeUnit.MILLISECONDS.sleep(10L); raf = new RandomAccessFile(f, "rwd"); raf.seek(startRange); raf.write(buf); try {raf.close();} catch (Exception e) {} // 6) 如果Web資源已經全部傳輸完了,將.tmp檔案的字尾去掉,還原為檔案本來的字尾和格式,然後結束while迴圈 // 從Web伺服器返回的內容中的位元組,是以0為索引開始計數的 if (resourceEndPos == resourceTotalLength - 1) { f.renameTo(new File(LOCAL_PATH + File.separator + url.substring(url.lastIndexOf("/") + 1))); break; } // 7) 如果Web資源沒有傳輸完,計算下一次傳輸的Range的範圍 // 如果剩下的要傳輸的內容不超過FRAGMENT_SIZE的1.5倍,就一起全部傳輸過來,減少HttpURLConnection連線帶來的資源消耗 if (resourceTotalLength - resourceEndPos <= FRAGMENT_SIZE * 3 / 2) { startRange = resourceEndPos; endRange = -1L; } else { startRange = resourceEndPos; endRange = resourceEndPos + FRAGMENT_SIZE; } System.out.println("next startRange=" + startRange + ", endRange=" + endRange); } catch (Exception e) { System.err.println(e.getMessage()); return; } finally { if (in != null) { try {in.close();} catch (Exception e) {} } if (conn != null) { conn.disconnect(); } } } long time = System.currentTimeMillis() - startTime; System.out.println("傳輸Web資源[" + url + "]共耗時" + time / 1000 + "s" + time % 1000 + "ms"); } /** * URL格式限定如下: * <p> * URL的長度至少是20 * </p> * <p> * URL的協議型別必須是HTTP * </p> * <p> * URL中必須包含".",且"."後面的字元個數不能超過10 * </p> * * @param url * @return */ private static String checkURLFormat(String url) { if (url.length() < 20) { return "url的長度至少為20"; } String protocol = url.substring(0, 7); if (!protocol.equalsIgnoreCase("http://")) { return "url必須以http://開頭(不區分大小寫)"; } int dotIndex = url.lastIndexOf("."); if (dotIndex == -1) { return "url中必須有'.'"; } String resourceSuffix = url.substring(dotIndex); if (resourceSuffix.length() > 10) { return "url格式不正確,資源名稱的字尾('.'後面的字元)不能超過10個字元"; } return null; } /** * 本地檔案格式限定如下: * <p> * 如果localFile不為null,那麼該檔案在本地必須存在並且是檔案 * </p> * <p> * 如果localFile不為null,那麼該檔案的字尾必須是.tmp,說明該檔案在之前沒有傳輸完,本次繼續傳輸 * </p> * * @param localFile * @return */ private static String checkLocalFileFormat(String localFile) { if (localFile == null) { return null; } String retMsg = null; try { File f = new File(localFile); if (!f.exists()) { retMsg = "本地檔案[" + localFile + "]不存在"; } else if (!f.isFile()) { retMsg = "本地檔案[" + localFile + "]不是一個檔案"; } else if (!localFile.endsWith(".tmp")) { retMsg = "本地檔案[" + localFile + "]應該以.tmp結尾"; } } catch (Exception e) { retMsg = "在讀取本地檔案[" + localFile + "]時出錯,請檢查該檔案"; } return retMsg; } }
該程式執行示例:
第一種情況:之前沒有獲取過該Web資源,並且本次會一次性獲取該Web資源。
執行命令:java net.url.BreakpointResume http://img.article.pchome.net/00/22/70/23/pic_lib/wm/03.jpg
然後會發現在LOCAL_PATH目錄下獲取到了網上的這個圖片檔案。
第二種情況:之前獲取過該Web資源的部分內容,然後本次會獲取剩下的所有內容。
將
if (++num == 10) {
// break;
}
中的註釋去掉,然後執行命令:java net.url.BreakpointResume http://img.article.pchome.net/00/22/70/23/pic_lib/wm/03.jpg,這時會發現LOCAL_PATH目錄下生成了03.jpg.tmp檔案,如圖所示:
然後將剛才取消註釋的那一行再註釋掉,再執行命令:java net.url.BreakpointResume http://img.article.pchome.net/00/22/70/23/pic_lib/wm/03.jpg C:\Users\frank\Desktop\03.jpg.tmp,
這時會發現03.jpg.tmp檔案變成了03.jpg檔案。
本示例中仍然存留有一個小問題,在註釋裡已經標明瞭。目前我也沒法完全解決,希望有大牛看到後能給予解答!