JAVA实现http-http-ftp的断点续传功能

背景

最近项目有一个需求,就是客户端通过http方式访问http服务器(tomcat)进行下载,然后tomcat通过ftp方式进行下载原文件,然后在tomcat中进行文件流的转发,实现客户端的断点续传功能
整体架构图如下在这里插入图片描述
这里我们假设客户端想要下载的文件叫做:下载测试.zip
在这里主要分为下面这几个步骤:
1:客户端A 通过 http方式访问服务端(tomcat),请求下载文件 下载测试.zip;
2 : 服务端接收A的请求进行解析 ,获取到想要下载的文件分片 下载的文件名称
3: 服务器内部调用 ftp方式下载文件 ,通过B中接受到的 分片起始位置进行 ftp方式断点续传。
4 :服务端将通过ftp方式的下载的文件流分片转发到客户机,实现整体功能上的断点续传。

这里 总体需要实现两个断点续传:
1 客户端到服务端的HTTP方式的断点续传
2 服务端到FTP服务器的断点续传

整体构建:

首先我们把项目的框架搭起来:
1 客户端 普通的java方式 访问http
2 服务端 通过SpringBoot实现http服务器转发的效果
3 ftp服务器 通过windows自带的功能进行组建ftp服务器

功能代码与构思

ftp的断点续传主要是使用的

ftpclient使用的是org.apache.commons.net.ftp.FTPClient 进行连接ftp服务器
    public class FTPConnect {
    /**
     *  方法仅仅传入一个文件的节点游标 然后再使用ftpClient进行			setRestartOffset(fileChipCursor); 从游标这里开始读取   ftpclient使用的是 org
     * @return 上传的状态
     * @throws IOException
     */
    public  InputStream getInputStream(long fileChipCursor) throws IOException {
        //设置被动模式
        ftpClient.enterLocalPassiveMode();
        ftpClient.changeWorkingDirectory(new String(getRemotePath().getBytes("GBK"),"ISO-8859-1"));
        ftpClient.setRestartOffset(fileChipCursor);
        return ftpClient.retrieveFileStream(new String(getResource().getBytes("GBK"),"iso-8859-1"));
    }


需要获取到远程的文件路径进行判断
 public  long getRemoteFileSize() throws IOException {
        //检查远程文件是否存在 并设置编码格式 防止中文名称的 文件下载为空
        FTPFile[] files = ftpClient.listFiles(new String(getRemotePath().getBytes("GBK"),"iso-8859-1"));
        long lRemoteSize=0L;
        if (files.length>0){
            for (FTPFile ff:files){
                String suffix = ff.getName().substring(ff.getName().lastIndexOf(".") + 1);
                if (suffix.equals("zip")){
                    lRemoteSize=ff.getSize();
                }
            }
        }

        return lRemoteSize;
    }

获取到远程的路径
  private String getRemotePath(){
  		//这里是相对于ftp服务器的相对路径
        String remotePath = "/"+Namepath**+"/"+path**+"/";
        return remotePath;
    }
    }

ftp连接工具类

package ****;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPReply;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.SocketException;
/**
 * @Description ftp文件传输工具类
 * @Auther zhangjun
 * @Data 2020/4/27 10:38
 **/
public class FtpClientUtil {
    //账号
    private static String username = "adminftp";
    //密码
    private static String password = "123456";
    //默认地址
    private static String ip = "172.16.15.250";
    //默认端口号
    private static String port = "21";
    /**
     * ftp链接
     * @throws IOException
     */
    public static FTPClient ftpConnection() throws IOException {
        FTPClient ftpClient = new FTPClient();
        ftpClient.setConnectTimeout(0);
        try {
            ftpClient.connect(ip, Integer.parseInt(port));
            ftpClient.login(username, password);
            int replyCode = ftpClient.getReplyCode(); //是否成功登录服务器
            if(!FTPReply.isPositiveCompletion(replyCode)) {
                ftpClient.disconnect();
                System.exit(1);
            }
            //告诉对面服务器开一个端口
            ftpClient.enterLocalPassiveMode();
            ftpClient.setControlEncoding("GBK");
            FTPClientConfig conf = new FTPClientConfig(FTPClientConfig.SYST_NT);
            conf.setServerLanguageCode("zh");
            ftpClient.configure(conf);
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ftpClient;
    }
 
    /**
     * 断开FTP连接
     * @param ftpClient  初始化的对象
     * @throws IOException
     */
    public static void close(FTPClient ftpClient) throws IOException{
        if(ftpClient!=null && ftpClient.isConnected()){
            ftpClient.logout();
            ftpClient.disconnect();
        }
    }
  
}

SpringBoot的http服务端代码

这里的服务端对应的就是springBoot的下载功能 因为可以端对服务端就是下载请求,只是服务端不像是以前读取本地文件发送给 客户端,这次是转发 ftp目录上面的文件流给客户端。
代码实现:

/**
 * Project Name: ***
 * File Name: httpController
 * Package Name:****
 * Date: 2020/5/12 14:16
 * Copyright (c) 2020,All Rights Reserved.
 */
package ***;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Controller;
import org.json.JSONException;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * @Author: zhangjun
 * @Description:
 * @Date: Create in 14:16 2020/5/12 
 */
@Controller
public class httpController {

