在上一篇文章我們介紹了獲取大檔案的一個唯一的特徵值MD5,通過MD5我們可以唯一的標識一個檔案,並可以實現秒傳效果,今天的這篇文章主要介紹大檔案的上傳操作,當然談到上傳檔案,網路是必不可少的,現在也有很多較為流行的網路框架,如volley,OkHttp,Retrofit。而今天的這篇文章是採用最原始的上傳檔案的方法,通過HttpClient上傳檔案的方式。
HttpClient API
在API 23(6.0系統)之前,HttpClient類是Android API中本身自帶的方法,但是在23及以後的版本中谷歌放棄了HttpClient,如果想要使用需要在gradle檔案中加上下面程式碼
1 2 3 |
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請求。如下程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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()方法實現的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** @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.那麼我們分塊數量計算為
1 |
int chunks=(int)(fileLength/chunkLength+(fileLength%chunkLength>0?1:0)); |
這樣我們就計算了分塊總數,則我們可以計算我們每一次上傳的塊的起始位置如下
1 |
offset=chunk*chunkLength;//我們伺服器將第一塊為0塊,如果你的服務介面設的是從1開始,那就是offset就為(chunk-1)*chunkLength; |
計算出了offset,我們上傳每一塊只需要執行程式碼randomAccessFile.seek(chunk*chunkLength);即可,然後讀取chunkLength長度的資料。
好了,程式碼來了
自定義FileBody
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
/** * 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
* 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 + '}'; } } |
具體上傳實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
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方法在指定位置寫入資料到檔案即可。
到此,本篇文章真的結束了,若文章有不足或者錯誤的地方,歡迎指正,以防止給其他讀者錯誤引導
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式