在上一篇文章我們介紹了獲取大文件的一個唯一的特徵值MD5,通過MD5我們可以唯一的標識一個文件,並可以實現秒傳效果,今天的這篇文章主要介紹大文件的上傳操作,當然談到上傳文件,網絡是必不可少的,現在也有很多較爲流行的網絡框架,如volley,OkHttp,Retrofit。而今天的這篇文章是採用最原始的上傳文件的方法,通過HttpClient上傳文件的方式。
HttpClient API
在API 23(6.0系統)之前,HttpClient類是Android API中本身自帶的方法,但是在23及以後的版本中谷歌放棄了HttpClient,如果想要使用需要在gradle文件中加上下面代碼
android {
useLibrary 'org.apache.http.legacy'
}
加入上面的代碼後,我們build一下就可以API23及以後版本中可以繼續使用HttpClient,在使用HttpClient上傳文件時可以使用MultipartEntity,FileBody,要使用這個類對象的話,我們需要導入相關jar包,在此我使用的是httpmine-4.1.3.jar。可能有些人說了,爲何廢棄了,還要用,不要問爲什麼,因爲我也不知道,哈哈,其實是懶,主要是公司老項目用的是這個,還沒準備大動,所以就在這基礎上做的。當然後期肯定要使用最新最流行的的技術,暫時未考慮(寫文章的時候正在學習Retrofit+RxJava,也學的已經差不多了,入了門道,準備開刀)。
Demo運行圖
文件上傳分析
在分析文件分塊上傳之前我們先來介紹如何直接上傳單個文件。在Android中的apache包中有一個HttpClient的默認實現類DefaultHttpClient,在上傳的時候我們需要指定上傳方式如是GET,POST等請求方式,而在apache包中提供了了對應的HttpPost,HttpGet.在這裏我們使用POST請求。如下代碼
MultipartEntity mpEntity=new MultipartEntity();
try {
mpEntity.addPart("md5", new StringBody(chunkInfo.getMd5()));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
FileBody fileBody = new FileBody(new File(chunkInfo.getFilePath()));
mpEntity.addPart("file", fileBody);
HttpPost post = new HttpPost(actionUrl);
// 發送請求體
post.setEntity(mpEntity);
DefaultHttpClient dhc = new DefaultHttpClient();
try {
dhc.getParams().setParameter(
CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
HttpResponse response = dhc.execute(post);
int res = response.getStatusLine().getStatusCode();
Log.e("圖片上傳返回響應碼", res + ",");
switch (res) {
case 200:
//流形式獲得
StringBuilder builder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
for (String s = bufferedReader.readLine(); s != null; s = bufferedReader.readLine()) {
builder.append(s);
}
retMsg = builder.toString();
break;
case 404:
retMsg = "-1";
break;
default:
retMsg = "500";
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
很簡單,通過MultipartEntity,FileBody就可以實現文件上傳了。上面的代碼很簡單,當然如果想展示上傳進度的話,我們只需要寫個類繼承FilterOutputStream,就可以自己寫個監聽回調展示進度,然後再發個廣播更新UI,詳細代碼不貼了,可點擊一鍵直達查看。
在上傳整個文件的時候我們看到主要用到的是FileBody,那麼我們就可以從這個地方入手,實現文件分塊上傳。通過源碼寫文件主要是通過writeTo()方法實現的
/** @deprecated */
@Deprecated
public void writeTo(OutputStream out, int mode) throws IOException {
this.writeTo(out);
}
public void writeTo(OutputStream out) throws IOException {
if(out == null) {
throw new IllegalArgumentException("Output stream may not be null");
} else {
FileInputStream in = new FileInputStream(this.file);
try {
byte[] tmp = new byte[4096];
int l;
while((l = in.read(tmp)) != -1) {
out.write(tmp, 0, l);
}
out.flush();
} finally {
in.close();
}
}
}
看到writeTo方法的具體實現後你就知道了,通過while((l = in.read(tmp)) != -1)判斷並循環讀取文件到輸出流。那麼既然我們是講文件分塊上傳,我們可以讀取文件的一部分就可以了這樣就可以實現分塊上傳了。
文件分塊分析
對於文件的從指定位置讀取指定大小數據,我用了RandomAccessFile對文件隨機讀取,通過seek()方法指定讀取的起始位置
假如我們我們的文件是長度大小fileLength,我們將分塊大小是chunkLength.那麼我們分塊數量計算爲
int chunks=(int)(fileLength/chunkLength+(fileLength%chunkLength>0?1:0));
這樣我們就計算了分塊總數,則我們可以計算我們每一次上傳的塊的起始位置如下
offset=chunk*chunkLength;//我們服務器將第一塊爲0塊,如果你的服務接口設的是從1開始,那就是offset就爲(chunk-1)*chunkLength;
計算出了offset,我們上傳每一塊只需要執行代碼randomAccessFile.seek(chunk*chunkLength);即可,然後讀取chunkLength長度的數據。
好了,代碼來了
自定義FileBody
/**
* Created by xiehui on 2016/10/13.
*/
public class CustomFileBody extends AbstractContentBody {
private File file = null;
private int chunk = 0; //第幾個分片
private int chunks = 1; //總分片數
private int chunkLength = 1024 * 1024 * 1; //分片大小1MB
public CustomFileBody(File file) {
this(file, "application/octet-stream");
}
public CustomFileBody(ChunkInfo chunkInfo) {
this(new File(chunkInfo.getFilePath()), "application/octet-stream");
this.chunk = chunkInfo.getChunk();
this.chunks = chunkInfo.getChunks();
this.file = new File(chunkInfo.getFilePath());
if (this.chunk == this.chunks) {
//先不判斷,固定1M
//this.chunkLength=this.file.length()-(this)
}
}
public CustomFileBody(File file, String mimeType) {
super(mimeType);
if (file == null) {
throw new IllegalArgumentException("File may not be null");
} else {
this.file = file;
}
}
@Override
public String getFilename() {
return this.file.getName();
}
@Override
public String getCharset() {
return null;
}
public InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}
@Override
public String getTransferEncoding() {
return "binary";
}
@Override
public long getContentLength() {
return chunkLength;
}
@Override
public void writeTo(OutputStream out) throws IOException {
if (out == null) {
throw new IllegalArgumentException("Output stream may not be null");
} else {
//不使用FileInputStream
RandomAccessFile randomAccessFile = new RandomAccessFile(this.file, "r");
try {
//int size = 1024 * 1;//1KB緩衝區讀取數據
byte[] tmp = new byte[1024];
//randomAccessFile.seek(chunk * chunkLength);
if (chunk+1<chunks){//中間分片
randomAccessFile.seek(chunk*chunkLength);
int n = 0;
long readLength = 0;//記錄已讀字節數
while (readLength <= chunkLength - 1024) {
n = randomAccessFile.read(tmp, 0, 1024);
readLength += 1024;
out.write(tmp, 0, n);
}
if (readLength <= chunkLength) {
n = randomAccessFile.read(tmp, 0, (int)(chunkLength - readLength));
out.write(tmp, 0, n);
}
}else{
randomAccessFile.seek(chunk*chunkLength);
int n = 0;
while ((n = randomAccessFile.read(tmp, 0, 1024)) != -1) {
out.write(tmp, 0, n);
}
}
out.flush();
} finally {
randomAccessFile.close();
}
}
}
public File getFile() {
return this.file;
}
}
文件分塊上傳模型類ChunkInfo
* Created by xiehui on 2016/10/21.
*/
public class ChunkInfo extends FileInfo implements Serializable{
/**
* 文件的當前分片值
*/
private int chunk=1;
/**
* 文件總分片值
*/
private int chunks=1;
/**
* 下載進度值
*/
private int progress=1;
public int getChunks() {
return chunks;
}
public void setChunks(int chunks) {
this.chunks = chunks;
}
public int getChunk() {
return chunk;
}
public void setChunk(int chunk) {
this.chunk = chunk;
}
public int getProgress() {
return progress;
}
public void setProgress(int progress) {
this.progress = progress;
}
@Override
public String toString() {
return "ChunkInfo{" +
"chunk=" + chunk +
", chunks=" + chunks +
", progress=" + progress +
'}';
}
}
具體上傳實現
public String uploadFile() {
String retMsg = "1";
CustomMultipartEntity mpEntity = new CustomMultipartEntity(
new CustomMultipartEntity.ProgressListener() {
@Override
public void transferred(long num) {
Intent intent2 = new Intent();
ChunkInfo chunkIntent = new ChunkInfo();
chunkIntent.setChunks(chunkInfo.getChunks());
chunkIntent.setChunk(chunkInfo.getChunk());
chunkIntent.setProgress((int) num);
intent2.putExtra("chunkIntent", chunkIntent);
intent2.setAction("ACTION_UPDATE");
context.sendBroadcast(intent2);
}
});
try {
mpEntity.addPart("chunk", new StringBody(chunkInfo.getChunk() + ""));
mpEntity.addPart("chunks", new StringBody(chunkInfo.getChunks() + ""));
mpEntity.addPart("fileLength", new StringBody(chunkInfo.getFileLength()));
mpEntity.addPart("md5", new StringBody(chunkInfo.getMd5()));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
CustomFileBody customFileBody = new CustomFileBody(chunkInfo);
mpEntity.addPart("file", customFileBody);
HttpPost post = new HttpPost(actionUrl);
// 發送請求體
post.setEntity(mpEntity);
DefaultHttpClient dhc = new DefaultHttpClient();
try {
dhc.getParams().setParameter(
CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
HttpResponse response = dhc.execute(post);
int res = response.getStatusLine().getStatusCode();
switch (res) {
case 200:
//流形式獲得
StringBuilder builder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
for (String s = bufferedReader.readLine(); s != null; s = bufferedReader.readLine()) {
builder.append(s);
}
retMsg = builder.toString();
break;
case 404:
retMsg = "-1";
break;
default:
retMsg = "500";
}
} catch (Exception e) {
e.printStackTrace();
}
return retMsg;
}
到此文件分塊上傳已基本完畢。那麼此時你可能會問秒傳的實現在哪了呢?別激動,在前面的分析中我們上傳的參數有一個是md5,我們上傳文件後將此值保存在數據庫,以及圖片的url鏈接,那麼當我們上傳文件之前先通過這個調用一個接口並上傳參數md5,服務接口查詢數據庫是否有此md5的文件,如果有的話,直接將圖片url返回即可,此時就提示用戶文件上傳成功,如果數據庫沒有此md5文件,則上傳文件。
接口延伸
由於客戶端上傳的是文件塊,當最後一塊上傳完成後,如果接口是每一分塊保存了一個臨時文件,則需要對分塊的文件進行合併及刪除。這個服務器FileChannel進行進行讀寫,當然也可以使用RandomAccessFile,因爲我們上傳了文件的總大小,則接口接收到分塊文件時直接創建一個文件並調用randomAccessFile.setLength();方法設置長度,之後通過上傳的seek方法在指定位置寫入數據到文件即可。
到此,本篇文章真的結束了,若文章有不足或者錯誤的地方,歡迎指正,以防止給其他讀者錯誤引導