Java断点续传
说明:本文章是在Springmvc的基础上完成的(有问题可以参考之前文章Springmvc框架搭建),主要功能是断点续传,同时下载时如果是图片直接展示。
使用场景:
①在浏览器下载的时候可以暂停下载后继续下载。
②在下载某一软件时,电脑突然断网,如果是一般下载继续下载会抛出异常,如果是断点续传会继续下载。这两点是断点续传的主要表现。
下载流程:
代码实现描述:
a.文件第一次下载,下载成功,文件保存。
b.文件第一次下载,中间断网暂停,联网成功调用地址,读取断点,根据文件断点继续下载,下载成功,文件保存。
准备内容:
a.我的文件下载目录:
b.我的访问路径 localhost:8080/forAll/fileDownload?filename=2.zip&actiontype=download
filename是要下载的文件的名称。
actiontype:是打开文件的方式,如果download是下载,show是图片展示。
c.我的代码层次分文3个层次 Controller Service ServiceImpl Utils
Controller 层
package com.zpkj.space.controller; import com.zpkj.space.service.UpLoadService; import com.zpkj.space.utils.UploadAllUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.apache.commons.lang3.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; /** * Created by 李庆伟 on */ @Controller @RequestMapping("forAll") public class UpLoadController { @Autowired private UpLoadService upLoadService; private String result = ""; private static String file_path = "E:\\Linux镜像及虚拟机"; private String page_path = "error/downLoadError"; /** * 断点续传 * @param request * @param response * @return * @throws IOException */ @RequestMapping("/fileDownload") public String fileDownload(HttpServletRequest request, HttpServletResponse response) throws IOException { StringBuffer sb = new StringBuffer(); try { // 下载本地文件 String actiontype = request.getParameter("actiontype") == null ? "show" : request.getParameter("actiontype"); if ("show".equals(actiontype)) { fileShow(request, response); return null; } sb.append("\n --下载文件信息开始--"); String filename = request.getParameter("filename") == null ? "" : request.getParameter("filename"); request.setAttribute("filename", filename); sb.append("\n actiontype=" + actiontype + " filename=" + filename); // 参数名称是否可用和打印地址 String message = upLoadService.isAvailableFilename(request, filename); if (!"".equals(message)) { sb.append("\n message=" + message); request.setAttribute("message", message); return null; } sb.append("\n\r每次用于匹配文件的file_path"+file_path+"===========文件名:"+filename); // 文件服务器地址 String path = readfile(file_path, filename); sb.append("\n\r调用readfile方法后的文件名:"+filename); sb.append("\n 服务器文件路径:" + path); File downloadFile = new File(path); // 文件是否可用 message = UploadAllUtils.isAvailableFile(downloadFile); if (!"".equals(message)) { sb.append("\n message=" + message); request.setAttribute("message", message); return null; } String requestLongth = request.getHeader("RequestLongth"); if(StringUtils.isNotBlank(requestLongth)&&requestLongth.trim().equals("1")){ response.setHeader("Content-Length",String.valueOf(downloadFile.length())); return null; } // 下载成功否 message = upLoadService.isdownLoad(request, response, downloadFile, sb); if (!"".equals(message)) { System.out.println(message); sb.append("\n message=" + message); request.setAttribute("message", message); return null; } sb.append("\n--下载文件信息结束--"); } catch (Exception e) { request.setAttribute("message", "文件异常!" + e.getMessage()); return null; }finally { System.out.println(sb.toString()); } return null; } /** * 图片展示 * * @param request * @param response * @return * @throws IOException */ @RequestMapping("/fileShow") public String fileShow(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // 动作类型 String actiontype = request.getParameter("actiontype") == null ? "show" : request.getParameter("actiontype"); if ("download".equals(actiontype)) { fileDownload(request, response); return null; } // 本地文件 String filename = request.getParameter("filename") == null ? "" : request.getParameter("filename"); request.setAttribute("filename", filename); // 参数名称是否可用和打印地址 String message = upLoadService.isAvailableFilename(request, filename); if (!"".equals(message)) { request.setAttribute("message", message); return page_path; } // 文件服务器地址 // String path = file_path + "" + filename; // String path = // "E:/Install/" // + name; String path = readfile(file_path, filename); System.out.println("服务器文件路径:" + path); File showFile = new File(path); // 文件是否可用 message = UploadAllUtils.isAvailableFile(showFile); if (!"".equals(message)) { request.setAttribute("message", message); return page_path; } // 是否是图片 message = UploadAllUtils.isImage(showFile); if (!"".equals(message)) { request.setAttribute("message", message); return page_path; } // 展示成功否 message = upLoadService.isShow(response, showFile); if (!"".equals(message)) { request.setAttribute("message", message); return page_path; } } catch (Exception e) { request.setAttribute("message", "文件异常!" + e.getMessage()); return page_path; } return null; } public String readfile(String filepath, String fileName) { String resultLocal = ""; File file = new File(filepath); if (!file.isDirectory()) { if (file.getName().equals(fileName)) { resultLocal = file.getAbsolutePath(); } } else { String[] filelist = file.list(); for (int i = 0; i < filelist.length; i++) { File readfile = new File(filepath + File.separator + filelist[i]); if (!readfile.isDirectory()) { if (readfile.getName().equals(fileName)) { resultLocal = readfile.getAbsolutePath(); break; } } else if (readfile.isDirectory()) { readfile(filepath + File.separator + filelist[i], fileName); } } } return resultLocal; } }
Service 层
package com.zpkj.space.service; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; /** * Created by 李庆伟 */ public interface UpLoadService { public String isAvailableFilename(HttpServletRequest request, String filename); public String isdownLoad(HttpServletRequest request, HttpServletResponse response, File downloadFile, StringBuffer sb); public String isShow(HttpServletResponse response, File showFile); }
ServiceImpl 层
package com.zpkj.space.service.impl; import com.zpkj.space.service.UpLoadService; import com.zpkj.space.utils.UploadAllUtils; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; /** * Created by 李庆伟 on */ @Service public class UpLoadServiceImpl implements UpLoadService { /** * 判断参数是否可用 * * @param request * @param filename * @return */ public String isAvailableFilename(HttpServletRequest request, String filename) { String message = ""; try { String param = request.getQueryString(); if (param == null) { message = "没有传递参数"; return message; } System.out.print("连接地址为:" + request.getRequestURL() + "?" + param); Assert.hasText(filename); } catch (IllegalArgumentException e) { message = "文件名参数为空!"; return message; } return message; } /** * 断点续传核心方法 * @param request * @param response * @param downloadFile * @param sb * @return */ public String isdownLoad(HttpServletRequest request, HttpServletResponse response, File downloadFile, StringBuffer sb) { sb.append("\n 进入下载方法:"); String message = ""; long fileLength = downloadFile.length(); // 记录文件大小 long pastLength = 0; // 记录已下载文件大小 int rangeSwitch = 0; // 0:从头开始的全文下载;1:从某字节开始的下载(bytes=27000-);2:从某字节开始到某字节结束的下载(bytes=27000-39000) long toLength = 0; // 记录客户端需要下载的字节段的最后一个字节偏移量(比如bytes=27000-39000,则这个值是为39000) long contentLength = 0; // 客户端请求的字节总量 String rangeBytes = ""; // 记录客户端传来的形如“bytes=27000-”或者“bytes=27000-39000”的内容 String contentRange = "";// 响应 if (request.getHeader("Range") != null) { // 客户端请求的下载的文件块的开始字节 response.setStatus(javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT); rangeBytes = request.getHeader("Range").replaceAll("bytes=", ""); sb.append("\n Request-Range:" + rangeBytes); if (rangeBytes.indexOf('-') == rangeBytes.length() - 1) {// bytes=969998336- rangeSwitch = 1; rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));//上传开始 pastLength = Long.parseLong(rangeBytes.trim());//上传开始 contentLength = fileLength - pastLength; // 客户端请求的是 969998336 // 之后的字节 } else { // bytes=1275856879-1275877358 rangeSwitch = 2; String startPos = rangeBytes.substring(0, rangeBytes.indexOf('-'));//上传开始 String endPos = rangeBytes.substring( rangeBytes.indexOf('-') + 1, rangeBytes.length());//上传长度 pastLength = Long.parseLong(startPos.trim()); // bytes=1275856879-1275877358,从第 // 1275856879 // 个字节开始下载 toLength = Long.parseLong(endPos); // bytes=1275856879-1275877358,到第 // 1275877358 个字节结束 contentLength = toLength - pastLength; // 客户端请求的是 // 1275856879-1275877358 // 之间的字节 } } else { // 从开始进行下载 contentLength = fileLength; // 客户端要求全文下载 } /** * 如果设设置了Content -Length,则客户端会自动进行多线程下载。如果不希望支持多线程,则不要设置这个参数。 响应的格式是: * Content - Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节] * response.setHeader("Content- Length", new Long(file.length() - * pastLength).toString()); */ response.setHeader("Content-Length", new Long(fileLength - pastLength).toString()); // response.reset(); // 告诉客户端允许断点续传多线程连接下载,响应的格式是:Accept-Ranges: bytes response.setHeader("Accept-Ranges", "bytes");// 如果是第一次下,还没有断点续传,状态是默认的 // 200,无需显式设置;响应的格式是:HTTP/1.1 // 200 OK sb.append("\n Content-Length:" + new Long(fileLength - pastLength).toString()); if (pastLength != 0) { // 不是从最开始下载, // 响应的格式是: // Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小] sb.append("---不是从开始进行下载!服务器即将开始断点续传..."); switch (rangeSwitch) { case 0: { } case 1: { // 针对 bytes=27000- 的请求 contentRange = new StringBuffer("bytes ") .append(new Long(pastLength).toString()).append("-") .append(new Long(fileLength - 1).toString()) .append("/").append(new Long(fileLength).toString()) .toString(); response.setHeader("Content-Range", contentRange); break; } case 2: { // 针对 bytes=27000-39000 的请求 contentRange = rangeBytes + "/" + new Long(fileLength).toString(); response.setHeader("Content-Range", contentRange); break; } default: { break; } } } else { // 是从开始下载 sb.append("---是从开始进行下载!"); } sb.append("\n Response-contentRange:" + contentRange); //进行封装下载 //new DownloadMultiThread(response,downloadFile,contentLength,pastLength,rangeSwitch); RandomAccessFile raf = null; // 负责读取数据 ServletOutputStream os = null; // 写出数据 OutputStream out = null; // 缓冲 byte b[] = new byte[1024]; // 暂存容器 try { response.addHeader("Content-Disposition", "attachment; filename=\"" + downloadFile.getName() + "\""); response.setContentType(UploadAllUtils.setContentType(downloadFile .getName())); // set the MIME type. os = response.getOutputStream(); out = new BufferedOutputStream(os); raf = new RandomAccessFile(downloadFile, "r"); switch (rangeSwitch) { case 0: { // 普通下载,或者从头开始的下载 // 同1 } case 1: { if (pastLength == 0) { } else { // 针对 bytes=27000- 的请求 raf.seek(pastLength); // 形如 bytes=969998336- // 的客户端请求,跳过969998336 个字节 } int n = 0; while ((n = raf.read(b, 0, b.length)) != -1) { out.write(b, 0, n); } break; } case 2: { // 针对 bytes=27000-39000 的请求 raf.seek(pastLength); // 形如 bytes=1275856879-1275877358 // 的客户端请求,找到第 1275856879 个字节 int n = 0; long readLength = 0; // 记录已读字节数 while (readLength <= contentLength - 1024) {// 大部分字节在这里读取 n = raf.read(b, 0, 1024); readLength += 1024; out.write(b, 0, n); } if (readLength <= contentLength) { // 余下的不足 1024 个字节在这里读取 n = raf.read(b, 0, (int) (contentLength - readLength)); out.write(b, 0, n); } break; } default: { break; } } out.flush(); sb.append("---下载结束---"); } catch (IOException e) { /** * 在写数据的时候, 对于 ClientAbortException 之类的异常, * 是因为客户端取消了下载,而服务器端继续向浏览器写入数据时, 抛出这个异常,这个是正常的。 尤其是对于迅雷这种吸血的客户端软件, * 明明已经有一个线程在读取 bytes=1275856879-1275877358, * 如果短时间内没有读取完毕,迅雷会再启第二个、第三个。。。线程来读取相同的字节段, 直到有一个线程读取完毕,迅雷会 KILL * 掉其他正在下载同一字节段的线程, 强行中止字节读出,造成服务器抛 ClientAbortException。 * 所以,我们忽略这种异常 */ message = "#提醒# 向客户端传输时出现IO异常,但此异常是允许的,有可能客户端取消了下载,导致此异常,不用关心!" + e.getMessage(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { System.out.println(e.getMessage()); System.out.println(e); } } if (out != null) { try { out.close(); } catch (IOException e) { System.out.println(e.getMessage()); System.out.println(e); } } if (raf != null) { try { raf.close(); } catch (IOException e) { System.out.println(e.getMessage()); System.out.println(e); } } } return message; } /** * 图片展示核心代码 * * @param response * @param showFile * @return */ public String isShow(HttpServletResponse response, File showFile) { String message = ""; try { response.setContentType(UploadAllUtils.setContentType(showFile .getName())); // set the MIME type. // response.setContentType("image/jpeg; charset=GBK"); // response.setHeader("Content-Disposition", // "attachment; filename="+new // String("temp.jpg".getBytes("GBK"),"ISO8859_1")); ServletOutputStream outputStream = response.getOutputStream(); FileInputStream inputStream = new FileInputStream(showFile); byte[] buffer = new byte[1024]; int i = -1; while ((i = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, i); } outputStream.flush(); outputStream.close(); inputStream.close(); outputStream = null; } catch (IOException e) { message = "IO异常,无法展示图片" + e.getMessage(); } return message; } }Utils 中一个类
package com.zpkj.space.utils; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; /** * Created by lqw on */ public class UploadAllUtils { /** * 判断文件是否可用 * @param file * @return */ public static String isAvailableFile(File file){ String message=""; if (file.exists()) { if (file.isFile()) { if (file.length()==0){ message="文件是一个空文件"; } if (!file.canRead()) { message="文件不是一个可读的文件"; } } else { message="文件是一个文件夹"; } } else { message="文件不存在!"; } return message; } public static String setContentType(String returnFileName) { String contentType = "application/octet-stream"; if ( returnFileName.lastIndexOf(".") < 0) return contentType; returnFileName = returnFileName .toLowerCase(); returnFileName = returnFileName.substring(returnFileName .lastIndexOf("." ) + 1); if ( returnFileName.equals("html") || returnFileName.equals("htm") || returnFileName .equals("shtml" )) { contentType = "text/html"; } else if ( returnFileName.equals("apk")) { contentType = "application/vnd.android.package-archive" ; } else if ( returnFileName.equals("sis")) { contentType = "application/vnd.symbian.install"; } else if ( returnFileName.equals("sisx")) { contentType = "application/vnd.symbian.install"; } else if ( returnFileName.equals("exe")) { contentType = "application/x-msdownload"; } else if ( returnFileName.equals("msi")) { contentType = "application/x-msdownload"; } else if ( returnFileName.equals("css")) { contentType = "text/css"; } else if ( returnFileName.equals("xml")) { contentType = "text/xml"; } else if ( returnFileName.equals("gif")) { contentType = "image/gif"; } else if ( returnFileName.equals("jpeg") || returnFileName.equals("jpg")) { contentType = "image/jpeg"; } else if ( returnFileName.equals("js")) { contentType = "application/x-javascript"; } else if ( returnFileName.equals("atom")) { contentType = "application/atom+xml"; } else if ( returnFileName.equals("rss")) { contentType = "application/rss+xml"; } else if ( returnFileName.equals("mml")) { contentType = "text/mathml"; } else if ( returnFileName.equals("txt")) { contentType = "text/plain"; } else if ( returnFileName.equals("jad")) { contentType = "text/vnd.sun.j2me.app-descriptor" ; } else if ( returnFileName.equals("wml")) { contentType = "text/vnd.wap.wml"; } else if ( returnFileName.equals("htc")) { contentType = "text/x-component"; } else if ( returnFileName.equals("png")) { contentType = "image/png"; } else if ( returnFileName.equals("tif") || returnFileName.equals("tiff")) { contentType = "image/tiff"; } else if ( returnFileName.equals("wbmp")) { contentType = "image/vnd.wap.wbmp"; } else if ( returnFileName.equals("ico")) { contentType = "image/x-icon"; } else if ( returnFileName.equals("jng")) { contentType = "image/x-jng"; } else if ( returnFileName.equals("bmp")) { contentType = "image/x-ms-bmp"; } else if ( returnFileName.equals("svg")) { contentType = "image/svg+xml"; } else if ( returnFileName.equals("jar") || returnFileName.equals("var") || returnFileName .equals("ear" )) { contentType = "application/java-archive"; } else if ( returnFileName.equals("doc")) { contentType = "application/msword"; } else if ( returnFileName.equals("pdf")) { contentType = "application/pdf"; } else if ( returnFileName.equals("rtf")) { contentType = "application/rtf"; } else if ( returnFileName.equals("xls")) { contentType = "application/vnd.ms-excel"; } else if ( returnFileName.equals("ppt")) { contentType = "application/vnd.ms-powerpoint"; } else if ( returnFileName.equals("7z")) { contentType = "application/x-7z-compressed"; } else if ( returnFileName.equals("rar")) { contentType = "application/x-rar-compressed"; } else if ( returnFileName.equals("swf")) { contentType = "application/x-shockwave-flash"; } else if ( returnFileName.equals("rpm")) { contentType = "application/x-redhat-package-manager" ; } else if ( returnFileName.equals("der") || returnFileName.equals("pem") || returnFileName .equals("crt" )) { contentType = "application/x-x509-ca-cert"; } else if ( returnFileName.equals("xhtml")) { contentType = "application/xhtml+xml"; } else if ( returnFileName.equals("zip")) { contentType = "application/zip"; } else if ( returnFileName.equals("mid") || returnFileName.equals("midi") || returnFileName .equals("kar" )) { contentType = "audio/midi"; } else if ( returnFileName.equals("mp3")) { contentType = "audio/mpeg"; } else if ( returnFileName.equals("ogg")) { contentType = "audio/ogg"; } else if ( returnFileName.equals("m4a")) { contentType = "audio/x-m4a"; } else if ( returnFileName.equals("ra")) { contentType = "audio/x-realaudio"; } else if ( returnFileName.equals("3gpp") || returnFileName.equals("3gp")) { contentType = "video/3gpp"; } else if ( returnFileName.equals("mp4")) { contentType = "video/mp4"; } else if ( returnFileName.equals("mpeg") || returnFileName.equals("mpg")) { contentType = "video/mpeg"; } else if ( returnFileName.equals("mov")) { contentType = "video/quicktime"; } else if ( returnFileName.equals("flv")) { contentType = "video/x-flv"; } else if ( returnFileName.equals("m4v")) { contentType = "video/x-m4v"; } else if ( returnFileName.equals("mng")) { contentType = "video/x-mng"; } else if ( returnFileName.equals("asx") || returnFileName.equals("asf")) { contentType = "video/x-ms-asf"; } else if ( returnFileName.equals("wmv")) { contentType = "video/x-ms-wmv"; } else if ( returnFileName.equals("avi")) { contentType = "video/x-msvideo"; } return contentType; } /** * 判断文件是否是图片 * @param file * @return */ public static String isImage(File file) { String message=""; BufferedImage bi=null; try { bi = ImageIO.read(file); } catch (IOException e) { e.printStackTrace(); } if(bi == null){ message="文件不是图片!"; } return message; } }
想要看效果可以随便找个Springmvc框架,把代码扔进去,按照之前的访问路径访问就可以了
到此断点续传功能就OK了。