很簡單的Java斷點續傳實現原理_java

來源:互聯網
上載者:User

原理解析

在開發當中,“斷點續傳”這種功能很實用和常見,聽上去也是比較有“逼格”的感覺。所以通常我們都有興趣去研究研究這種功能是如何?的?
以Java來說,網路上也能找到不少關於實作類別似功能的資料。但是呢,大多數都是舉個Demo然後貼出源碼,真正對其實現原理有詳細的說明很少。
於是我們在最初接觸的時候,很可能就是直接Crtl + C/V代碼,然後搗鼓搗鼓,然而最終也能把效果弄出來。但初學時這樣做其實很顯然是有好有壞的。
好處在於,源碼很多,解釋很少;如果我們肯下功夫,針對於別人貼出的代碼裡那些自己不明白的東西去查資料,去鑽研。最終多半會收穫頗豐。
壞處也很明顯:作為初學者,面對一大堆的源碼,感覺好多東西都很陌生,就很容易望而生畏。即使最終大致瞭解了用法,但也不一定明白實現原理。

我們今天就一起從最基本的角度切入,來看看所謂的“斷點續傳”這個東西是不是真的如此“高逼格”。
其實在接觸一件新的“事物”的時候,將它擬化成一些我們本身比較熟悉的事物,來參照和對比著學習。通常會事半功倍。
如果我們剛接觸“斷點續傳”這個概念,肯定很難說清楚個一二三。那麼,“玩遊戲”我們肯定不會陌生。

OK,那就假設我們現在有一款“通關制的RPG遊戲”。想想我們在玩這類遊戲時通常會怎麼做?
很明顯,第一天我們浴血奮戰,大殺四方,假設終於來到了第四關。雖然激戰正酣,但一看牆上的時鐘,已經淩晨12點,該睡覺了。
這個時候就很尷尬了,為了能夠在下一次玩的時候,順利接軌上我們本次遊戲的進度,我們應該怎麼辦呢?
很簡單,我們不關掉遊戲,直接去睡覺,第二天再接著玩唄。這樣是可以,但似乎總覺著有哪裡讓人不爽。
那麼,這個時候,如果這個遊戲有一個功能叫做“存檔”,就很關鍵了。我們直接選擇存檔,輸入存檔名“第四關”,然後就可以關閉遊戲了。
等到下次進行遊戲時,我們直接找到“第四關”這個存檔,然後進行讀檔,就可以接著進行遊戲了。

這個時候,所謂的“斷點續傳”就很好理解了。我們順著我們之前“玩遊戲”的思路來理一下:
假設,現在有一個檔案需要我們進行下載,當我們下載了一部分的時候,出現情況了,比如:電腦死機、沒電、網路中斷等等。
其實這就好比我們之前玩遊戲玩著玩著,突然12點需要去睡覺休息了是一個道理。OK,那麼這個時候的情況是:

 • 如果遊戲不能存檔,那麼則意味著我們下次遊戲的時候,這次已經通過的4關的進度將會丟失,無法接檔。
 • 對應的,如果“下載”的行為無法記錄本次下載的一個進度。那麼,當我們再次下載這個檔案也就只能從頭來過。
 話到這裡,其實我們已經發現了,對於我們以上所說的行為,關鍵就在於一個字“”!
而我們要實現讓一種斷開的行為“續”起來的目的,關鍵就在於要有“介質”能夠記錄和讀取行為出現”中斷”的這個節點的資訊。

轉化到編程世界

實際上這就是“斷點續傳”最最基礎的原理,用大白話說就是:我們要在下載行為出現中斷的時候,記錄下中斷的位置資訊,然後在下次行為中讀取。
有了這個位置資訊之後,想想我們該怎麼做。是的,很簡單,在新的下載行為開始的時候,直接從屬記錄的這個位置開始下載內容,而不再從頭開始。
好吧,我們用大白話掰扯了這麼久的原理,開始覺得無聊了。那麼我們現在最後總結一下,然後就來看看我們應該怎麼把原理轉換到編程世界中去。

 • 當“上傳(下載)的行為”出現中斷,我們需要記錄本次上傳(下載)的位置(position)。
 • 當“續”這一行為開始,我們直接跳轉到postion處繼續上傳(下載)的行為。 

顯然問題的關鍵就在於所謂的“position”,以我們舉的“通關遊戲來說”,可以用“第幾關”來作為這個position的單位。
那麼轉換到所謂的“斷點續傳”,我們該使用什麼來衡量“position”呢?很顯然,迴歸二進位,因為這裡的本質無非就是檔案的讀寫。