    @RequestMapping(value = "/download/{path1}/{path2}", method = RequestMethod.GET)
    public void download(@PathVariable("path1") String path1,@PathVariable("path2") String path2, HttpServletRequest request, HttpServletResponse response) {
        try{
            //EasyConnect6301.zip FTPConnect 里面自己创建两个变量作为path1 与 path2 我这里使用的 productname与version 
            FTPConnect Ftp = new FTPConnect();
            Ftp.setproduceName(path1);
            Ftp.setProductversion(path2);
            //从request中获取 range 获取客户端请求的断点信息
            String headerInfo = request.getHeader("Range");
            if (headerInfo != null) {
                response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
            }
            // 表示下载范围的pojo
            ResponseContentRange range = getRange(Ftp.getRemoteFileSize(), headerInfo);
            String fileName = "EasyConnect6301.zip";
            response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
            if (request.getHeader(HttpHeaders.USER_AGENT).contains("MSIE")) {
                fileName = URLEncoder.encode(fileName, "UTF-8");
            } else {
                fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
            }
            response.setContentType("application/octet-stream");
            response.setContentLengthLong(Ftp.getRemoteFileSize());
            response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setHeader("Content-Range", "bytes " + range.getStartIndex() + "-" + (range.getStartIndex()
                    + range.getContentSize() - 1) + "/" + Ftp.getRemoteFileSize());
            byte[] buffer = new byte[1024*1024];
            int n;
            int writeCount = 0;
            OutputStream outputStream = response.getOutputStream();
            //**Ftp.getInputStream((int) range.getStartIndex());**这段代码是这个服务端的核心
            将ftp的文件流直接写入response中。做到中转。
            InputStream in =**Ftp.getInputStream((int) range.getStartIndex());**
            while ((n = in.read(buffer))!= -1 && writeCount < range.getContentSize()) {
                    outputStream.write(buffer, 0, n);
                     writeCount += n;
            }
            in.close();
           
            Ftp.getFtpClient().completePendingCommand();
            Ftp.getFtpClient().logout();
            Ftp.getFtpClient().disconnect();
            //使用完了一定要关闭ftp链接,不然ftp服务占用过高,后面下载特别卡顿
        } catch (IOException ignore) {
        }
    }
    /**
     * 根据给定的rangeInfo,解析出回复的内容的范围
     *
     * @param maxSize   范围的最大值
     * @param rangeInfo rangeInfo
     * @return
     */
    private ResponseContentRange getRange(long maxSize, String rangeInfo) {
        long startIndex = 0L, contentLength = maxSize;
        if (rangeInfo != null && rangeInfo.trim().length() > 0) {
            String rangBytes = rangeInfo.replaceAll("bytes=", "");
            if (rangBytes.endsWith("-")) {
                startIndex = Long.parseLong(rangBytes.substring(0, rangBytes.indexOf("-")));
                contentLength = maxSize - startIndex;
            } else if (rangBytes.startsWith("-")) {
                startIndex = Long.parseLong(rangBytes.substring(rangBytes.indexOf("-") + 1));
                contentLength = maxSize - startIndex;
            } else {
                String[] indexs = rangBytes.split("-");
                startIndex = Long.parseLong(indexs[0]);
                contentLength = Long.parseLong(indexs[1]) - startIndex + 1;
            }
        }
        return new ResponseContentRange(startIndex, contentLength);
    }
    class ResponseContentRange {
        private long startIndex;
        private long ContentSize;

