java
主页 > 软件编程 > java >

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

2022-08-21 | 佚名 | 点击:

最近在研究断点下载(下载续传)的功能,此功能需要服务端和客户端进行对接编写,本篇也是记录一下关于贴上关于实现服务端(Spring Boot)与客户端(Android)是如何实现下载续传功能

断点下载功能(下载续传)解释:

客户端由于突然性网络中断等原因,导致的下载失败,这个时候重新下载,可以继续从上次的地方进行下载,而不是重新下载

原理

首先,我们先说明了断点续传的功能,实际上的原理比较简单

客户端和服务端规定好一个规则,客户端传递一个参数,告知服务端需要数据从何处开始传输,服务端接收到参数进行处理,之后文件读写流从指定位置开始传输给客户端

实际上,上述的参数,在http协议中已经有规范,参数名为Range

而对于服务端来说,只要处理好Range请求头参数,即可实现下载续传的功能

我们来看下Range请求头数据格式如下:

格式如下:

1

2

3

4

5

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进行编写

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

/**

 * 断点下载文件

 *

 * @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的网络框架写的,需要先引用依赖

1

implementation 'com.squareup.okhttp3:okhttp:3.9.0'

下面给出的是封装好的方法(含进度,下载失败和成功回调):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

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);

    }

}

使用:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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
相关文章
最新更新