欢迎光临
我们一直在努力

java 实现断点续传服务

一:什么是断点续传

客户端软件断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载

(将文件分片以及后续合并是一个不小的工作量,由于项目时间有限,我并没有做分片,只是实现了可断点下载)

二:实现原理

2.1 实现思路

需要前端和后端的配合,前端在请求头中 标明 下载开始的位置,后端重标记位置开始向前端输出文件剩余部分。

在简单模式下,前端不需要知道文件大小,也不许要知道文件是否已经下载完毕。当文件可以正常打开时即文件下载完毕。(若想知道文件是否下载完毕,可写个接口比较Range 值与文件大小)

一般服务请求头

GET /down.zip HTTP/1.1 
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- 
excel, application/msword, application/vnd.ms-powerpoint, */* 
Accept-Language: zh-cn 
Accept-Encoding: gzip, deflate 
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) 
Connection: Keep-Alive

响应头

200 
Content-Length=106786028 
Accept-Ranges=bytes 
Date=Mon, 30 Apr 2001 12:56:11 GMT 
ETag=W/"02ca57e173c11:95b" 
Content-Type=application/octet-stream 
Server=Microsoft-IIS/5.0 
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT

如果要服务器支持断点续传功能的话,需要在请求头中表明文件开始下载的位置

请求头

GET /down.zip HTTP/1.0 
User-Agent: NetFox 
RANGE: bytes=2000070- #表示文件从2000070处开始下载
# Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2

响应头

206 
Content-Length=106786028 
Content-Range=bytes 2000070-106786027/106786028 
Date=Mon, 30 Apr 2001 12:55:20 GMT 
ETag=W/"02ca57e173c11:95b" 
Content-Type=application/octet-stream 
Server=Microsoft-IIS/5.0 
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT

三:java代码实现

3.1 BreakPoinService类

import org.springframework.stereotype.Service;

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

@Service
public class BreakPoinService {
    //断点续传
    public void downLoadByBreakpoint(File file, long start, long end, HttpServletResponse response){
        OutputStream stream = null;
        RandomAccessFile fif = null;
        try {
            if (end <= 0) {
                end = file.length() - 1;
            }
            stream = response.getOutputStream();
            response.reset();
            response.setStatus(206);
            response.setContentType("application/octet-stream");
            response.setHeader("Content-disposition", "attachment; filename=" + file.getName());
            response.setHeader("Content-Length", String.valueOf(end - start + 1));
            response.setHeader("file-size", String.valueOf(file.length()));
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("Content-Range", String.format("bytes %s-%s/%s", start, end, file.length()));
            fif = new RandomAccessFile(file, "r");
            fif.seek(start);
            long index = start;
            int d;
            byte[] buf = new byte[10240];
            while (index <= end && (d = fif.read(buf)) != -1) {
                if (index + d > end) {
                    d = (int)(end - index + 1);
                }
                index += d;
                stream.write(buf, 0, d);
            }
            stream.flush();
        } catch (Exception e) {
            try {
                if (stream != null)
                    stream.close();
                if (fif != null)
                    fif.close();
            } catch (Exception e11) {
            }
        }
    }

    //全量下载
    public void downLoadAll(File file, HttpServletResponse response){
        OutputStream stream = null;
        BufferedInputStream fif = null;
        try {
            stream = response.getOutputStream();
            response.reset();
            response.setContentType("application/octet-stream");
            response.setHeader("Content-disposition", "attachment; filename=" + file.getName());
            response.setHeader("Content-Length", String.valueOf(file.length()));
            fif = new BufferedInputStream(new FileInputStream(file));
            int d;
            byte[] buf = new byte[10240];
            while ((d = fif.read(buf)) != -1) {
                stream.write(buf, 0, d);
            }
            stream.flush();
        } catch (Exception e) {
            try {
                if (stream != null)
                    stream.close();
                if (fif != null)
                    fif.close();
            } catch (Exception e11) {
            }
        }
    }

}

3.2 断点续传控制类

import cn.ztuo.api.cos.QCloudStorageService;
import cn.ztuo.api.service.IBreakpointResumeService;
import cn.ztuo.api.service.impl.BreakPoinService;
import cn.ztuo.commons.annotation.PassToken;
import cn.ztuo.commons.response.CommonResult;
import cn.ztuo.mbg.entity.BreakpointResume;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**

* 断点续传控制类
*/
@RestController
@RequestMapping("/breakpoint")
public class BreakPointController {