        public long getStartIndex() {
            return startIndex;
        }
        ResponseContentRange(long startIndex,long contentLength){
            this.ContentSize=contentLength;
            this.startIndex=startIndex;
        }
        public void setStartIndex(long startIndex) {
            this.startIndex = startIndex;
        }
        public long getContentSize() {
            return ContentSize;
        }
        public void setContentSize(long contentLength) {
            this.ContentSize = contentLength;
        }
    }
}

客户端代码构建

客户端就是使用一个简单的java.net自带的工具,实现url链接,注意,下载方法是使用的get方式 ,我本来尝试用post方式,但是实在是失败了。但是使用get方式 我的功能代码还是测试通过了,只是说传递url参数的时候需要对中文进行转码。

package com.supermap.digicty.sdm;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: zhangjun
 * @Description:
 * @Date: Create in 16:14 2020/4/29
 * 根据输入的url和设定的线程数,来完成断点续传功能。
 * 每个线程支负责某一小段的数据下载;再通过RandomAccessFile完成数据的整合。
 */
public class MultiTheradDownLoad {
    private String urlStr = null;
    private String filename = null;
    private String tmpfilename = null;
    private int threadNum = 0;
    private CountDownLatch latch = null;//设置一个计数器,代码内主要用来完成对缓存文件的删除
    private long fileLength = 0l;
    private long threadLength = 0l;
    private long[] startPos;//保留每个线程下载数据的起始位置。
    private long[] endPos;//保留每个线程下载数据的截止位置。
    private boolean bool = false;
    private URL url = null;

    //有参构造函数,先构造需要的数据
    public MultiTheradDownLoad(String urlStr, int threadNum) {
        this.urlStr = urlStr;
        this.threadNum = threadNum;
        startPos = new long[this.threadNum];
        endPos = new long[this.threadNum];
        latch = new CountDownLatch(this.threadNum);
    }

