1. 程式人生 > >Android FTP 多執行緒斷點續傳下載\上傳

Android FTP 多執行緒斷點續傳下載\上傳

最近在給我的開源下載框架Aria增加FTP斷點續傳下載和上傳功能,在此過程中,爬了FTP的不少坑,終於將功能實現了,在此把一些核心功能點記錄下載。

FTP下載原理

FTP單執行緒斷點續傳

FTP和傳統的HTTP協議有所不同,由於FTP沒有所謂的標頭檔案,因此我們不能像HTTP那樣通過設定header向伺服器指定下載區間。
但是FTP協議提供了一個更好用的命令REST用於從指定位置恢復任務,同時FTP協議也提供了一個命令SIZE用於獲取下載的檔案大小,有了這兩個命令,FTP斷點續傳也就沒有什麼問題。
FTP斷點續傳的原理和HTTP的斷點續傳原理差不多,在暫停時記錄檔案的停止位置,再次下載時,先讀取記錄的位置,如果位置存在,則通過REST

命令告訴伺服器從指定區間進行下載。

FTP多執行緒斷點續傳

多執行緒下載的原理和HTTP多執行緒下載的原理差不多。先獲取檔案大小,然後根據執行緒數,對整個檔案進行分段下載,在任務停止時,記錄每一條執行緒的暫停位置,重新開始下載,每一條執行緒讀取對應的下載記錄,然後每一執行緒從指定位置開始下載。
分段下載
和HTTP所不同的是,FTP並沒有提供檔案區間的API,因此,FTP在分段下載中,只有起始位置而沒有結束位置。
因此,你需要在指定位置手動停止執行緒。

功能實現

本文使用將採用apache commons-net實現FTP斷點續傳下載\上傳功能。
通過下文的幾步操作,你就能很簡單的實現FTP斷點續傳。

登入

FTP協議和HTTP協議有所不同,使用FTP進行下載時,你需要進行登入操作。
當然,如果你伺服器沒有登入功能,你可以忽略登入操作。

FTPClient client = new FTPClient();
client.connect(serverIp, port); //連線到FTP伺服器
client.login(userName, passsword);

通過上面三行程式碼,就可以很簡單的登入到FTP伺服器上。
在進行登入後,還需要驗證是否登入成功

int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
    client.disconnect();
    Log.d(TAG, "無法連線到ftp伺服器,錯誤碼為:"
+ reply); return; }

由於FTP協議中,連線成功的狀態有多個,因此需要通過FTPReply.isPositiveCompletion(reply)用於驗證是否成功連線到FTP伺服器。

檔案資訊獲取

在連線到FTP伺服器後,就需要開始獲取下載最重要的幾個引數(檔案長度、檔名)。
客戶端可以通過client.listFiles(remotePath)獲取FTP伺服器上該路徑的檔案列表。

  • 如果路徑是檔案,只會返回一個長度為1的陣列。
  • 如果該路徑為資料夾,則會返回該資料夾下對應的所有檔案。
String remotePath = "/upload/qjnn.apk"; //FTP伺服器上檔案路徑
FTPFile[] files = client.listFiles(remotePath);
FTPFile file = files[0];  //檔案資訊
long size = file.getSize();
String fileaName = file.getName();

如果你的檔案為英文名,並且路徑中沒有中文,那麼通過上述程式碼,便可以獲取到正確的檔案資訊。
但如果FTP上的伺服器上的檔名有中文或路徑有中文,那麼上述程式碼,你將獲取不到正確的檔案資訊。

正確的寫法

由於FTP伺服器預設的編碼是ISO-8859-1,因此,客戶端在獲取檔案資訊時

  • 需要請求伺服器使用UTF-8編碼(如果伺服器支援的話),如果伺服器不支援開啟UTF-8編碼,那麼客戶端需要指定字串編碼格式
  • 客戶端在請求remotePath路徑、獲取檔名時,都需要對路徑進行編碼轉換處理。
String remotePath = "/upload/qjnn.apk"; //FTP伺服器上檔案路徑
String charSet = "UTF-8";
if (!FTPReply.isPositiveCompletion(client.sendCommand("OPTS UTF8", "ON"))) {    //向伺服器請求使用"UTF-8"編碼
    charSet = "GBK";
}
FTPFile[] files = client.listFiles(new String(remotePath.getBytes(charSet), "ISO-8859-1")); //對remotePath進行編碼轉換
FTPFile file = files[0];  //檔案資訊
long size = file.getSize();
String fileaName = new String(fileName.getBytes(), Charset.forName(charSet));

