Android 中除了HTTP協議下載之外還可以使用FTP下載,HTTP的斷點下載使用RandomAccessFile文件,FTP使用reset命令。和HTTP所不同的是,FTP并沒有提供文件區間的API,因此,FTP在分段下載中,只有起始位置而沒有結束位置。
因此,你需要在指定位置手動停止線程。
FTP多線程斷點下載實現主要有以下步驟:
1,登錄服務器
FTP協議和HTTP協議有所不同,使用FTP進行下載時,你需要進行登錄操作。
當然,如果你服務器沒有登錄功能,你可以忽略登錄操作。
FTPClient client = new FTPClient();
client.connect(serverIp, port); //連接到FTP服務器
client.login(userName, passsword);
在登錄成功之后,還要驗證登陸是否成功
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
client.disconnect();
Log.d(TAG, "無法連接到ftp服務器,錯誤碼為:" + reply);
return;
}
由于FTP協議中,連接成功的狀態有多個,因此需要通過FTPReply.isPositiveCompletion(reply)用于驗證是否成功連接到FTP服務器。
2,獲取文件信息
在連接到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服務器默認的編碼是ISO-8859-1,因此,客戶端在獲取文件信息時需要進行以下操作:
1)需要請求服務器使用UTF-8編碼(如果服務器支持的話),如果服務器不支持開啟UTF-8編碼,那么客戶端需要指定字符串編碼格式。
2)客戶端在請求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));
3,配置每條線程的下載區間
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)如果下載記錄存在,從記錄位置開始下載,如果記錄不存在,則重新開始下載。
4,每個線程的下載任務
由于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);
}
}
這里還有幾個坑需要處理一下:
1)對于FTP客戶端來說,一般需要設置被動模式。
2)在獲取文件流后,還需要使用FTPReply.isPositivePreliminary(reply)進行第二次命令判斷。
至此FTP多線程斷點下載已經完成,最后說一下多線程斷點上傳的思路:
FTP 文件斷點續傳的方式原理和下載的都差不多:
1)都是在停止的時候記錄停止位置,重新開始下載的時候從指定位置通過REST命令恢復斷點。
2)都需要在任務執行前獲取文件信息,比對服務器上的文件。
而和下載有區別的是:
1)FTP上傳時需要指定工作目錄、在遠程服務器上創建文件夾。
2)需要服務器給用戶打開刪除和讀入IO的權限,否則會出現550權限錯誤問題。
3)上傳文件需要storeFileStream獲取outputStream流。