原文: Java 斷點下載(下載續傳)服務端及客戶端(Android)代碼 - Stars-One的雜貨小窩 最近在研究斷點下載(下載續傳)的功能,此功能需要服務端和客戶端進行對接編寫,本篇也是記錄一下關於貼上關於實現服務端(Spring Boot)與客戶端(Android)是如何實現下載續傳功能 ...
最近在研究斷點下載(下載續傳)的功能,此功能需要服務端和客戶端進行對接編寫,本篇也是記錄一下關於貼上關於實現服務端(Spring Boot)與客戶端(Android)是如何實現下載續傳功能
斷點下載功能(下載續傳)解釋:
客戶端由於突然性網路中斷等原因,導致的下載失敗,這個時候重新下載,可以繼續從上次的地方進行下載,而不是重新下載
原理
首先,我們先說明瞭斷點續傳的功能,實際上的原理比較簡單
客戶端和服務端規定好一個規則,客戶端傳遞一個參數,告知服務端需要數據從何處開始傳輸,服務端接收到參數進行處理,之後文件讀寫流從指定位置開始傳輸給客戶端
實際上,上述的參數,在http協議中已經有規範,參數名為Range
而對於服務端來說,只要處理好Range請求頭參數,即可實現下載續傳的功能
我們來看下Range
請求頭數據格式如下:
格式如下:
Range:bytes=300-800
//客戶端需要文件300-800位元組範圍的數據(即500B數據)
Range:bytes=300-
//客戶端需要文件300位元組之後的數據
我們根據上面的格式,服務端對Range
欄位進行處理(String字元串數據處理),在流中返回指定的數據大小即可
那麼,如何讓流返回指定的數據大小或從指定位置開始傳輸數據呢?
這裡,Java提供了RandomAccessFile
類,通過seekTo()
方法,可以讓我們將流設置從指定位置開始讀取或寫入數據
這裡讀取和寫入數據,我是採用的Java7之後新增的NIO的Channel進行流的寫入(當然,用傳統的文件IO流(BIO)也可以)
這裡,我所說的客戶端是指的Android客戶端,由於App開發也是基於Java,所以也是可以使用RandomAccessFile
這個類
對於客戶端來說,有以下邏輯:
先讀取本地已下載文件的大小,然後請求下載數據將文件大小的數據作為請求頭的數值傳到服務端,之後也是利用
RandomAccessFile
移動到文件的指定位置開始寫入數據即可
擴展-大文件快速下載思路
利用上面的思路,我們還可以可以得到一個大文件快速下載的思路:
如,一份文件,大小為2000B(這個大小可以通過網路請求,從返回數據的請求頭content-length獲取
獲取)
客戶端拿回到文件的總大小,根據調優演算法,將平分成合適的N份,通過線程池,來下載這個N個單文件
在下載完畢之後,將N個文件按照順序合併成單個文件即可
代碼
上面說明瞭具體的思路,那麼下麵就是貼出服務端和客戶端的代碼示例
服務端
服務端是採用的spring boot進行編寫
/**
* 斷點下載文件
*
* @return
*/
@GetMapping("download")
public void download( HttpServletRequest request, HttpServletResponse response) throws IOException {
//todo 這裡文件按照你的需求調整
File file = new File("D:\\temp\\測試文件.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;
}
//註意下麵設置的相關請求頭
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
//相當於設置請求頭content-length
response.setContentLengthLong(downloadSize);
//使用URLEncoder處理中文名(否則會出現亂碼)
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的網路框架寫的,需要先引用依賴
implementation 'com.squareup.okhttp3:okhttp:3.9.0'
下麵給出的是封裝好的方法(含進度,下載失敗和成功回調):
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
// 利用通道完成文件的複製(非直接緩衝區)
ThreadUtils.getIoPool().submit(new Runnable() {
@Override
public void run() {
try {
//續傳開始的進度
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");
//從上次未完成的位置開始下載
randomAccessFile.seek(startSize);
FileChannel foschannel = randomAccessFile.getChannel();
// 通道沒有辦法傳輸數據,必須依賴緩衝區
// 分配指定大小的緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 將通道中的數據存入緩衝區中
while (fisChannel.read(byteBuffer) != -1) { // fisChannel 中的數據讀到 byteBuffer 緩衝區中
byteBuffer.flip(); // 切換成讀數據模式
// 將緩衝區中的數據寫入通道
foschannel.write(byteBuffer);
final double progress = (foschannel.size() / length);
BigDecimal two = new BigDecimal(progress);
double result = two.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
//計算進度,回調
if (listener != null) {
listener.onProgress(result);
}
byteBuffer.clear(); // 清空緩衝區
}
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("下載進度: " + progress);
}
@Override
public void onError(Exception e) {
KLog.e("下載錯誤: " + e.getMessage());
}
@Override
public void onSuccess(File outputFile) {
KLog.d("下載成功");
}
});
提問之前,請先看提問須知 點擊右側圖標發起提問 或者加入QQ群一起學習 TornadoFx學習交流群:1071184701