网上关于多线程下载的教程基本都是以请求某个具体的文件url路径为例子, 这种例子与实际应用不太相符。
一般我们都是通过请求一个Controller路径或servlet路径来下载文件内容:
这里就涉及到根据请求头rang返回客户端需要的字节数:
服务端代码:
package org.wenchj.servlet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
public class DownloadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void download(HttpServletRequest request, HttpServletResponse response) {
try {
String filePath = request.getSession().getServletContext().getRealPath("/115com_v2.1.1.exe");
System.out.println(filePath);
File f = new File(filePath);
FileInputStream fis = new FileInputStream(f);
response.reset();
// 告诉客户端允许断点续传多线程连接下载
// 响应的格式是:
// Accept-Ranges: bytes
response.setHeader("Accept-Ranges", "bytes");
long p = 0;
long l = 0;
l = f.length();
// 如果是第一次下,还没有断点续传,状态是默认的 200,无需显式设置
// 响应的格式是:
// HTTP/1.1 200 OK
if (request.getHeader("Range") != null) // 客户端请求的下载的文件块的开始字节
{
// 如果是下载文件的范围而不是全部,向客户端声明支持并开始文件块下载
// 要设置状态
// 响应的格式是:
// HTTP/1.1 206 Partial Content
response.setStatus(javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT);// 206
// 从请求中得到开始的字节
// 请求的格式是:
// Range: bytes=[文件块的开始字节]-
p = Long.parseLong(request.getHeader("Range").replaceAll("bytes=", "").replaceAll("-", ""));
}
// 下载的文件(或块)长度
// 响应的格式是:
// Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节]
response.setHeader("Content-Length", new Long(l - p).toString());
if (p != 0) {
// 不是从最开始下载,
// 响应的格式是:
// Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
response.setHeader(
"Content-Range",
"bytes " + new Long(p).toString() + "-" + new Long(l - 1).toString() + "/"
+ new Long(l).toString());
}
// response.setHeader("Connection", "Close"); //如果有此句话不能用 IE 直接下载
// 使客户端直接下载
// 响应的格式是:
// Content-Type: application/octet-stream
response.setContentType("application/octet-stream");
// 为客户端下载指定默认的下载文件名称
// 响应的格式是:
// Content-Disposition: attachment;filename="[文件名]"
// response.setHeader("Content-Disposition",
// "attachment;filename=\"" + s.substring(s.lastIndexOf("\\") + 1) +
// "\""); //经测试 RandomAccessFile 也可以实现,有兴趣可将注释去掉,并注释掉
// FileInputStream 版本的语句
// response.setHeader("Content-Disposition", "attachment;filename=\"" + app.getName() + ".apk\"");
// 可选
setFileAttachmentHeader(response, f.getName());
// raf.seek(p);
fis.skip(p);
byte[] b = new byte[1024];
int i;
// while ( (i = raf.read(b)) != -1 ) //经测试 RandomAccessFile
// 也可以实现,有兴趣可将注释去掉,并注释掉 FileInputStream 版本的语句
while ((i = fis.read(b)) != -1) {
response.getOutputStream().write(b, 0, i);
}
// raf.close();//经测试 RandomAccessFile 也可以实现,有兴趣可将注释去掉,并注释掉
// FileInputStream 版本的语句
fis.close();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
download2(request, response);
// download2(request, response);
}
private void download2(HttpServletRequest request, HttpServletResponse response) throws IOException {
// String fileName = "/smack_3_3_1.zip";
String fileName = "/115com_v2.1.1.exe";
String filePath = request.getSession().getServletContext().getRealPath(fileName);
System.out.println("[" + Thread.currentThread().getName() + "]" + filePath);
File f = new File(filePath);
response.setHeader("Content-Length", String.valueOf(f.length()));
// 可选
setFileAttachmentHeader(response, f.getName());
System.out.println("Range--->" + request.getHeader("Range"));
byte[] content = FileUtils.readFileToByteArray(f);
System.out.println("文件总长度content.length-->" + content.length);
String rangeHeader = request.getHeader("Range");
if (rangeHeader != null) {
String[] split = rangeHeader.split("=");
String[] range = split[1].split("-");
int start = Integer.valueOf(range[0]);
int end = Integer.valueOf(range[1]);
System.out.println("start:" + start + ", end:" + end);
byte[] temp = new byte[end - start];
// 截取数组
temp = ArrayUtils.subarray(content, start, end + 1);
response.setHeader("Content-Length", String.valueOf(end - start));
responseFileContent(response, temp);
} else {
response.setHeader("Content-Length", String.valueOf(f.length()));
responseFileContent(response, content);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
IOException {
this.doGet(request, response);
}
/*
* 返回文件内容
*/
private void responseFileContent(HttpServletResponse response, byte[] content) {
try {
OutputStream outStream = response.getOutputStream();
outStream.write(content);
outStream.flush();
outStream.close();
} catch (Exception e) {
// e.printStackTrace();
}
}
/*
* 以附件形式下载(用浏览器下载会弹出对话框)
*/
private static void setFileAttachmentHeader(HttpServletResponse response, String fileName) {
try {
String encodedFileName = new String(fileName.getBytes(), "ISO8859-1");
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
客户端代码:
package org.wenchj.servlet;
import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class MulThreadDownloader {
public static void main(String[] args) throws Exception {
String path = "http://localhost:8088/download/DownloadServlet";
int threadsize = 3;
new MulThreadDownloader().download(path, threadsize);
}
private void download(String path, int threadsize) throws Exception {
URL downpath = new URL(path);
HttpURLConnection conn = (HttpURLConnection) downpath.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) {
int length = conn.getContentLength();// 获取网络文件的长度
System.out.println("获取网络文件的长度" + length);
File file = new File("115com_v2.1.1.exe");
RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");// 生成本地文件
accessFile.setLength(length);
accessFile.close();
// 计算每条线程负责下载的数据量
int block = (length % threadsize) == 0 ? length / threadsize : (length / threadsize) + 1;
System.out.println("block:" + block);
for (int threadid = 0; threadid < threadsize; threadid++) {
new DownloadThread(threadid, downpath, block, file).start();
}
}
}
// 负责下载操作
private final class DownloadThread extends Thread {
private int threadid;
private URL downpath;
private int block;
private File file;
public DownloadThread(int threadid, URL downpath, int block, File file) {
this.threadid = threadid;
this.downpath = downpath;
this.block = block;
this.file = file;
}
@Override
public void run() {
// if (threadid == 0) {
// try {
// System.out.println("线程" + threadid + "睡眠20s...");
// Thread.sleep(20 * 1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
int startposition = threadid * block;// 从网络文件的什么位置开始下载数据
int endposition = ((threadid + 1) * block) - 1;// 下载到网络文件的什么位置结束
// 指示该线程要从网络文件的startposition位置开始下载,下载到endposition位置结束
// Range:bytes=startposition-endposition
try {
RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
accessFile.seek(startposition);// 移动指针到文件的某个位置
HttpURLConnection conn = (HttpURLConnection) downpath.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes=" + startposition + "-" + endposition);
InputStream inStream = conn.getInputStream();
System.out.println(Thread.currentThread().getName() + ", ContentLength:" + conn.getContentLength());
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
accessFile.write(buffer, 0, len);
}
accessFile.close();
inStream.close();
System.out.println("线程" + (threadid) + "下载完成");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}