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

原理解析

在開發當中,“斷點續傳”這種功能很實用和常見,聽上去也是比較有“逼格”的感覺。所以通常我們都有興趣去研究研究這種功能是如何實現的?
以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”,由此也就完成了續傳。

摘自:http://blog.csdn.net/ghost_Programmer/article/details/51923895

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章