通過以上程式碼,便可以獲取到正確的檔案資訊。

檔案下載

配置每條執行緒的下載區間

long fileLength = mEntity.getFileSize();
Properties pro = CommonUtil.loadConfig(mConfigFile);
int blockSize = (int) (fileLength / mThreadNum);
int[] recordL = new int[mThreadNum];
for (int i = 0; i < mThreadNum; i++) {
  recordL[i] = -1;
}
int rl = 0;
for (int i = 0; i < mThreadNum; i++) {
  long startL = i * blockSize, endL = (i + 1) * blockSize;
  Object state = pro.getProperty(mTempFile.getName() + "_state_" + i);
  if (state != null && Integer.parseInt(state + "") == 1) {  //該執行緒已經完成
    if (resumeRecordLocation(i, startL, endL)) return;
    continue;
  }
  //分配下載位置
  Object record = pro.getProperty(fileName + "_record_" + i);
  //如果有記錄,則恢復下載
  if (record != null && Long.parseLong(record + "") >= 0) {
    Long r = Long.parseLong(record + "");
    mConstance.CURRENT_LOCATION += r - startL;
    Log.d(TAG, "任務【" + mEntity.getFileName() + "】執行緒__" + i + "__恢復下載");
    startL = r;
    recordL[rl] = i;
    rl++;
  } else {
    recordL[rl] = i;
    rl++;
  }
  //最後一個執行緒的結束位置即為檔案的總長度
  if (i == (mThreadNum - 1)) endL = fileLength;
  //建立分段執行緒
  AbsThreadTask task = createSingThreadTask(i, startL, endL, fileLength);
  if (task == null) return;
  mTask.put(i, task);
}
startSingleTask(recordL);

在上面的程式碼中,主要做了兩步操作:

  1. 在檔案下載前,先從本地檔案中讀取當前下載的每一條執行緒的下載情況
  2. 如果下載記錄存在,從記錄位置開始下載,如果記錄不存在,則重新開始下載

FTP 分段執行緒區間自動停止

由於FTP協議沒有區間下載的原因,為了讓執行緒只下載特定區間的內容,需要客戶端在單條執行緒累計讀的資料長度已經超過了所分配的區間長度的時候,停止該條執行緒。

 client.enterLocalPassiveMode();    //設定被動模式
 client.setFileType(FTP.BINARY_FILE_TYPE);  //設定檔案傳輸模式
 client.setRestartOffset(mConfig.START_LOCATION);   //設定恢復下載的位置
 client.allocate(mBufSize);
 is = client.retrieveFileStream(new String(remotePath.getBytes(charSet), SERVER_CHARSET));
 //傳送第二次指令時,還需要再做一次判斷
 reply = client.getReplyCode();
 if (!FTPReply.isPositivePreliminary(reply)) {
    client.disconnect();
    fail(mChildCurrentLocation, "獲取檔案資訊錯誤,錯誤碼為:" + reply, null);
    return;
  }
 file = new BufferedRandomAccessFile(mConfig.TEMP_FILE, "rwd", mBufSize);
 file.seek(mConfig.START_LOCATION);
 byte[] buffer = new byte[mBufSize];
 int len;
 while ((len = is.read(buffer)) != -1) { 
    //如果該條執行緒讀取的資料長度大於所分配的區間長度,則只能讀到區間的最大長度
    if (mChildCurrentLocation + len >= mConfig.END_LOCATION) {
        len = (int) (mConfig.END_LOCATION - mChildCurrentLocation);
        file.write(buffer, 0, len);
        progress(len);
        break;
    } else {
        file.write(buffer, 0, len);
        progress(len);
    }
 }

這裡還有幾個坑需要處理一下:

  • 對於FTP客戶端來說,一般需要設定被動模式,被動模式和主動模式的區別
  • 在獲取檔案流後,還需要使用FTPReply.isPositivePreliminary(reply)進行第二次命令判斷

關於FTP檔案上傳

FTP 檔案斷點續傳的方式原理和下載的都差不多:

  1. 都是在停止的時候記錄停止位置,重新開始下載的時候從指定位置通過REST命令恢復斷點。
  2. 都需要在任務執行前獲取檔案資訊,比對伺服器上的檔案。

而和下載有區別的是:

  1. FTP上傳時需要指定工作目錄、在遠端伺服器上建立資料夾
  2. 需要伺服器給使用者開啟刪除和讀入IO的許可權,否則會出現550許可權錯誤問題
  3. 上傳檔案需要storeFileStream獲取outputStream流

最終效果

FTP 下載.gif
FTP 上傳.gif

參考文件