Java實(shí)現(xiàn)斷點(diǎn)下載服務(wù)端與客戶端的示例代碼
最近在研究斷點(diǎn)下載(下載續(xù)傳)的功能,此功能需要服務(wù)端和客戶端進(jìn)行對(duì)接編寫(xiě),本篇也是記錄一下關(guān)于貼上關(guān)于實(shí)現(xiàn)服務(wù)端(Spring Boot)與客戶端(Android)是如何實(shí)現(xiàn)下載續(xù)傳功能
斷點(diǎn)下載功能(下載續(xù)傳)解釋:
客戶端由于突然性網(wǎng)絡(luò)中斷等原因,導(dǎo)致的下載失敗,這個(gè)時(shí)候重新下載,可以繼續(xù)從上次的地方進(jìn)行下載,而不是重新下載
原理
首先,我們先說(shuō)明了斷點(diǎn)續(xù)傳的功能,實(shí)際上的原理比較簡(jiǎn)單
客戶端和服務(wù)端規(guī)定好一個(gè)規(guī)則,客戶端傳遞一個(gè)參數(shù),告知服務(wù)端需要數(shù)據(jù)從何處開(kāi)始傳輸,服務(wù)端接收到參數(shù)進(jìn)行處理,之后文件讀寫(xiě)流從指定位置開(kāi)始傳輸給客戶端
實(shí)際上,上述的參數(shù),在http協(xié)議中已經(jīng)有規(guī)范,參數(shù)名為Range
而對(duì)于服務(wù)端來(lái)說(shuō),只要處理好Range請(qǐng)求頭參數(shù),即可實(shí)現(xiàn)下載續(xù)傳的功能
我們來(lái)看下Range
請(qǐng)求頭數(shù)據(jù)格式如下:
格式如下:
Range:bytes=300-800 //客戶端需要文件300-800字節(jié)范圍的數(shù)據(jù)(即500B數(shù)據(jù)) Range:bytes=300- //客戶端需要文件300字節(jié)之后的數(shù)據(jù)
我們根據(jù)上面的格式,服務(wù)端對(duì)Range
字段進(jìn)行處理(String字符串?dāng)?shù)據(jù)處理),在流中返回指定的數(shù)據(jù)大小即可
那么,如何讓流返回指定的數(shù)據(jù)大小或從指定位置開(kāi)始傳輸數(shù)據(jù)呢?
這里,Java提供了RandomAccessFile
類(lèi),通過(guò)seekTo()
方法,可以讓我們將流設(shè)置從指定位置開(kāi)始讀取或?qū)懭霐?shù)據(jù)
這里讀取和寫(xiě)入數(shù)據(jù),我是采用的Java7之后新增的NIO的Channel進(jìn)行流的寫(xiě)入(當(dāng)然,用傳統(tǒng)的文件IO流(BIO)也可以)
這里,我所說(shuō)的客戶端是指的Android客戶端,由于App開(kāi)發(fā)也是基于Java,所以也是可以使用RandomAccessFile
這個(gè)類(lèi)
對(duì)于客戶端來(lái)說(shuō),有以下邏輯:
先讀取本地已下載文件的大小,然后請(qǐng)求下載數(shù)據(jù)將文件大小的數(shù)據(jù)作為請(qǐng)求頭的數(shù)值傳到服務(wù)端,之后也是利用RandomAccessFile
移動(dòng)到文件的指定位置開(kāi)始寫(xiě)入數(shù)據(jù)即可
擴(kuò)展-大文件快速下載思路
利用上面的思路,我們還可以可以得到一個(gè)大文件快速下載的思路:
如,一份文件,大小為2000B(這個(gè)大小可以通過(guò)網(wǎng)絡(luò)請(qǐng)求,從返回?cái)?shù)據(jù)的請(qǐng)求頭content-length獲取
獲?。?/p>
客戶端拿回到文件的總大小,根據(jù)調(diào)優(yōu)算法,將平分成合適的N份,通過(guò)線程池,來(lái)下載這個(gè)N個(gè)單文件
在下載完畢之后,將N個(gè)文件按照順序合并成單個(gè)文件即可
代碼
上面說(shuō)明了具體的思路,那么下面就是貼出服務(wù)端和客戶端的代碼示例
服務(wù)端
服務(wù)端是采用的spring boot進(jìn)行編寫(xiě)
/** * 斷點(diǎn)下載文件 * * @return */ @GetMapping("download") public void download( HttpServletRequest request, HttpServletResponse response) throws IOException { //todo 這里文件按照你的需求調(diào)整 File file = new File("D:\\temp\\測(cè)試文件.zip"); if (!file.exists()) { response.setStatus(HttpStatus.NOT_FOUND.value()); return; } long fromPos = 0; long downloadSize = file.length(); if (request.getHeader("Range") != null) { response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); String[] ary = request.getHeader("Range").replaceAll("bytes=", "").split("-"); fromPos = Long.parseLong(ary[0]); downloadSize = (ary.length < 2 ? downloadSize : Long.parseLong(ary[1])) - fromPos; } //注意下面設(shè)置的相關(guān)請(qǐng)求頭 response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); //相當(dāng)于設(shè)置請(qǐng)求頭content-length response.setContentLengthLong(downloadSize); //使用URLEncoder處理中文名(否則會(huì)出現(xiàn)亂碼) response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8")); response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Content-Range", String.format("bytes %s-%s/%s", fromPos, (fromPos + downloadSize), downloadSize)); RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); randomAccessFile.seek(fromPos); FileChannel inChannel = randomAccessFile.getChannel(); WritableByteChannel outChannel = Channels.newChannel(response.getOutputStream()); try { while (downloadSize > 0) { long count = inChannel.transferTo(fromPos, downloadSize, outChannel); if (count > 0) { fromPos += count; downloadSize -= count; } } inChannel.close(); outChannel.close(); randomAccessFile.close(); } catch (IOException e) { e.printStackTrace(); } }
客戶端
Android客戶端,是基于Okhttp的網(wǎng)絡(luò)框架寫(xiě)的,需要先引用依賴(lài)
implementation 'com.squareup.okhttp3:okhttp:3.9.0'
下面給出的是封裝好的方法(含進(jìn)度,下載失敗和成功回調(diào)):
package com.tyky.update.utils; import com.blankj.utilcode.util.ThreadUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class FileDownloadUtil { public static void download(String url, File file, OnDownloadListener listener) { //http://10.232.107.44:9060/swan-business/file/download // 利用通道完成文件的復(fù)制(非直接緩沖區(qū)) ThreadUtils.getIoPool().submit(new Runnable() { @Override public void run() { try { //續(xù)傳開(kāi)始的進(jìn)度 long startSize = 0; if (file.exists()) { startSize = file.length(); } OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); Request request = new Request.Builder().url(url) .addHeader("Range", "bytes=" + startSize) .get().build(); Call call = okHttpClient.newCall(request); Response resp = call.execute(); double length = Long.parseLong(resp.header("Content-Length")) * 1.0; InputStream fis = resp.body().byteStream(); ReadableByteChannel fisChannel = Channels.newChannel(fis); RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); //從上次未完成的位置開(kāi)始下載 randomAccessFile.seek(startSize); FileChannel foschannel = randomAccessFile.getChannel(); // 通道沒(méi)有辦法傳輸數(shù)據(jù),必須依賴(lài)緩沖區(qū) // 分配指定大小的緩沖區(qū) ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 將通道中的數(shù)據(jù)存入緩沖區(qū)中 while (fisChannel.read(byteBuffer) != -1) { // fisChannel 中的數(shù)據(jù)讀到 byteBuffer 緩沖區(qū)中 byteBuffer.flip(); // 切換成讀數(shù)據(jù)模式 // 將緩沖區(qū)中的數(shù)據(jù)寫(xiě)入通道 foschannel.write(byteBuffer); final double progress = (foschannel.size() / length); BigDecimal two = new BigDecimal(progress); double result = two.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue(); //計(jì)算進(jìn)度,回調(diào) if (listener != null) { listener.onProgress(result); } byteBuffer.clear(); // 清空緩沖區(qū) } foschannel.close(); fisChannel.close(); randomAccessFile.close(); if (listener != null) { listener.onSuccess(file); } } catch (IOException e) { if (listener != null) { listener.onError(e); } } } }); } public interface OnDownloadListener { void onProgress(double progress); void onError(Exception e); void onSuccess(File outputFile); } }
使用:
FileDownloadUtil.download(downloadUrl, file, new FileDownloadUtil.OnDownloadListener() { @Override public void onProgress(double progress) { KLog.d("下載進(jìn)度: " + progress); } @Override public void onError(Exception e) { KLog.e("下載錯(cuò)誤: " + e.getMessage()); } @Override public void onSuccess(File outputFile) { KLog.d("下載成功"); } });
到此這篇關(guān)于Java實(shí)現(xiàn)斷點(diǎn)下載服務(wù)端與客戶端的示例代碼的文章就介紹到這了,更多相關(guān)Java斷點(diǎn)下載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中PageHelper分頁(yè)后對(duì)list操作導(dǎo)致分頁(yè)無(wú)效
在項(xiàng)目中使用分頁(yè)插件的時(shí)候發(fā)現(xiàn)PageHelper插件失效了,本文就來(lái)介紹一下Java中PageHelper分頁(yè)后對(duì)list操作導(dǎo)致分頁(yè)無(wú)效的解決方法,感興趣的可以了解一下2021-05-05Spring Boot拓展XML格式的請(qǐng)求和響應(yīng)操作過(guò)程
在我們開(kāi)發(fā)過(guò)程中,我們經(jīng)常使用的參數(shù)絕大多少事HTML和JSON格式的請(qǐng)求和響應(yīng)處理,但是我們?cè)趯?shí)際開(kāi)發(fā)過(guò)程中,我們可能經(jīng)歷一些,比如對(duì)于XML格式的請(qǐng)求,本文給大家介紹Spring Boot拓展XML格式的請(qǐng)求和響應(yīng),感興趣的朋友一起看看吧2023-10-10spring boot配置MySQL數(shù)據(jù)庫(kù)連接、Hikari連接池和Mybatis的簡(jiǎn)單配置方法
這篇文章主要介紹了spring boot配置MySQL數(shù)據(jù)庫(kù)連接、Hikari連接池和Mybatis的簡(jiǎn)單配置方法,需要的朋友可以參考下2018-03-03SpringBoot整合RabbitMQ實(shí)現(xiàn)六種工作模式的示例
這篇文章主要介紹了SpringBoot整合RabbitMQ實(shí)現(xiàn)六種工作模式,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Java中關(guān)鍵字final finally finalize的區(qū)別介紹
這篇文章主要給大家分享的是 Java中final,finally,finalize 到底有什么區(qū)別,文章圍繞final,finally,finalize的相關(guān)資料展開(kāi)詳細(xì)內(nèi)容,具有一定的參考的價(jià)值,需要的朋友可以參考一下2022-04-04JAVA設(shè)計(jì)模式之訪問(wèn)者模式原理與用法詳解
這篇文章主要介紹了JAVA設(shè)計(jì)模式之訪問(wèn)者模式,簡(jiǎn)單說(shuō)明了訪問(wèn)者模式的原理,并結(jié)合實(shí)例分析了java訪問(wèn)者模式的定義與用法,需要的朋友可以參考下2017-08-08spring boot配置ssl實(shí)現(xiàn)HTTPS的方法
這篇文章主要介紹了spring boot配置ssl實(shí)現(xiàn)HTTPS的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03AsyncConfigurerSupport自定義異步線程池處理異常
這篇文章主要為大家介紹了AsyncConfigurerSupport自定義異步線程池處理異常詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06idea?maven依賴(lài)引入失效無(wú)法正常導(dǎo)入依賴(lài)問(wèn)題的解決方法
有時(shí)候idea導(dǎo)入一個(gè)新項(xiàng)目,或者pom文件修改(新增)了依賴(lài),pom文件和代碼會(huì)報(bào)紅,提示依賴(lài)包不存在,下面這篇文章主要給大家介紹了關(guān)于idea?maven依賴(lài)引入失效無(wú)法正常導(dǎo)入依賴(lài)問(wèn)題的解決方法,需要的朋友可以參考下2023-04-04