Java实现断点下载服务端与客户端的示例代码

作者:Stars-one 时间:2022-07-25 14:16:17 

最近在研究断点下载(下载续传)的功能,此功能需要服务端和客户端进行对接编写,本篇也是记录一下关于贴上关于实现服务端(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("下载成功");
   }
});

来源:https://www.cnblogs.com/stars-one/p/16592706.html

标签:Java,断点,下载
0
投稿

猜你喜欢

  • 详解Java类库的概念以及import的使用方法

    2022-04-18 06:56:54
  • Android开发之DrawerLayout实现抽屉效果

    2023-09-30 03:40:59
  • C# 邮件发送和接收实现代码

    2021-11-23 03:32:36
  • 使用JPA双向多对多关联关系@ManyToMany

    2021-12-04 03:37:12
  • 基于Android中实现定时器的3种解决方法

    2022-02-10 15:03:53
  • 在Android项目中使用AspectJ的方法

    2023-02-01 23:33:54
  • Java并发编程之阻塞队列详解

    2022-06-20 02:08:19
  • Spring @Profile注解详解

    2023-04-20 06:26:16
  • Java8新特性:函数式编程

    2021-12-01 03:09:02
  • Spring JPA联表查询之OneToOne源码详解

    2022-08-06 13:57:55
  • Spring Boot整合Swagger测试api构建全纪录

    2022-10-21 09:05:25
  • 浅谈Spring与SpringMVC父子容器的关系与初始化

    2023-02-08 12:09:05
  • C# 实现颜色渐变窗体控件详细讲解

    2021-12-31 07:10:28
  • Java基于Tcp的基础聊天功能实例

    2023-11-25 05:26:56
  • springboot使用单元测试实战

    2023-05-17 11:55:29
  • C#使用linq语句查询数组中以特定字符开头元素的方法

    2022-09-22 06:39:07
  • C#在Winform开发中使用Grid++报表

    2022-04-20 03:57:13
  • 深入理解Java中观察者模式与委托的对比

    2023-10-30 18:00:03
  • C#导出数据到CSV文件的通用类实例

    2023-05-22 09:23:16
  • Java中Range函数的简单介绍

    2023-10-18 05:33:35
  • asp之家 软件编程 m.aspxhome.com