    @Autowired
    private IBreakpointResumeService breakpointResumeService;
    @Autowired
    private BreakPoinService breakPoinService;

    @Autowired
    private QCloudStorageService storageService;

    @PassToken
    @GetMapping(value = "resource")
    public CommonResult download(HttpServletRequest request, HttpServletResponse response, @RequestParam("key") String key) {

        LambdaQueryWrapper<BreakpointResume> brWrapper=new LambdaQueryWrapper<>();
        brWrapper.eq(BreakpointResume::getCodKey,key);
        List<BreakpointResume> list = breakpointResumeService.list(brWrapper);
        String str=null;
        //如果本地存在取本地文件
        if(list.size()>0){
            BreakpointResume breakpointResume = list.get(0);
            str=breakpointResume.getFilePath();
        }else{//本地不存在
            try{
                String download = storageService.download(key);
                BreakpointResume breakpointResume=new BreakpointResume();
                breakpointResume.setCodKey(key);
                breakpointResume.setFilePath(download);
                breakpointResume.setCreateTime(new Date());
                breakpointResume.setUpdateTime(new Date());
                boolean save = breakpointResumeService.save(breakpointResume);
                if(save){
                    str=download;
                }else{
                    return CommonResult.error();
                }
            }catch (Exception e){
                return CommonResult.error();
            }
        }
        if(str==null){
            return CommonResult.error();
        }
        File file=new File(str);
        if (file.exists()) {
            String range = request.getHeader("Range");
            if (range != null && (range = range.trim()).length() > 0) {
                Pattern rangePattern = Pattern.compile("^bytes=([0-9]+)-([0-9]+)?$");
                Matcher matcher = rangePattern.matcher(range);
                if (matcher.find()) {
                    Integer start = Integer.valueOf(matcher.group(1));
                    Integer end = 0;
                    String endStr = matcher.group(2);
                    if (endStr != null && (endStr = endStr.trim()).length() > 0)
                        end = Integer.valueOf(endStr);
                    breakPoinService.downLoadByBreakpoint(file, start, end, response);
                    return null;
                }
            }
            breakPoinService.downLoadAll(file, response);
            return null;
        }
        return CommonResult.error();

    }
}

3.3 自定义全局响应类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    private String code;
    private String msg;
    private T data;

    public CommonResult(String code,String msg){
        this.code=code;
        this.msg=msg;
    }

    public static CommonResult success(){
        return create("200","成功");
    }
    public static <T> CommonResult success(T data){
        CommonResult result = create("200", "成功");
        result.setData(data);
        return result;
    }

    public static CommonResult error(){
        return create("500","服务器开小差了");
    }

    public static  CommonResult  create(String code,String msg){
        return new CommonResult(code,msg);
    }

}

另附一段

另外一文件抄过来的内容