那麼剩下的工作就很簡單了,先是記錄position,這似乎都沒什麼值得說的,因為只是資料的持久化而已(記憶體,檔案,資料庫),我們有很多方式。

另一個關鍵在於當“續傳”的行為開始,我們需要需要從上次記錄的position位置開始讀寫操作,所以我們需要一個類似於“指標”功能的東西。
我們當然也可以自己想辦法去實現這樣一個“指標”,但高興地是,Java已經為我們提供了這樣的一個類,那就是RandomAccessFile。
這個類的功能從名字就很直觀的體現了,能夠隨機的去訪問檔案。我們看一下API文檔中對該類的說明:

此類的執行個體支援對隨機訪問檔案的讀取和寫入。隨機訪問檔案的行為類似儲存在檔案系統中的一個大型 byte 數組。

如果隨機訪問檔案以讀取/寫入模式建立,則輸出操作也可用;輸出操作從檔案指標開始寫入位元組,並隨著對位元組的寫入而前移此檔案指標。

寫入隱含數組的當前末尾之後的輸出操作導致該數組擴充。該檔案指標可以通過 getFilePointer 方法讀取,並通過 seek 方法設定。 

看完API說明,我們笑了,是的,這不正是我們要的嗎?那好吧,我們磨刀磨了這麼久了,還不去砍砍柴嗎?

執行個體示範

既然是針對於檔案的“斷點續傳”,那麼很明顯,我們先搞一個檔案出來。也許音頻檔案,影像檔什麼的看上去會更上檔次一點。
但我們已經說了,在電腦大兄弟眼中,它們最終都將迴歸“二進位”。所以我們這裡就建立一個簡單的”txt”檔案,因為txt更利於理解。

我們在D盤的根目錄下建立一個名為”test.txt”的檔案,檔案內容很簡單,如圖所示:


沒錯,我們輸入的內容就是簡單的6個英語字母。然後我們右鍵→屬性:


我們看到,檔案現在的大小是6個位元組。這也就是為什麼我們說,所有的東西到最後還是離不開“二進位”。
是的,我們都明白,因為我們輸入了6個英文字母,而1個英文字母將佔據的儲存空間是1個位元組(即8個位元位)。
目前為止,我們看到的都很無聊,因為這基本等於廢話,稍微有電腦常識的人都知道這些知識。別著急,我們繼續。

在Java中對一個檔案進行讀寫操作很簡單。假設現在的需求如果是“把D盤的這個檔案寫入到E盤”,那麼我們會提起鍵盤,啪啪啪啪,搞定!
但其實所謂的檔案的“上傳(下載)”不是也沒什麼不同嗎?區別就僅僅在於行為由“僅僅在本機之間”轉變成了”本機與伺服器之間”的檔案讀寫。
這時我們會說,“別逼逼了,這些誰都知道,‘斷點續傳'呢?“,其實到了這裡也已經很簡單了,我們再次明確,斷點續傳要做的無非就是:
前一次讀寫行為如果出現中斷,請記錄下此次讀寫完成的檔案內容的位置資訊;當“續傳開始”則直接將指標移到此處,開始繼續讀寫操作。

反覆的強調原理,實際上是因為只要弄明白了原理,剩下的就只是招式而已了。這就就像武俠小說裡的“九九歸一”大法一樣,最高境界就是迴歸本源。
任何複雜的事物,只要明白其原理,我們就能將其剝離,還原為一個個簡單的事物。同理,一系列簡單的事物,經過邏輯組合,就形成了複雜的事物。

下面,我們馬上就將迴歸混沌,以最基本的形式類比一次“斷點續傳”。在這裡我們連伺服器的代碼都不去寫了,直接通過一個本地測試類別搞定。
我們要實現的效果很簡單:將在D盤的”test.txt”檔案寫入到E盤當中,但中途我們會類比一次”中斷”行為,然後在重新繼續上傳,最終完成整個過程。
也就是說,我們這裡將會把“D盤”視作一台電腦,並且直接將”E盤”視作一台伺服器。那麼這樣我們甚至都不再與http協議扯上半毛錢關係了,(當然實際開發我們肯定是還是得與它扯上關係的 ^<^),從而只關心最基本的檔案讀寫的”斷”和”續”的原理是怎麼樣的。

為了通過對比加深理解,我們先來寫一段正常的代碼,即正常讀寫,不發生中斷:

