ToB企服应用市场:ToB评测及商务社交产业平台

标题: Java 断点下载(下载续传)服务端及客户端(Android)代码 [打印本页]

作者: 天空闲话    时间: 2022-9-16 17:18
标题: Java 断点下载(下载续传)服务端及客户端(Android)代码
原文: Java 断点下载(下载续传)服务端及客户端(Android)代码 - Stars-One的杂货小窝
最近在研究断点下载(下载续传)的功能,此功能需要服务端和客户端进行对接编写,本篇也是记录一下关于贴上关于实现服务端(Spring Boot)与客户端(Android)是如何实现下载续传功能
断点下载功能(下载续传)解释:
客户端由于突然性网络中断等原因,导致的下载失败,这个时候重新下载,可以继续从上次的地方进行下载,而不是重新下载
原理

首先,我们先说明了断点续传的功能,实际上的原理比较简单
客户端和服务端规定好一个规则,客户端传递一个参数,告知服务端需要数据从何处开始传输,服务端接收到参数进行处理,之后文件读写流从指定位置开始传输给客户端
实际上,上述的参数,在http协议中已经有规范,参数名为Range
而对于服务端来说,只要处理好Range请求头参数,即可实现下载续传的功能
我们来看下Range请求头数据格式如下:
格式如下:
  1. Range:bytes=300-800
  2. //客户端需要文件300-800字节范围的数据(即500B数据)
  3. Range:bytes=300-
  4. //客户端需要文件300字节之后的数据
复制代码
我们根据上面的格式,服务端对Range字段进行处理(String字符串数据处理),在流中返回指定的数据大小即可
那么,如何让流返回指定的数据大小或从指定位置开始传输数据呢?
这里,Java提供了RandomAccessFile类,通过seekTo()方法,可以让我们将流设置从指定位置开始读取或写入数据
这里读取和写入数据,我是采用的Java7之后新增的NIO的Channel进行流的写入(当然,用传统的文件IO流(BIO)也可以)
这里,我所说的客户端是指的Android客户端,由于App开发也是基于Java,所以也是可以使用RandomAccessFile这个类
对于客户端来说,有以下逻辑:
先读取本地已下载文件的大小,然后请求下载数据将文件大小的数据作为请求头的数值传到服务端,之后也是利用RandomAccessFile移动到文件的指定位置开始写入数据即可
扩展-大文件快速下载思路

利用上面的思路,我们还可以可以得到一个大文件快速下载的思路:
如,一份文件,大小为2000B(这个大小可以通过网络请求,从返回数据的请求头content-length获取获取)
客户端拿回到文件的总大小,根据调优算法,将平分成合适的N份,通过线程池,来下载这个N个单文件
在下载完毕之后,将N个文件按照顺序合并成单个文件即可
代码

上面说明了具体的思路,那么下面就是贴出服务端和客户端的代码示例
服务端

服务端是采用的spring boot进行编写
  1. /**
  2. * 断点下载文件
  3. *
  4. * @return
  5. */
  6. @GetMapping("download")
  7. public void download( HttpServletRequest request, HttpServletResponse response) throws IOException {
  8.     //todo 这里文件按照你的需求调整
  9.     File file = new File("D:\\temp\\测试文件.zip");
  10.     if (!file.exists()) {
  11.         response.setStatus(HttpStatus.NOT_FOUND.value());
  12.         return;
  13.     }
  14.     long fromPos = 0;
  15.     long downloadSize = file.length();
  16.     if (request.getHeader("Range") != null) {
  17.         response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
  18.         String[] ary = request.getHeader("Range").replaceAll("bytes=", "").split("-");
  19.         fromPos = Long.parseLong(ary[0]);
  20.         downloadSize = (ary.length < 2 ? downloadSize : Long.parseLong(ary[1])) - fromPos;
  21.     }
  22.     //注意下面设置的相关请求头
  23.     response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
  24.     //相当于设置请求头content-length
  25.     response.setContentLengthLong(downloadSize);
  26.     //使用URLEncoder处理中文名(否则会出现乱码)
  27.     response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
  28.     response.setHeader("Accept-Ranges", "bytes");
  29.     response.setHeader("Content-Range", String.format("bytes %s-%s/%s", fromPos, (fromPos + downloadSize), downloadSize));
  30.     RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
  31.     randomAccessFile.seek(fromPos);
  32.     FileChannel inChannel = randomAccessFile.getChannel();
  33.     WritableByteChannel outChannel = Channels.newChannel(response.getOutputStream());
  34.     try {
  35.         while (downloadSize > 0) {
  36.             long count = inChannel.transferTo(fromPos, downloadSize, outChannel);
  37.             if (count > 0) {
  38.                 fromPos += count;
  39.                 downloadSize -= count;
  40.             }
  41.         }
  42.         inChannel.close();
  43.         outChannel.close();
  44.         randomAccessFile.close();
  45.     } catch (IOException e) {
  46.         e.printStackTrace();
  47.     }
  48. }
