前言
之前項目需要上傳大文件的功能,上傳大文件經常遇到上傳一半由於網絡或者其他一些原因上傳失敗。然后又得重新上傳(很麻煩),所以就想能不能做個斷點上傳的功能。於是網上搜索,發現市面上很少有斷點上傳的案例,有找到一個案例也是采用SOCKET作為上傳方式(大文件上傳,不適合使用POST,GET形式)。由於大文件夾不適合http上傳的方式,所以就想能不能把大文件切割成n塊小文件,然后上傳這些小文件,所有小文件全部上傳成功后再在服務器上進行拼接。這樣不就可以實現斷點上傳,又解決了http不適合上傳大文件的難題了嗎!!!
原理分析
*******Android客戶端********
首先,android端調用服務器接口1,參數為filename(服務器標識判斷是否上傳過)
如果存在filename,說明之前上傳過,則續傳;如果沒有,則從零開始上傳。
然后,android端調用服務器接口2,傳入參數name,chunck(傳到第幾塊),chuncks(總共多少塊)
*******服務器端********
接口一:根據上傳文件名稱filename 判斷是否之前上傳過,沒有則返回客戶端chunck=1,有則讀取記錄chunck並返回。
接口二:上傳文件,如果上傳塊數chunck=chuncks,遍歷所有塊文件拼接成一個完整文件。
*******服務端源代碼********
服務器接口1
@WebServlet(urlPatterns = { "/ckeckFileServlet" }) public class CkeckFileServlet extends HttpServlet { private FileUploadStatusServiceI statusService; String repositoryPath; String uploadPath; @Override public void init(ServletConfig config) throws ServletException { ServletContext servletContext = config.getServletContext(); WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext); statusService = (FileUploadStatusServiceI) context.getBean("fileUploadStatusServiceImpl"); repositoryPath = FileUtils.getTempDirectoryPath(); uploadPath = config.getServletContext().getRealPath("datas/uploader"); File up = new File(uploadPath); if (!up.exists()) { up.mkdir(); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // TODO Auto-generated method stub
String fileName = new String(req.getParameter("filename")); //String chunk = req.getParameter("chunk"); //System.out.println(chunk);
System.out.println(fileName); resp.setContentType("text/json; charset=utf-8"); TfileUploadStatus file = statusService.get(fileName); try { if (file != null) { int schunk = file.getChunk(); deleteFile(uploadPath + schunk + "_" + fileName); //long off = schunk * Long.parseLong(chunkSize);
resp.getWriter().write("{\"off\":" + schunk + "}"); } else { resp.getWriter().write("{\"off\":1}"); } } catch (Exception e) { // TODO Auto-generated catch block
e.printStackTrace(); } } }
服務器接口2
@WebServlet(urlPatterns = { "/uploaderWithContinuinglyTransferring" }) public class UploaderServletWithContinuinglyTransferring extends HttpServlet { private static final long serialVersionUID = 1L; private FileUploadStatusServiceI statusService; String repositoryPath; String uploadPath; @Override public void init(ServletConfig config) throws ServletException { ServletContext servletContext = config.getServletContext(); WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext); statusService = (FileUploadStatusServiceI) context.getBean("fileUploadStatusServiceImpl"); repositoryPath = FileUtils.getTempDirectoryPath(); System.out.println("臨時目錄:" + repositoryPath); uploadPath = config.getServletContext().getRealPath("datas/uploader"); System.out.println("目錄:" + uploadPath); File up = new File(uploadPath); if (!up.exists()) { up.mkdir(); } } @SuppressWarnings("unchecked") public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setCharacterEncoding("UTF-8"); Integer schunk = null;// 分割塊數 Integer schunks = null;// 總分割數 String name = null;// 文件名 BufferedOutputStream outputStream = null; if (ServletFileUpload.isMultipartContent(request)) { try { DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024); factory.setRepository(new File(repositoryPath));// 設置臨時目錄 ServletFileUpload upload = new ServletFileUpload(factory); upload.setHeaderEncoding("UTF-8"); upload.setSizeMax(5 * 1024 * 1024 * 1024);// 設置附近大小 List<FileItem> items = upload.parseRequest(request); // 生成新文件名 String newFileName = null; for (FileItem item : items) { if (!item.isFormField()) {// 如果是文件類型 name = newFileName;// 獲得文件名 if (name != null) { String nFname = newFileName; if (schunk != null) { nFname = schunk + "_" + name; } File savedFile = new File(uploadPath, nFname); item.write(savedFile); } } else { // 判斷是否帶分割信息 if (item.getFieldName().equals("chunk")) { schunk = Integer.parseInt(item.getString()); //System.out.println(schunk); } if (item.getFieldName().equals("chunks")) { schunks = Integer.parseInt(item.getString()); } if (item.getFieldName().equals("name")) { newFileName = new String(item.getString()); } } } //System.out.println(schunk + "/" + schunks); if (schunk != null && schunk == 1) { TfileUploadStatus file = statusService.get(newFileName); if (file != null) { statusService.updateChunk(newFileName, schunk); } else { statusService.add(newFileName, schunk, schunks); } } else { TfileUploadStatus file = statusService.get(newFileName); if (file != null) { statusService.updateChunk(newFileName, schunk); } } if (schunk != null && schunk.intValue() == schunks.intValue()) { outputStream = new BufferedOutputStream(new FileOutputStream(new File(uploadPath, newFileName))); // 遍歷文件合並 for (int i = 1; i <= schunks; i++) { //System.out.println("文件合並:" + i + "/" + schunks); File tempFile = new File(uploadPath, i + "_" + name); byte[] bytes = FileUtils.readFileToByteArray(tempFile); outputStream.write(bytes); outputStream.flush(); tempFile.delete(); } outputStream.flush(); } response.getWriter().write("{\"status\":true,\"newName\":\"" + newFileName + "\"}"); } catch (FileUploadException e) { e.printStackTrace(); response.getWriter().write("{\"status\":false}"); } catch (Exception e) { e.printStackTrace(); response.getWriter().write("{\"status\":false}"); } finally { try { if (outputStream != null) outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
*******android端源碼********
UploadTask 上傳線程類
package com.mainaer.wjoklib.okhttp.upload; import android.database.sqlite.SQLiteDatabase; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.text.TextUtils; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; /** * 上傳線程 * * @author hst * @date 2016/9/6 . */ public class UploadTask implements Runnable { private static String FILE_MODE = "rwd"; private OkHttpClient mClient; private SQLiteDatabase db; private UploadTaskListener mListener; private Builder mBuilder; private String id;// task id private String url;// file url private String fileName; // File name when saving private int uploadStatus; private int chunck, chuncks;//流塊 private int position; private int errorCode; static String BOUNDARY = "----------" + System.currentTimeMillis(); public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("multipart/form-data;boundary=" + BOUNDARY); private UploadTask(Builder builder) { mBuilder = builder; mClient = new OkHttpClient(); this.id = mBuilder.id; this.url = mBuilder.url; this.fileName = mBuilder.fileName; this.uploadStatus = mBuilder.uploadStatus; this.chunck = mBuilder.chunck; this.setmListener(mBuilder.listener); // 以kb為計算單位 } @Override public void run() { try { int blockLength = 1024 * 1024; File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator +fileName); if (file.length() % blockLength == 0) { chuncks = (int) file.length() / blockLength; } else { chuncks = (int) file.length() / blockLength + 1; } while (chunck <= chuncks&&uploadStatus!= UploadStatus.UPLOAD_STATUS_PAUSE&&uploadStatus!= UploadStatus.UPLOAD_STATUS_ERROR) { uploadStatus = UploadStatus.UPLOAD_STATUS_UPLOADING; Map<String, String> params = new HashMap<String, String>(); params.put("name", fileName); params.put("chunks", chuncks + ""); params.put("chunk", chunck + ""); final byte[] mBlock = FileUtils.getBlock((chunck - 1) * blockLength, file, blockLength); MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MultipartBody.FORM); addParams(builder, params); RequestBody requestBody = RequestBody.create(MEDIA_TYPE_MARKDOWN, mBlock); builder.addFormDataPart("mFile", fileName, requestBody); Request request = new Request.Builder() .url(url+ "uploaderWithContinuinglyTransferring") .post(builder.build()) .build(); Response response = null; response = mClient.newCall(request).execute(); if (response.isSuccessful()) { onCallBack(); chunck++; /* if (chunck <= chuncks) { run(); }*/ } else { uploadStatus = UploadStatus.UPLOAD_STATUS_ERROR; onCallBack(); } } } catch (IOException e) { uploadStatus = UploadStatus.UPLOAD_STATUS_ERROR; onCallBack(); e.printStackTrace(); } } /* *//** * 刪除數據庫文件和已經上傳的文件 *//* public void cancel() { if (mListener != null) mListener.onCancel(UploadTask.this); }*/ /** * 分發回調事件到ui層 */ private void onCallBack() { mHandler.sendEmptyMessage(uploadStatus); // 同步manager中的task信息 //UploadManager.getInstance().updateUploadTask(this); } Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { int code = msg.what; switch (code) { // 上傳失敗 case UploadStatus.UPLOAD_STATUS_ERROR: mListener.onError(UploadTask.this, errorCode,position); break; // 正在上傳 case UploadStatus.UPLOAD_STATUS_UPLOADING: mListener.onUploading(UploadTask.this, getDownLoadPercent(), position); // 暫停上傳 break; case UploadStatus.UPLOAD_STATUS_PAUSE: mListener.onPause(UploadTask.this); break; } } }; private String getDownLoadPercent() { String baifenbi = "0";// 接受百分比的值 if (chunck >= chuncks) { return "100"; } double baiy = chunck * 1.0; double baiz = chuncks * 1.0; // 防止分母為0出現NoN if (baiz > 0) { double fen = (baiy / baiz) * 100; //NumberFormat nf = NumberFormat.getPercentInstance(); //nf.setMinimumFractionDigits(2); //保留到小數點后幾位 // 百分比格式,后面不足2位的用0補齊 //baifenbi = nf.format(fen); //注釋掉的也是一種方法 DecimalFormat df1 = new DecimalFormat("0");//0.00 baifenbi = df1.format(fen); } return baifenbi; } private String getFileNameFromUrl(String url) { if (!TextUtils.isEmpty(url)) { return url.substring(url.lastIndexOf("/") + 1); } return System.currentTimeMillis() + ""; } private void close(Closeable closeable) { try { closeable.close(); } catch (IOException e) { e.printStackTrace(); } } public void setClient(OkHttpClient mClient) { this.mClient = mClient; } public Builder getBuilder() { return mBuilder; } public void setBuilder(Builder builder) { this.mBuilder = builder; } public String getId() { if (!TextUtils.isEmpty(id)) { } else { id = url; } return id; } public String getUrl() { return url; } public String getFileName() { return fileName; } public void setUploadStatus(int uploadStatus) { this.uploadStatus = uploadStatus; } public int getUploadStatus() { return uploadStatus; } public void setmListener(UploadTaskListener mListener) { this.mListener = mListener; } public static class Builder { private String id;// task id private String url;// file url private String fileName; // File name when saving private int uploadStatus = UploadStatus.UPLOAD_STATUS_INIT; private int chunck;//第幾塊 private UploadTaskListener listener; /** * 作為上傳task開始、刪除、停止的key值,如果為空則默認是url * * @param id * @return */ public Builder setId(String id) { this.id = id; return this; } /** * 上傳url(not null) * * @param url * @return */ public Builder setUrl(String url) { this.url = url; return this; } /** * 設置上傳狀態 * * @param uploadStatus * @return */ public Builder setUploadStatus(int uploadStatus) { this.uploadStatus = uploadStatus; return this; } /** * 第幾塊 * * @param chunck * @return */ public Builder setChunck(int chunck) { this.chunck = chunck; return this; } /** * 設置文件名 * * @param fileName * @return */ public Builder setFileName(String fileName) { this.fileName = fileName; return this; } /** * 設置上傳回調 * * @param listener * @return */ public Builder setListener(UploadTaskListener listener) { this.listener = listener; return this; } public UploadTask build() { return new UploadTask(this); } } private void addParams(MultipartBody.Builder builder, Map<String, String> params) { if (params != null && !params.isEmpty()) { for (String key : params.keySet()) { builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"" + key + "\""), RequestBody.create(null, params.get(key))); } } } }
UploadManager上傳管理器
package com.mainaer.wjoklib.okhttp.upload;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
/**
* 上傳管理器
*
* @author wangjian
* @date 2016/5/13 .
*/
public class UploadManager {
private static Context mContext;
private static SQLiteDatabase db;
private OkHttpClient mClient;
private int mPoolSize = 20;
// 將執行結果保存在future變量中
private Map<String, Future> mFutureMap;
private ExecutorService mExecutor;
private Map<String, UploadTask> mCurrentTaskList;
static UploadManager manager;
/**
* 方法加鎖,防止多線程操作時出現多個實例
*/
private static synchronized void init() {
if (manager == null) {
manager = new UploadManager();
}
}
/**
* 獲得當前對象實例
*
* @return 當前實例對象
*/
public final static UploadManager getInstance() {
if (manager == null) {
init();
}
return manager;
}
/**
* 管理器初始化,建議在application中調用
*
* @param context
*/
public static void init(Context context, SQLiteDatabase db1) {
mContext = context;
db = db1;
getInstance();
}
public UploadManager() {
initOkhttpClient();
// 初始化線程池
mExecutor = Executors.newFixedThreadPool(mPoolSize);
mFutureMap = new HashMap<>();
mCurrentTaskList = new HashMap<>();
}
/**
* 初始化okhttp
*/
private void initOkhttpClient() {
OkHttpClient.Builder okBuilder = new OkHttpClient.Builder();
okBuilder.connectTimeout(1000, TimeUnit.SECONDS);
okBuilder.readTimeout(1000, TimeUnit.SECONDS);
okBuilder.writeTimeout(1000, TimeUnit.SECONDS);
mClient = okBuilder.build();
}
/**
* 添加上傳任務
*
* @param uploadTask
*/
public void addUploadTask(UploadTask uploadTask) {
if (uploadTask != null && !isUploading(uploadTask)) {
uploadTask.setClient(mClient);
uploadTask.setUploadStatus(UploadStatus.UPLOAD_STATUS_INIT);
// 保存上傳task列表
mCurrentTaskList.put(uploadTask.getId(), uploadTask);
Future future = mExecutor.submit(uploadTask);
mFutureMap.put(uploadTask.getId(), future);
}
}
private boolean isUploading(UploadTask task) {
if (task != null) {
if (task.getUploadStatus() == UploadStatus.UPLOAD_STATUS_UPLOADING) {
return true;
}
}
return false;
}
/**
* 暫停上傳任務
*
* @param id 任務id
*/
public void pause(String id) {
UploadTask task = getUploadTask(id);
if (task != null) {
task.setUploadStatus(UploadStatus.UPLOAD_STATUS_PAUSE);
}
}
/**
* 重新開始已經暫停的上傳任務
*
* @param id 任務id
*/
public void resume(String id, UploadTaskListener listener) {
UploadTask task = getUploadTask(id);
if (task != null) {
addUploadTask(task);
}
}
/* *//**
* 取消上傳任務(同時會刪除已經上傳的文件,和清空數據庫緩存)
*
* @param id 任務id
* @param listener
*//*
public void cancel(String id, UploadTaskListener listener) {
UploadTask task = getUploadTask(id);
if (task != null) {
mCurrentTaskList.remove(id);
mFutureMap.remove(id);
task.setmListener(listener);
task.cancel();
task.setDownloadStatus(UploadStatus.DOWNLOAD_STATUS_CANCEL);
}
}*/
/**
* 實時更新manager中的task信息
*
* @param task
*/
public void updateUploadTask(UploadTask task) {
if (task != null) {
UploadTask currTask = getUploadTask(task.getId());
if (currTask != null) {
mCurrentTaskList.put(task.getId(), task);
}
}
}
/**
* 獲得指定的task
*
* @param id task id
* @return
*/
public UploadTask getUploadTask(String id) {
UploadTask currTask = mCurrentTaskList.get(id);
if (currTask == null) {
currTask = parseEntity2Task(new UploadTask.Builder().build());
// 放入task list中
mCurrentTaskList.put(id, currTask);
}
return currTask;
}
private UploadTask parseEntity2Task(UploadTask currTask) {
UploadTask.Builder builder = new UploadTask.Builder()//
.setUploadStatus(currTask.getUploadStatus())
.setFileName(currTask.getFileName())//
.setUrl(currTask.getUrl())
.setId(currTask.getId());
currTask.setBuilder(builder);
return currTask;
}
}
FileUtils文件分塊類
package com.mainaer.wjoklib.okhttp.upload; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; public class FileUtils { public static byte[] getBlock(long offset, File file, int blockSize) { byte[] result = new byte[blockSize]; RandomAccessFile accessFile = null; try { accessFile = new RandomAccessFile(file, "r"); accessFile.seek(offset); int readSize = accessFile.read(result); if (readSize == -1) { return null; } else if (readSize == blockSize) { return result; } else { byte[] tmpByte = new byte[readSize]; System.arraycopy(result, 0, tmpByte, 0, readSize); return tmpByte; } } catch (IOException e) { e.printStackTrace(); } finally { if (accessFile != null) { try { accessFile.close(); } catch (IOException e1) { } } } return null; } }
UploadTaskListener 接口類
package com.mainaer.wjoklib.okhttp.upload; import com.mainaer.wjoklib.okhttp.download.DownloadStatus; import java.io.File; /** * Created by hst on 16/9/21. */ public interface UploadTaskListener { /** * 上傳中 * * @param percent * @param uploadTask */ void onUploading(UploadTask uploadTask, String percent,int position) /** * 上傳成功 * * @param file * @param uploadTask */ void onUploadSuccess(UploadTask uploadTask, File file); /** * 上傳失敗 * * @param uploadTask * @param errorCode {@link DownloadStatus} */ void onError(UploadTask uploadTask, int errorCode,int position); /** * 上傳暫停 * * @param uploadTask * */ void onPause(UploadTask uploadTask); }
android源碼地址:https://github.com/handsometong/okhttpUpLoader