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

前言

分块上传和断点下载很像,就是讲文件分为多份来传输,从而实现暂停和继续传输。区别是断点下载的进度保存在客户端,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.本文方案不支持多线程分块上传,必须按照文件切割的顺序,依次上传

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,048评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,471评论 25 707
  • 参考Android网络请求心路历程Android Http接地气网络请求(HttpURLConnection) 一...
    合肥黑阅读 20,960评论 7 63
  • 就在今天刚被辅导员由于党课结业考试没及格被老师训过的下午,在微博上看到一个秒拍视频,不长,两分半钟,大概就是催着我...
    Connie王阅读 1,096评论 0 0
  • 于2016/11/21日、正好进入了冬季、我们来到了这个农家小院、张谷英 这个村庄环境优美、三面是一座座山峰耸立、...
    两个老顽童阅读 200评论 0 0