复制代码
客户端

Android客户端,是基于Okhttp的网络框架写的,需要先引用依赖
  1. implementation 'com.squareup.okhttp3:okhttp:3.9.0'
复制代码
下面给出的是封装好的方法(含进度,下载失败和成功回调):
  1. package com.tyky.update.utils;
  2. import com.blankj.utilcode.util.ThreadUtils;
  3. import java.io.File;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.io.RandomAccessFile;
  7. import java.math.BigDecimal;
  8. import java.nio.ByteBuffer;
  9. import java.nio.channels.Channels;
  10. import java.nio.channels.FileChannel;
  11. import java.nio.channels.ReadableByteChannel;
  12. import okhttp3.Call;
  13. import okhttp3.OkHttpClient;
  14. import okhttp3.Request;
  15. import okhttp3.Response;
  16. public class FileDownloadUtil {
  17.     public static void download(String url, File file, OnDownloadListener listener) {
  18.         //http://10.232.107.44:9060/swan-business/file/download
  19.         // 利用通道完成文件的复制(非直接缓冲区)
  20.         ThreadUtils.getIoPool().submit(new Runnable() {
  21.             @Override
  22.             public void run() {
  23.                 try {
  24.                     //续传开始的进度
  25.                     long startSize = 0;
  26.                     if (file.exists()) {
  27.                         startSize = file.length();
  28.                     }
  29.                     OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
  30.                     Request request = new Request.Builder().url(url)
  31.                             .addHeader("Range", "bytes=" + startSize)
  32.                             .get().build();
  33.                     Call call = okHttpClient.newCall(request);
  34.                     Response resp = call.execute();
  35.                     double length = Long.parseLong(resp.header("Content-Length")) * 1.0;
  36.                     InputStream fis = resp.body().byteStream();
  37.                     ReadableByteChannel fisChannel = Channels.newChannel(fis);
  38.                     RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
  39.                     //从上次未完成的位置开始下载
  40.                     randomAccessFile.seek(startSize);
  41.                     FileChannel foschannel = randomAccessFile.getChannel();
  42.                     // 通道没有办法传输数据,必须依赖缓冲区
  43.                     // 分配指定大小的缓冲区
  44.                     ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
  45.                     // 将通道中的数据存入缓冲区中
  46.                     while (fisChannel.read(byteBuffer) != -1) {  // fisChannel 中的数据读到 byteBuffer 缓冲区中
  47.                         byteBuffer.flip();  // 切换成读数据模式
  48.                         // 将缓冲区中的数据写入通道
  49.                         foschannel.write(byteBuffer);
  50.                         final double progress = (foschannel.size() / length);
  51.                         BigDecimal two = new BigDecimal(progress);
  52.                         double result = two.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
  53.                         //计算进度,回调
  54.                         if (listener != null) {
  55.                             listener.onProgress(result);
  56.                         }
  57.                         byteBuffer.clear();  // 清空缓冲区
  58.                     }
  59.                     foschannel.close();
  60.                     fisChannel.close();
  61.                     randomAccessFile.close();
  62.                     if (listener != null) {
  63.                         listener.onSuccess(file);
  64.                     }
  65.                 } catch (IOException e) {
  66.                     if (listener != null) {
  67.                         listener.onError(e);
  68.                     }
  69.                 }
  70.             }
  71.         });
  72.     }
  73.     public interface OnDownloadListener {
  74.         void onProgress(double progress);
  75.         void onError(Exception e);
  76.         void onSuccess(File outputFile);
  77.     }
  78. }
复制代码
使用:
  1. FileDownloadUtil.download(downloadUrl, file, new FileDownloadUtil.OnDownloadListener() {
  2.     @Override
  3.     public void onProgress(double progress) {
  4.         KLog.d("下载进度: " + progress);
  5.     }
  6.     @Override
  7.     public void onError(Exception e) {
  8.         KLog.e("下载错误: " + e.getMessage());
  9.     }
  10.     @Override
  11.     public void onSuccess(File outputFile) {
  12.         KLog.d("下载成功");
  13.     }
  14. });
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4