Breakpoint upload/download-implementation idea and code

1. Continue downloading from breakpoint:

Ideas:

  1. 断点续下载不需要服务端提供特殊接口,客户端自己实现就可以,http 协议本身支持续下载
  2. get 方式下载
  3. 开始下载的文件位置传参:统一的 “Range” 字段,http 协议自带
  4. 需要客户端创建数据库,记录断点下载信息

code:

/**
 * 文件下载任务
 */
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
    private final String TAG = DownloadTask.this.getClass().getSimpleName();

    public static final int TYPE_SUCCESS = 0;
    public static final int TYPE_FAILED = 1;
    public static final int TYPE_PAUSED = 2;
    public static final int TYPE_CANCELED = 3;

    private Context mContext;
    private DownloadFileBean mDownloadBean;
    private DownloadListener mDownloadListener;
    private boolean isCanceled = false;
    private boolean isPaused = false;
    private int mLastProgress;

    public DownloadTask(Context context, DownloadFileBean downloadBean) {
        try {
            this.mContext = context;
            this.mDownloadBean = downloadBean.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected Integer doInBackground(String... strings) {
        Log.d(TAG, "doInBackground:");
        if (mDownloadBean == null) {
            Log.d(TAG, "doInBackground:Error: Download bean null!");
            return TYPE_FAILED;
        }
        Log.d(TAG, "doInBackground: mDownloadBean=" + mDownloadBean);
        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file;
        long downloadLength = 0;   //记录已经下载的文件长度
        //文件下载地址
        String downloadUrl = mDownloadBean.getUrl();
        String filePath = mDownloadBean.getPath();
        file = new File(filePath);
        if (file.exists()) {
            //如果文件存在的话,得到文件的大小
            downloadLength = file.length();
        }
        //得到下载内容的大小
        long contentLength = getContentLengthOk(downloadUrl);
        Log.d(TAG, "doInBackground: contentLength=" + contentLength +
                " downloadLength=" + downloadLength);
        if (contentLength == 0) {
            return TYPE_FAILED;
        } else if (contentLength == downloadLength) {
            //已下载字节和文件总字节相等,说明已经下载完成了
            Log.d(TAG, "doInBackground: Already exists!");
            return TYPE_SUCCESS;
        }

        OkHttpClient client = new OkHttpClient();
        /**
         * HTTP请求是有一个Header的,里面有个Range属性是定义下载区域的,它接收的值是一个区间范围,
         * 比如:Range:bytes=0-10000。这样我们就可以按照一定的规则,将一个大文件拆分为若干很小的部分,
         * 然后分批次的下载,每个小块下载完成之后,再合并到文件中;这样即使下载中断了,重新下载时,
         * 也可以通过文件的字节长度来判断下载的起始点,然后重启断点续传的过程,直到最后完成下载过程。
         */
        Request request = new Request.Builder()
                .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
                .url(downloadUrl)
                .build();
        try {
            Response response = client.newCall(request).execute();
            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {

                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {

                }
            });
            if (response != null) {
                Log.d(TAG, "doInBackground: Get stream form response!");
                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");
                savedFile.seek(downloadLength);//跳过已经下载的字节
                byte[] b = new byte[1024];
                int total = 0;
                int len;
                Log.d(TAG, "doInBackground: Start read file stream!");
                while ((len = is.read(b)) != -1) {
                    if (isCanceled) {
                        return TYPE_CANCELED;
                    } else if (isPaused) {
                        return TYPE_PAUSED;
                    } else {
                        total += len;
                        savedFile.write(b, 0, len);
                        //计算已经下载的百分比
                        int progress = (int) ((total + downloadLength) * 100 / contentLength);
                        //注意:在doInBackground()中是不可以进行UI操作的,如果需要更新UI,比如说反馈当前任务的执行进度,
                        //可以调用publishProgress()方法完成。
                        publishProgress(progress);
                    }
                }
                response.close();
                return TYPE_SUCCESS;
            }
        } catch (IOException e) {
            Log.d(TAG, "doInBackground:IOException: " + e.getMessage());
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (savedFile != null) {
                    savedFile.close();
                }
                if (isCanceled && file != null) {
                    file.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }

    /**
     * 得到下载内容的完整大小
     */
    private long getContentLengthOk(String downloadUrl) {
        Log.d(TAG, "getContentLengthOk:");
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(downloadUrl).build();
        try {
            Response response = client.newCall(request).execute();
            if (response != null && response.isSuccessful()) {
                long contentLength = response.body().contentLength();
                Log.d(TAG, "getContentLengthOk: contentLength=" + contentLength);
                response.body().close();
                return contentLength;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 当在后台任务中调用了publishProgress(Progress...)方法之后,onProgressUpdate()方法
     * 就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以
     * 对界面进行相应的更新。
     *
     * @param values
     */
    @Override
    protected void onProgressUpdate(Integer... values) {
        int progress = values[0];
        if (progress > mLastProgress) {
            if (mDownloadListener != null) mDownloadListener.onProgress(progress);
            mLastProgress = progress;
        }
    }

    /**
     * 当后台任务执行完毕并通过Return语句进行返回时,这个方法就很快被调用。返回的数据会作为参数
     * 传递到此方法中,可以利用返回的数据来进行一些UI操作。
     *
     * @param status
     */
    @Override
    protected void onPostExecute(Integer status) {
        switch (status) {
            case TYPE_SUCCESS:
                if (mDownloadListener != null) mDownloadListener.onSuccess();
                break;
            case TYPE_FAILED:
                if (mDownloadListener != null) mDownloadListener.onFailed();
                break;
            case TYPE_PAUSED:
                if (mDownloadListener != null) mDownloadListener.onPaused();
                break;
            case TYPE_CANCELED:
                if (mDownloadListener != null) mDownloadListener.onCanceled();
                break;
            default:
                break;
        }
    }

    public void pauseDownload() {
        isPaused = true;
    }

    public void cancelDownload() {
        isCanceled = true;
    }

    public void setDownloadListener(DownloadListener downloadListener) {
        this.mDownloadListener = downloadListener;
    }

}

/**
 * 文件下载任务回调接口
 */
public interface DownloadListener {
    /**
     * 通知当前的下载进度
     */
    void onProgress(int progress);

    /**
     * 通知下载成功
     */
    void onSuccess();

    /**
     * 通知下载失败
     */
    void onFailed();

    /**
     * 通知下载暂停
     */
    void onPaused();

    /**
     * 通知下载取消事件
     */
    void onCanceled();
}

Reference:
Use HttpUrlConnection native interface to encapsulate request implementation: https://blog.csdn.net/SEU_Calvin/article/details/53749776
Use OkHttp to encapsulate request implementation: https://juejin.cn/post/6844903854115389447

2. File upload in parts:

Ideas:

1. 文件分片上传,需要服务端给出特殊接口,实现文件分片接收和整体拼接的操作;
2. 端上从文件指定位置读取并写入传输流就可以;
3. 需要客户端创建数据库,记录断点上传信息

code:

/**
 * 分片文件,上传,这部分是通用的
 */
public class PartFileBody extends FileBody {

    /**
     * 分片写入的开始位置,字节偏移量
     */
    private final long mOffset;
    /**
     * 分片写入的长度,最后一次分片大小不定,前面的分片都是固定的
     */
    private final long mLength;
    /**
     * 文件流输出计数
     */
    private long mCount;

    public PartFileBody(File file, long offset, long length) {
        super(file);
        this.mOffset = offset;
        this.mLength = length;
        this.mCount = 0;
    }

    @Override
    public long getContentLength() {
        return mLength;
    }

    @Override
    public void writeTo(OutputStream out) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(mFile, "r");
        raf.seek(mOffset);
        try {
            byte[] buffer = new byte[4096];
            int length;
            while ((length = raf.read(buffer)) != -1) {
                if ((mCount + length) > mLength) {
                    length = (int) (mLength - mCount);
                }
                out.write(buffer, 0, length);
                mCount += length;
                if (mCount == mLength) {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            raf.close();
        }
    }
}

/**
 * 上传请求,这部分是自定义的服务端接口协议,关键字段都一样,具体传输方式可自定义
 */
public synchronized void uploadFile(Context ctx, UploadFileBean uploadBean, String filePath, long contentOffset, long contentLength, RequestListener listener) {
    String url = url;

    File file = new File(filePath);
    Log.d(TAG, "VFU:" + "uploadFile: " + file .getAbsolutePath());
    MultipartBody body = new MultipartBody();
    body.addPart("content", new PartFileBody(file , contentOffset, contentLength));
    Request request = new Request
            .Builder()
            .url(url)
            .urlParam("size", uploadBean.getSize())
            .urlParam("sid", uploadBean.getSid())
            .urlParam("range_start", uploadBean.getRange_start())
            .urlParam("range_end", uploadBean.getRange_end())
            .post(body)
            .build();
    Log.d(TAG, "VFU:" + "uploadFile:");
    //TODO 发起请求
}

3. Summary

Theoretically speaking, both upload and download can be transmitted in pieces, and different pieces of a file can be transmitted synchronously to improve efficiency. However, in actual use, uploaded files are generally uploaded in fragments, and whether they are synchronized depends on business needs, while file downloads only need to be resumed without fragmentation;

You can refer to: the difference between breakpoint resume transmission and fragment transmission

Guess you like

Origin blog.csdn.net/u013168615/article/details/128422115