在上一篇文章我们介绍了获取大文件的一个唯一的特征值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(); }
查看。
在上传整个文件的时候我们看到主要用到的是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方法在指定位置写入数据到文件即可。
到此,本篇文章真的结束了,若文章有不足或者错误的地方,欢迎指正,以防止给其他读者错误引导