美文网首页Android 问题杂记Android开发经验谈Android开发
OkHttp文件上传(2):实现文件分块上传

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

作者: 08_carmelo | 来源:发表于2018-01-19 22:18 被阅读2633次

    前言

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

    分块上传原理

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

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

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

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

    image.png

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

    上传第一块:

    image.png

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

    上传最后一块:

    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):实现文件分块上传

        本文链接:https://www.haomeiwen.com/subject/sdwaoxtx.html