public class Test { public static void main(String[] args) {  // 源檔案與目標檔案  File sourceFile = new File("D:/", "test.txt");  File targetFile = new File("E:/", "test.txt");  // 輸入輸出資料流  FileInputStream fis = null;  FileOutputStream fos = null;  // 資料緩衝區  byte[] buf = new byte[1];  try {   fis = new FileInputStream(sourceFile);   fos = new FileOutputStream(targetFile);   // 資料讀寫   while (fis.read(buf) != -1) {    System.out.println("write data...");    fos.write(buf);   }  } catch (FileNotFoundException e) {   System.out.println("指定檔案不存在");  } catch (IOException e) {   // TODO: handle exception  } finally {   try {    // 關閉輸入輸出資料流    if (fis != null)     fis.close();    if (fos != null)     fos.close();   } catch (IOException e) {    e.printStackTrace();   }  } }}

該段代碼運行,我們就會發現在E盤中已經成功拷貝了一份“test.txt”。這段代碼很簡單,唯一稍微說一下就是:
我們看到我們將buf,即緩衝區 設定的大小是1,這其實就代表我們每次read,是讀取一個位元組的資料(即1個英文字母)。

現在,我們就來類比這個讀寫中斷的行為,我們將之前的代碼完善如下:

import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.RandomAccessFile;public class Test { private static int position = -1; public static void main(String[] args) {  // 源檔案與目標檔案  File sourceFile = new File("D:/", "test.txt");  File targetFile = new File("E:/", "test.txt");  // 輸入輸出資料流  FileInputStream fis = null;  FileOutputStream fos = null;  // 資料緩衝區  byte[] buf = new byte[1];  try {   fis = new FileInputStream(sourceFile);   fos = new FileOutputStream(targetFile);   // 資料讀寫   while (fis.read(buf) != -1) {    fos.write(buf);    // 當已經上傳了3位元組的檔案內容時,網路中斷了,拋出異常    if (targetFile.length() == 3) {     position = 3;     throw new FileAccessException();    }   }  } catch (FileAccessException e) {   keepGoing(sourceFile,targetFile, position);  } catch (FileNotFoundException e) {   System.out.println("指定檔案不存在");  } catch (IOException e) {   // TODO: handle exception  } finally {   try {    // 關閉輸入輸出資料流    if (fis != null)     fis.close();    if (fos != null)     fos.close();   } catch (IOException e) {    e.printStackTrace();   }  } } private static void keepGoing(File source,File target, int position) {  try {   Thread.sleep(10000);  } catch (InterruptedException e) {   // TODO Auto-generated catch block   e.printStackTrace();  }  try {   RandomAccessFile readFile = new RandomAccessFile(source, "rw");   RandomAccessFile writeFile = new RandomAccessFile(target, "rw");   readFile.seek(position);   writeFile.seek(position);   // 資料緩衝區   byte[] buf = new byte[1];   // 資料讀寫   while (readFile.read(buf) != -1) {    writeFile.write(buf);   }  } catch (FileNotFoundException e) {   // TODO Auto-generated catch block   e.printStackTrace();  } catch (IOException e) {   // TODO Auto-generated catch block   e.printStackTrace();  } }}class FileAccessException extends Exception {}

總結一下,我們在這次改動當中都做了什麼工作:

 • 首先,我們定義了一個變數position,記錄在發生中斷的時候,已完成讀寫的位置。(這是為了方便,實際來說肯定應該講這個值存到檔案或者資料庫等進行持久化)
 然後在檔案讀寫的while迴圈中,我們去類比一個中斷行為的發生。這裡是當targetFile的檔案長度為3個位元組則類比拋出一個我們自訂的異常。(我們可以想象為實際下載中,已經上傳(下載)了”x”個位元組的內容,這個時候網路中斷了,那麼我們就在網路中斷拋出的異常中將”x”記錄下來)。
 • 剩下的就如果我們之前說的一樣,在“續傳”行為開始後,通過RandomAccessFile類來封裝我們的檔案,然後通過seek將指標指定到之前發生中斷的位置進行讀寫就搞定了。
(實際的檔案下載上傳,我們當然需要將儲存的中斷值上傳給伺服器,這個方式通常為httpConnection.setRequestProperty(“RANGE”,”bytes=x”);)

在我們這段代碼,開啟”續傳“行為,即keepGoing方法中:我們起頭讓線程休眠10秒鐘,這正是為了讓我們運行程式看到效果。
現在我們運行程式,那麼檔案就會開啟“由D盤上傳到E盤的過程”,我們首先點開E盤,會發現的確多了一個test.txt檔案,開啟它發現內容如下:


沒錯,這個時候我們發現內容只有“abc”。這是在我們預料以內的,因為我們的程式類比在檔案上傳了3個位元組的時候發生了中斷。

Ok,我們靜靜的等待10秒鐘過去,然後再點開該檔案,看看是否能夠成功:

通過截圖我們發現內容的確已經變成了“abc”,由此也就完成了續傳。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.