//实现文件下载功能
public String downloadFile(){
    File dir = new File(filepath);//获取文件路劲
    if(!dir.exists()) {
        System.out.println("文件路径错误");
        log.debug("文件路径错误");
        return "failed";// 判断文件或文件夹是否存在
    }
    File downloadFile = new File(dir, filename);//在指定目录下查找文件
    if(!downloadFile.isFile()){
        System.out.println("文件不存在");
        log.debug("文件不存在");
        return "failed";// 判断文件或文件夹是否存在
    }
    try {
        downloadFileRanges(downloadFile);
    } catch(ClientAbortException e){
        System.out.println("连接被终止");
        log.debug("连接被终止");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

private void downloadFileRanges(File downloadFile) throws IOException {
    // 要下载的文件大小
    long fileLength = downloadFile.length();
    // 已下载的文件大小
    long pastLength = 0;
    // 是否快车下载,否则为迅雷或其他
    boolean isFlashGet = true;
    // 用于记录需要下载的结束字节数(迅雷或其他下载)
    long lenEnd = 0;
    // 用于记录客户端要求下载的数据范围字串
    String rangeBytes = request.getHeader("Range");
    //用于随机读取写入文件
    RandomAccessFile raf = null;
    OutputStream os = null;
    OutputStream outPut = null;
    byte b[] = new byte[1024];
    // 如果客户端下载请求中包含了范围
    if (null != rangeBytes)
    {
        // 返回码 206
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        rangeBytes = request.getHeader("Range").replaceAll("bytes=", "");
        // 判断 Range 字串模式
        if (rangeBytes.indexOf('-') == rangeBytes.length() - 1)
        {
            // 无结束字节数,为快车
            isFlashGet = true;
            rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
            pastLength = Long.parseLong(rangeBytes.trim());
        }
        else
        {
            // 迅雷下载
            isFlashGet = false;
            String startBytes = rangeBytes.substring(0,
                                                     rangeBytes.indexOf('-'));
            String endBytes = rangeBytes.substring(
                rangeBytes.indexOf('-') + 1, rangeBytes.length());
            // 已下载文件段
            pastLength = Long.parseLong(startBytes.trim());
            // 还需下载的文件字节数(从已下载文件段开始)
            lenEnd = Long.parseLong(endBytes);
        }
    }
    // 通知客户端允许断点续传,响应格式为:Accept-Ranges: bytes
    response.setHeader("Accept-Ranges", "bytes");
    // response.reset();
    // 如果为第一次下载,则状态默认为 200,响应格式为: HTTP/1.1 200 ok
    if (0 != pastLength)
    {
        // 内容范围字串
        String contentRange = "";
        // 响应格式
        // Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]||[文件的总大小]
        if (isFlashGet)
        {
            contentRange = new StringBuffer("bytes")
                .append(new Long(pastLength).toString()).append("-")
                .append(new Long(fileLength - 1).toString())
                .append("/").append(new Long(fileLength).toString())
                .toString();
        }
        else
        {
            contentRange = new StringBuffer(rangeBytes).append("/")
                .append(new Long(fileLength).toString()).toString();
        }
        response.setHeader("Content-Range", contentRange);
    }
    String fileName = getDownloadChineseFileName(filename);
    response.setHeader("Content-Disposition",
                       "attachment;filename=" + fileName + "");
    // 响应的格式是:
    response.setContentType("application/octet-stream");
    response.addHeader("Content-Length", String.valueOf(fileLength));
    try
    {
        os = response.getOutputStream();
        outPut = new BufferedOutputStream(os);
        raf = new RandomAccessFile(downloadFile, "r");
        // 跳过已下载字节
        raf.seek(pastLength);
        if (isFlashGet)
        {
            // 快车等
            int n = 0;
            while ((n = raf.read(b, 0, 1024)) != -1)
            {
                outPut.write(b, 0, n);
            }
        }
        else
        {
            // 迅雷等
            while (raf.getFilePointer() < lenEnd)
            {
                outPut.write(raf.read());
            }
        }
        outPut.flush();
    }
    catch (IOException e)
    {
    /**
    * 在写数据的时候 对于 ClientAbortException 之类的异常
    * 是因为客户端取消了下载,而服务器端继续向浏览器写入数据时, 抛出这个异常,这个是正常的。 尤其是对于迅雷这种吸血的客户端软件。
    * 明明已经有一个线程在读取 bytes=1275856879-1275877358,
    * 如果短时间内没有读取完毕,迅雷会再启第二个、第三个。。。线程来读取相同的字节段, 直到有一个线程读取完毕,迅雷会 KILL
    * 掉其他正在下载同一字节段的线程, 强行中止字节读出,造成服务器抛 ClientAbortException。
    * 所以,我们忽略这种异常
    */
    }
    finally
    {
        if(outPut != null)
        {
            outPut.close();
        }
        if(raf != null)
        {
            raf.close();
        }
    }
}



private String getDownloadChineseFileName(String paramName)
{
    String downloadChineseFileName = "";
    try
    {
        downloadChineseFileName = new String(paramName.getBytes("GBK"),
                                             "ISO8859-1");
    }
    catch (UnsupportedEncodingException e)
    {
        e.printStackTrace();
    }
    return downloadChineseFileName;
}



public String getFilepath() {
    return filepath;
}
public void setFilepath(String filepath) {
    this.filepath = filepath;
}
public String getFilename() {
    return filename;
}
public void setFilename(String filename) {
    this.filename = filename;
}
public HttpServletRequest getRequest() {
    return request;
}
public HttpServletResponse getResponse() {
    return response;
}

2. struts部分    

<action name="downloadFile" class="downloadFileAction" method="downloadFile">
   <result name="failed" type="redirectAction">showDownloadFileNameList</result>
</action>

3. jsp部分    

<td><a href="downloadFile?filename=${fileMap.key }&&filepath=${fileMap.value }">文件下载</a></td>
赞(0)
版权归原作者所有,如有侵权请告知。达维营-前端网 » java 实现断点续传服务

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址