OkHttp文件上傳(2):實現(xiàn)文件分塊上傳

前言

分塊上傳和斷點下載很像,就是講文件分為多份來傳輸鼎姐,從而實現(xiàn)暫停和繼續(xù)傳輸洁墙。區(qū)別是斷點下載的進度保存在客戶端蛹疯,ey往是寫入數(shù)據(jù)庫,分塊上傳的進度保存在服務器热监,每次可以通過文件的md5請求服務器捺弦,來獲取最新的上傳偏移量。但是這樣明顯效率偏低孝扛,客戶端可以把offSet保存在內(nèi)存列吼,每上傳一塊文件服務器返回下一次的offSet。只不過這個offSet不需要保存在數(shù)據(jù)庫苦始,每次app關(guān)閉在打開繼續(xù)上傳可以請求服務器寞钥,獲取最新偏移量。

分塊上傳原理

1.客戶端向服務端申請文件的上傳地址

a. 如果上傳過陌选,直接返回uuid (快速上傳)

b. 沒上傳過理郑,返回 上傳地址url + 上傳偏移量offset

下面上傳一段31M大小的mp4文件,申請上傳地址服務端返回offSet = 0表示文件沒有上傳過咨油,需要從頭開始上傳

image.png

2.客戶端對本地文件進行分塊您炉,比如10M為一塊chunk

上傳第一塊:

image.png

3.客戶端以標準表單方式,上傳 offset 到 offset+chunk的文件分塊役电,每次上傳完服務端返回新的offset赚爵,客戶端更新offset值并繼續(xù)下一次上傳,如此循環(huán)。

上傳最后一塊:

image.png

4.最后服務端返回文件uuid冀膝,代表整個文件上傳成功

基于Okhttp的實現(xiàn)

Okhttp已經(jīng)支持表單形式的文件上傳唁奢,剩下的關(guān)鍵就是:

構(gòu)造分塊文件的RequestBody,對本地文件分塊窝剖,和服務端約定相關(guān)header麻掸,保存offset實現(xiàn)分塊上傳

構(gòu)造RequestBody

繼承之前實現(xiàn)的進度監(jiān)聽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 ;

        //計算分塊數(shù)

        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 字節(jié)

            sink.buffer().flush();

            offset += chunk;

            listener.transferred( offset );

        }

    }

}

注意這個RequestBody傳入Byte數(shù)組,從而實現(xiàn)了對文件的分塊上傳赐纱。

對文件分塊

上面的RequestBody支持傳輸Byte數(shù)組论笔,那么如何把文件切割成byte[]:


    /**

     * 文件分塊工具

     * @param offset 起始偏移位置

     * @param file 文件

     * @param blockSize 分塊大小

     * @return 分塊數(shù)據(jù)

     */

    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的分塊上傳

關(guān)鍵就是構(gòu)造Request對象:


    protected Request generateRequest(String url) {

        // 獲取分塊數(shù)據(jù),按照每次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();

        // 創(chuàng)建Request對象

        Request request = new Request.Builder()

                .url(url)

                .post(requestBody)

                .build();

        return request;

    }

用OkHttp執(zhí)行上傳:


上傳開始前調(diào)用獲取上傳地址的接口淫痰,從而獲取初始offSet最楷,然后開始上傳:

```java

while (offset < fileInfo.fileSize) {

          //doUpload是阻塞式方法,必須返回結(jié)果后才下一次調(diào)用

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

            if (result != STATUS_RETRY) {

                return result;

            }

        }

定義文件上傳的執(zhí)行方法doUpload:(和上文OkHttp監(jiān)聽進度的文件上傳一樣待错,只是不過構(gòu)造的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讀取服務端結(jié)果籽孙,更新offSet數(shù)值:


    // 解析服務端響應結(jié)果

    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; // 分塊數(shù)據(jù)上傳完成犯建,修正偏移

                    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值是保存在服務端的瓜客,比如中途上傳失敗了适瓦,下次繼續(xù)上傳,調(diào)用申請上傳地址接口谱仪,服務端會返回最新的offSet告訴你從哪開始上傳玻熙。

2.本文方案不支持多線程分塊上傳,必須按照文件切割的順序疯攒,依次上傳

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗦随,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子敬尺,更是在濱河造成了極大的恐慌枚尼,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砂吞,死亡現(xiàn)場離奇詭異署恍,居然都是意外死亡,警方通過查閱死者的電腦和手機呜舒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門锭汛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人袭蝗,你說我怎么就攤上這事唤殴。” “怎么了到腥?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵朵逝,是天一觀的道長。 經(jīng)常有香客問我乡范,道長配名,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任晋辆,我火速辦了婚禮渠脉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瓶佳。我一直安慰自己芋膘,他們只是感情好,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布霸饲。 她就那樣靜靜地躺著为朋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪厚脉。 梳的紋絲不亂的頭發(fā)上习寸,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音傻工,去河邊找鬼霞溪。 笑死,一個胖子當著我的面吹牛中捆,可吹牛的內(nèi)容都是我干的威鹿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼轨香,長吁一口氣:“原來是場噩夢啊……” “哼忽你!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起臂容,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤科雳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后脓杉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糟秘,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年球散,在試婚紗的時候發(fā)現(xiàn)自己被綠了尿赚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖凌净,靈堂內(nèi)的尸體忽然破棺而出悲龟,到底是詐尸還是另有隱情,我是刑警寧澤冰寻,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布须教,位于F島的核電站,受9級特大地震影響斩芭,放射性物質(zhì)發(fā)生泄漏轻腺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一划乖、第九天 我趴在偏房一處隱蔽的房頂上張望贬养。 院中可真熱鬧,春花似錦琴庵、人聲如沸煤蚌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尉桩。三九已至,卻和暖如春贪庙,著一層夾襖步出監(jiān)牢的瞬間蜘犁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工止邮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留这橙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓导披,卻偏偏與公主長得像屈扎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子撩匕,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理鹰晨,服務發(fā)現(xiàn),斷路器止毕,智...
    卡卡羅2017閱讀 134,699評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,283評論 25 707
  • 參考Android網(wǎng)絡請求心路歷程Android Http接地氣網(wǎng)絡請求(HttpURLConnection) 一...
    合肥黑閱讀 21,290評論 7 63
  • 就在今天剛被輔導員由于黨課結(jié)業(yè)考試沒及格被老師訓過的下午,在微博上看到一個秒拍視頻谨朝,不長卤妒,兩分半鐘甥绿,大概就是催著我...
    Connie王閱讀 1,158評論 0 0
  • 于2016/11/21日、正好進入了冬季则披、我們來到了這個農(nóng)家小院共缕、張谷英 這個村莊環(huán)境優(yōu)美、三面是一座座山峰聳立收叶、...
    兩個老頑童閱讀 248評論 0 0