OkHttp文件上传(2):实现文件分块上传

前言

分块上传和断点下载很像,就是讲文件分为多份来传输,从而实现暂停和继续传输。区别是断点下载的进度保存在客户端,ey往是写入数据库,分块上传的进度保存在服务器,每次可以通过文件的md5请求服务器,来获取最新的上传偏移量。但是这样明显效率偏低,客户端可以把offSet保存在内存,每上传一块文件服务器返回下一次的offSet。只不过这个offSet不需要保存在数据库,每次app关闭在打开继续上传可以请求服务器,获取最新偏移量。

分块上传原理

1.客户端向服务端申请文件的上传地址

a. 如果上传过,直接返回uuid (快速上传)

b. 没上传过,返回 上传地址url + 上传偏移量offset

下面上传一段31M大小的mp4文件,申请上传地址服务端返回offSet = 0表示文件没有上传过,需要从头开始上传

OkHttp文件上传(2):实现文件分块上传_第1张图片
image.png

2.客户端对本地文件进行分块,比如10M为一块chunk

上传第一块:

OkHttp文件上传(2):实现文件分块上传_第2张图片
image.png

3.客户端以标准表单方式,上传 offset 到 offset+chunk的文件分块,每次上传完服务端返回新的offset,客户端更新offset值并继续下一次上传,如此循环。

上传最后一块:

OkHttp文件上传(2):实现文件分块上传_第3张图片
image.png

4.最后服务端返回文件uuid,代表整个文件上传成功

基于Okhttp的实现

Okhttp已经支持表单形式的文件上传,剩下的关键就是:

构造分块文件的RequestBody,对本地文件分块,和服务端约定相关header,保存offset实现分块上传

构造RequestBody

继承之前实现的进度监听RequestBody:


public class MDProgressRequestBody extends FileProgressRequestBody {

    protected final byte[] content;

    public MDProgressRequestBody(byte[] content, String contentType , ProgressListener listener) {

        this.content = content;

        this.contentType = contentType;

        this. listener = listener;

    }

    @Override

    public long contentLength() {

        return content.length;

    }

    @Override

    public void writeTo(BufferedSink sink) throws IOException {

        int offset = 0 ;

        //计算分块数

        count = (int) ( content.length / SEGMENT_SIZE + (content.length % SEGMENT_SIZE != 0?1:0) );

        for( int i=0; i < count; i++ ) {

            int chunk = i != count -1  ? SEGMENT_SIZE : content.length - offset;

            sink.buffer().write(content, offset, chunk );//每次写入SEGMENT_SIZE 字节

            sink.buffer().flush();

            offset += chunk;

            listener.transferred( offset );

        }

    }

}

注意这个RequestBody传入Byte数组,从而实现了对文件的分块上传。

对文件分块

上面的RequestBody支持传输Byte数组,那么如何把文件切割成byte[]:


    /**

     * 文件分块工具

     * @param offset 起始偏移位置

     * @param file 文件

     * @param blockSize 分块大小

     * @return 分块数据

     */

    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;

    }

基于OkHttp的分块上传

关键就是构造Request对象:


    protected Request generateRequest(String url) {

        // 获取分块数据,按照每次10M的大小分块上传

        final int CHUNK_SIZE = 10 * 1024 * 1024;

        //切割文件为10M每份

        byte[] blockData = FileUtil.getBlock(offset, new File(fileInfo.filePath), CHUNK_SIZE);

        if (blockData == null) {

            throw new RuntimeException(String.format("upload file get blockData faild,filePath:%s , offest:%d", fileInfo.filePath, offset));

        }

        curBolckSize = blockData.length;

        // 分块上传,客户端和服务端约定,name字段传文件分块的始偏移量

        String formData = String.format("form-data;name=%s; filename=file", offset);

        RequestBody filePart = new MDProgressRequestBody(blockData, "application/octet-stream ", this);

        MultipartBody requestBody = new MultipartBody.Builder()

                .setType(MultipartBody.FORM)

                .addPart(Headers.of("Content-Disposition", formData), filePart)

                .build();

        // 创建Request对象

        Request request = new Request.Builder()

                .url(url)

                .post(requestBody)

                .build();

        return request;

    }

用OkHttp执行上传:


上传开始前调用获取上传地址的接口,从而获取初始offSet,然后开始上传:

```java

while (offset < fileInfo.fileSize) {

          //doUpload是阻塞式方法,必须返回结果后才下一次调用

            int result = doUpload(url);  // readResponse()会修正偏移量

            if (result != STATUS_RETRY) {

                return result;

            }

        }

定义文件上传的执行方法doUpload:(和上文OkHttp监听进度的文件上传一样,只是不过构造的Request不同)


    protected int doUpload(String url){

        try {

            OkHttpClient httpClient = OkHttpClientMgr.Instance().getOkHttpClient();

            call = httpClient.newCall( generateRequest(url) );

            Response response = call.execute();

            if (response.isSuccessful()) {

                sbFileUUID = new StringBuilder();

                return readResponse(response,sbFileUUID);

            } else( ... ) { // 重试

                return STATUS_RETRY;

            }

        } catch (IOException ioe) {

            LogUtil.e(LOG_TAG, "exception occurs while uploading file!",ioe);

        }

        return isCancelled() ? STATUS_CANCEL : STATUS_FAILED_EXIT;

    }

这里的readRespones读取服务端结果,更新offSet数值:


    // 解析服务端响应结果

    protected int readResponse(Response response, StringBuilder sbFileUUID) {

        int exitStatus = STATUS_FAILED_EXIT;

        ResponseBody body = response.body();

        if (body == null) {

            LogUtil.e(LOG_TAG, "readResponse body is null!", new Throwable());

            return exitStatus;

        }

        try {

            String content = body.string();

            JSONObject jsonObject = new JSONObject(content);

            if (jsonObject.has("uuid")) { // 上传成功,返回UUID

                String uuid = jsonObject.getString("uuid");

                if (uuid != null && !uuid.isEmpty()) {

                    sbFileUUID.append(uuid);

                    exitStatus = STATUS_SUCCESS;

                } else {

                    LogUtil.e(LOG_TAG, "readResponse fileUUID return empty! ");

                }

            } else if (jsonObject.has("offset")) { // 分块上传完成,返回新的偏移量

                long newOffset = (long) jsonObject.getLong("offset");

                if (newOffset != offset + curBolckSize) {

                    LogUtil.e(LOG_TAG, "readResponse offest-value exception ! ");

                } else {

                    offset = newOffset; // 分块数据上传完成,修正偏移

                    exitStatus = STATUS_RETRY;

                }

            } else {

                LogUtil.e(LOG_TAG, "readResponse unexpect data , no offest、uuid field !");

            }

        } catch (Exception ex) {

            LogUtil.e(LOG_TAG, "readResponse exception occurs!", ex);

        }

        return exitStatus;

    }

说明

1.offSet值是保存在服务端的,比如中途上传失败了,下次继续上传,调用申请上传地址接口,服务端会返回最新的offSet告诉你从哪开始上传。

2.本文方案不支持多线程分块上传,必须按照文件切割的顺序,依次上传

你可能感兴趣的:(OkHttp文件上传(2):实现文件分块上传)