    /*
     * 组织断点续传功能的方法
     */
    public void downloadPart() {
        File file = null;
        File tmpfile = null;
        //设置HTTP网络访问代理
        //从文件链接中获取文件名,此处没考虑文件名为空的情况,此种情况可能需使用UUID来生成一个唯一数来代表文件名。
        //改成从request中获取文件名
        try {
            //创建url
            url = new URL(urlStr);
            //打开下载链接,并且得到一个HttpURLConnection的一个对象httpcon
            HttpURLConnection httpcon = (HttpURLConnection) url.openConnection();
            httpcon.setRequestMethod("GET");
            //文件的实际名称与大小
            filename =  "E:\\a整体功能测试\\断点下载测试\\ASASA\\DM.zip";
            tmpfilename = filename + "_tmp";
            fileLength = httpcon.getContentLengthLong();
            //下载文件和临时文件
            file = new File(filename);//相对目录
            tmpfile = new File(tmpfilename);
            //每个线程需下载的资源大小;由于文件大小不确定,为避免数据丢失
            threadLength = fileLength%threadNum == 0 ? fileLength/threadNum : fileLength/threadNum+1;
            //打印下载信息
            System.out.println("80 文件名称: " + filename + " ," + "文件长度= "
                    + fileLength + "  线程的每个文件的大小= " + threadLength);

            //各个线程在exec线程池中进行,起始位置--结束位置
            if (file.exists()&& file.length() == fileLength) {
                System.out.println("85 文件已存在!!");
                return;
            } else {
                setBreakPoint(startPos, endPos, tmpfile);
                ExecutorService exec = Executors.newCachedThreadPool();
                for (int i = 0; i < threadNum; i++) {
                    exec.execute(new DownLoadThread(startPos[i], endPos[i],
                            this, i, tmpfile, latch));
                }
                latch.await();//当你的计数器减为0之前,会在此处一直阻塞。
                exec.shutdown();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //下载完成后,判断文件是否完整,并删除临时文件
        if (file.length() == fileLength) {
            if (tmpfile.exists()) {
                System.out.println("删除临时文件!!");
                tmpfile.delete();
            }
        }
    }

    /*
     * 断点设置方法,当有临时文件时,直接在临时文件中读取上次下载中断时的断点位置。没有临时文件,即第一次下载时,重新设置断点
     * Rantmpfile.seek()跳转到一个位置的目的是为了让各个断点存储的位置尽量分开。
     * 这是实现断点续传的重要基础。
     */
    private void setBreakPoint(long[] startPos, long[] endPos, File tmpfile) {
        RandomAccessFile rantmpfile = null;
        try {
            if (tmpfile.exists()) {
                System.out.println("继续下载!!");
                rantmpfile = new RandomAccessFile(tmpfile, "rw");
                for (int i = 0; i < threadNum; i++) {
                    rantmpfile.seek(8 * i + 8);
                    startPos[i] = rantmpfile.readLong();
                    rantmpfile.seek(8 * (i + 1000) + 16);
                    endPos[i] = rantmpfile.readLong();
                    System.out.println("线程的每个分片的大小: ");
                    System.out.println("thre 线程   " + (i + 1) + "  开始节点:"
                            + startPos[i] + ", 结束节点: " + endPos[i]);
                }
            } else {
                System.out.println(" 149 没有临时文件 重新开始下载 ");
                rantmpfile = new RandomAccessFile(tmpfile, "rw");
                //最后一个线程的截止位置大小为请求资源的大小
                for (int i = 0; i < threadNum; i++) {
                    startPos[i] = threadLength * i;
                    if (i == threadNum - 1) {
                        endPos[i] = fileLength;
                    } else {
                        endPos[i] = threadLength * (i + 1) - 1;
                    }

                    rantmpfile.seek(8 * i + 8);
                    rantmpfile.writeLong(startPos[i]);

                    rantmpfile.seek(8 * (i + 1000) + 16);
                    rantmpfile.writeLong(endPos[i]);

                    System.out.println("重新开始下载的文件分片:");
                    System.out.println(" 线程   " + (i + 1) + " 开始节点: "
                            + startPos[i] + ", 结束节点  : " + endPos[i]);
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rantmpfile != null) {
                    rantmpfile.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /*
     * 实现下载功能的内部类,通过读取断点来设置向服务器请求的数据区间。
     */
    class DownLoadThread implements Runnable {

        private long startPos;
        private long endPos;
        private MultiTheradDownLoad task = null;
        private RandomAccessFile downloadfile = null;
        private int id;
        private File tmpfile = null;
        private RandomAccessFile rantmpfile = null;
        private CountDownLatch latch = null;

        public DownLoadThread(long startPos, long endPos,
                              MultiTheradDownLoad task, int id, File tmpfile,
                              CountDownLatch latch) {
            this.startPos = startPos;
            this.endPos = endPos;
            this.task = task;
            this.tmpfile = tmpfile;
            try {
                this.downloadfile = new RandomAccessFile(this.task.filename,"rw");
                this.rantmpfile = new RandomAccessFile(this.tmpfile, "rw");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            this.id = id;
            this.latch = latch;
        }

        @Override
        public void run() {

            HttpURLConnection httpcon = null;
            InputStream is = null;
            int length = 0;
            System.out.println("线程" + id + " 开始下载!!");
            while (true) {
                try {
                    httpcon = (HttpURLConnection) task.url.openConnection();
                    httpcon.setRequestMethod("GET");
                    //防止网络阻塞,设置指定的超时时间;单位都是ms。超过指定时间,就会抛出异常
                     httpcon.setReadTimeout(200000000);//读取数据的超时设置
                     httpcon.setConnectTimeout(200000000);//连接的超时设置
                    if (startPos < endPos) {
                        //向服务器请求指定区间段的数据,这是实现断点续传的根本。
                        httpcon.setRequestProperty("Range", "bytes=" + startPos+ "-" + endPos);
                        System.out.println("线程 " + id+ " 长度:---- "+ (endPos - startPos));
                        downloadfile.seek(startPos);
                        if (httpcon.getResponseCode() != HttpURLConnection.HTTP_OK
                                && httpcon.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
                            this.task.bool = true;
                            httpcon.disconnect();
                            downloadfile.close();
                            System.out.println("线程 ---" + id + " 下载完成!!");
                            latch.countDown();//计数器自减
                            break;
                        }
                        is = httpcon.getInputStream();//获取服务器返回的资源流
                        long count = 0L;
                        byte[] buf = new byte[1024*1024*2];
                        while (!this.task.bool && (length = is.read(buf)) != -1) {
                            count += length;
                            downloadfile.write(buf, 0, length);
                            //不断更新每个线程下载资源的起始位置,并写入临时文件;为断点续传做准备
                            startPos += length;
                            System.out.println("229   线程本次写入 "+id+"==="+length);
                            rantmpfile.seek(8 * id + 8);
                            rantmpfile.writeLong(startPos);
                        }
                        System.out.println("线程 " + id
                                + " 总下载大小: " + count);

                        //关闭流
                        is.close();
                        httpcon.disconnect();
                        downloadfile.close();
                        rantmpfile.close();
                    }
                    latch.countDown();//计数器自减
                    System.out.println("线程 " + id + " 下载完成!!");
                    break;
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (is != null) {
                            is.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args) throws UnsupportedEncodingException {
        int threadNum = 1;
       //因为使用简单URL方式,所以必须转码服务端才能识别
        String AA= URLEncoder.encode("中文path1","UTF-8");
        String filepath = "http://172.16.15.250:8080/ftp/downFile/"+AA+"/path2";
        System.out.println(AA);
        MultiTheradDownLoad load = new MultiTheradDownLoad(filepath ,threadNum);
        load.downloadPart();
    }
}

总结

1 经过测试,该功能可以实现 文件从ftp 到 http的转发功能 并且在断电断网环境下都可以进行续传下载。并且文件不会丢失,不会损坏。 并且下载速度基本与ftp直连的速度一样 ,下载3G文件只需要 2分钟 。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
点量HttpFTP多线程断点续传下载组件(下载DLL)的开发目的是让用户可以无需关心Http/FTP协议的具体细节,只需要几十行甚至几行代码,便可以实现一个功能完善的Http/FTP下载软件。点量Http/FTP下载组件(DLL)支持多线程、断点续传、显示详细下载过程、自动查找镜像网址、支持代理传输等完善的功能。 点量HttpFTP下载内核源码使用高效的c++代码编写,提供标准的动态链接库(DLL),可供C/C++、Delphi、C#、Java、VB等语言和各常用开发环境调用,让您完全像调用系统API一样的调用。 点量Http/FTP组件的功能简介: 标准HttpFTP下载支持:完全标准的HttpFTP协议支持,内部通过网址自动区分是Http还是FTP下载。 极速下载(2.0以后版本):超越国内绝大多数下载软件的下载速度。新版内核在2M ADSL的环境下下载,有些文件的速度可以达到1400K字节/秒以上,超过了带宽的极限。下载速度可以用极速形容。 多线程传输:可以将文件自动分块,并采用多线程下载。并可自由设置线程数目。 断点续传:点量Http/FTP有优秀的断点续传支持,每次启动自动从上次下载的位置开始,不需要重复下载。 提供详细的下载详情接口(2.0以后版本):可以看到整个下载过程的步骤,比如开启了多少线程、服务器的应答过程、错误信息等。 支持多种高级设置:设置线程数目、磁盘缓存大小、搜索镜像服务器的详细参数设置、下载文件完成后同步文件为服务器上的文件时间、下载过程中可以自定义文件临时后缀、未完成的文件设为隐藏属性。 支持磁盘缓存:点量Http/FTP下载DLL支持设置磁盘缓存,减少对磁盘的读写,并提升下载速度。 支持设置Refer:点量Http/FTP下载组件支持设置下载时的Refer,以便可以绕过一些防盗链的网站,直接下载内容。 限速功能:点量Http/FTP下载组件可方便的设置下载限速。 多种磁盘分配方式:点量Http/FTP下载组件支持预分配和边下载边分配两种磁盘方式,满足多种下载需求。 自动搜索镜像加速:点量Http/FTP内置了镜像搜索功能,在下载文件的同时,会自动搜索哪些其它网站还有这个文件,自动从其它网址下载加速。 可提供源码:支付一定的费用,便可以获得全部的点量Http/FTP下载组件的源代码,免除您的所有后顾之忧。 良好的服务:作为点量软件旗下的软件,可享受到点量软件的优秀服务,我们的服务让您如同拥有一个称心的专业员工。 点量Http/FTP 下载组件可以适用于任何HttpFTP下载的领域,让您可以在1天内完成一个完整的Http下载软件的全部功能。比如,您可以用于产品的升级、文件的下载和传输等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值