最近跟着大神学习写了一个java多线程下载和断点续传的demo,在此记录一下相关信息记录
环境准备:
1.准备一个tomcat容器,用来模拟服务器环境,可直接到apache官网下载后解压后即可使用。
2.复制一个较大的文件到tomcat-->webapps-->ROOT目录下,建议复制exe文件,因为没有准备MD5码校验机制,所以在下载后可以通过观察该程序的运行状态判断文件下载是否成功。
3.启动tomcat容器,如有端口号被占用的情况,修改这个tomcat的端口号,具体步骤不再详述。或者停止占有这个端口号的进程也可以。然后再浏览器中输入下载链接,看一下能否响应下载。(链接一般就是http://127.0.0.1:8080/文件名称),有修改的情况请自行更正。
4.建议使用maven构建工程,当然,只是建议
预警:本人小白一枚,写demo也是尽量遵守格式规范什么的,但水平就在这里,所以代码难免会让人吐槽,也希望有高手能多多指点。
代码实现
先记录一下几个重要点
1.RandomAccessFile类,先看一下文档介绍,简单来讲就是这个文件类底层是以一个很大的byte数据,还定一个文件指针,可以从任意位置读取和写入文件内容。
/**
* Instances of this class support both reading and writing to a random access file. A
* random access file behaves like a large array of bytes stored in the file system.
* There is a kind of cursor, or index into the implied array, called the <em>file
* pointer</em>; input operations read bytes starting at the file pointer and advance
* the file pointer past the bytes read. If the random access file is created in
* read/write mode, then output operations are also available; output operations write
* bytes starting at the file pointer and advance the file pointer past the bytes
* written. Output operations that write past the current end of the implied array cause
* the array to be extended. The file pointer can be read by the {@code getFilePointer}
* method and set by the {@code seek} method.
* <p>
* It is generally true of all the reading routines in this class that if end-of-file is
* reached before the desired number of bytes has been read, an {@code EOFException}
* (which is a kind of {@code IOException}) is thrown. If any byte cannot be read for any
* reason other than end-of-file, an {@code IOException} other than {@code EOFException}
* is thrown. In particular, an {@code IOException} may be thrown if the stream has been
* closed.
* @author unascribed
* @since JDK1.0
*/
此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。
通常,如果此类中的所有读取例程在读取所需数量的字节之前已到达文件末尾,则抛出 EOFException(是一种 IOException)。如果由于某些原因无法读取任何字节,而不是在读取所需数量的字节之前已到达文件末尾,则抛出 IOException,而不是 EOFException。需要特别指出的是,如果流已被关闭,则可能抛出 IOException。
2.Range,是在 HTTP/1.1(http://www.w3.org/Protocols/rfc2616/rfc2616.html)里新增的一个 header field,也是现在众多号称多线程下载工具(如 FlashGet、迅雷等)实现多线程下载的核心所在。
。。。这个请求头属性第一次接触到,详细的不了解,就不在班门弄斧了,反正这个demo用到这个属性,而且这个属性很重要
3.每个线程的下载任务的分配
由于是多线程下载,所以下载前需要得知文件的大小,然后给每一个线程分配下载任务,简单来说就是RandomAccessFile数组分段填充。
// 分配每个线程下载任务的资源大小
int blockSize = length / threadCount;
// 每个线程的开始 与结束 的位置
for (int i = 0; i < threadCount; i++) {
int startIndex = i * blockSize;
int endIndex = (i + 1) * blockSize - 1;
// 最后一个线程下载剩余的所有资源
if (i == threadCount - 1) {
startIndex = i * blockSize;
endIndex = length - 1;
}
这里是demo的所有源代码连接:点击这里下载源代码
下载线程的部分源码
private static class DownloadThread extends Thread {
/**
* @Fields startIndex <简要描述>: 资源下载的起始位置<Br/>
*/
private int startIndex;
/**
* @Fields endIndex <简要描述>: 资源下载的结束为止 <Br/>
*/
private int endIndex;
/**
* @Fields threadId <简要描述>: 当前线程的ID号 <Br/>
*/
private int threadId;
public DownloadThread(int startIndex, int endIndex, int threadId) {
super();
this.startIndex = startIndex;
this.endIndex = endIndex;
this.threadId = threadId;
}
/**
*
*/
@Override
public void run() {
URL url;
try {
// 获取下载路径
url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求方式
connection.setRequestMethod("GET");
// 设置下载请的超时时间
connection.setConnectTimeout(5000);
// 查看下载路径的进度记录文件是否存在,若之前有下载断点,则从此次断点开始
File file = new File(getFileName(path) + threadId + ".txt");
if (file.exists() && file.length() > 0) {
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
// 读取上次下载的位置
String lastPositionString = reader.readLine();
int lastPosition = Integer.parseInt(lastPositionString);
// 更新此次下载的开始位置,从上次断开的位置开始下
startIndex = lastPosition;
System.out.println("当前线程ID::" + threadId + "真实下载位置 :: " + startIndex + "-----" + endIndex);
reader.close();
}
// 设置请求头信息
// Server通过请求头中的Range: bytes=0-xxx来判断是否是做Range请求,
// 如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成206,表示Partial
// Content,并设置Content-Range。
// 如果无效,则返回416状态码,表明Request Range Not Satisfiable
// @see{http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.17}
// 如果不包含Range的请求头,则继续通过常规的方式响应。
connection.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
int code = connection.getResponseCode();
if (code == 206) {
RandomAccessFile randomAccessFile = new RandomAccessFile(getFileName(path), "rw");
// 设置开始位置
randomAccessFile.seek(startIndex);
InputStream inputStream = connection.getInputStream();
int len = -1;
// 设置文件下载缓冲大小为1M,提高下载速度,不宜设置过大
byte[] buffer = new byte[1024 * 1024];
// 当前线程下载的文件大小
int total = 0;
while ((len = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, len);
total += len;
// 记录当前的下载位置记录 = 已下载的长度+开始下载的位置
int currentThreadPosition = total + startIndex;
// 将当前下载进度记录到文本文件中
RandomAccessFile downloadIndex = new RandomAccessFile(getFileName(path) + threadId + ".txt",
"rwd");
downloadIndex.write(String.valueOf(currentThreadPosition).getBytes());
downloadIndex.close();
}
randomAccessFile.close();
System.out.println("threadId : " + threadId + " 下载完成");
/**
* 会有多个线程同时对runningThread这个变量进行修改
* <p>
* 此处需要加上同步锁
*
*/
synchronized (this) {
// 若已经下载完毕,则将正在运行的线程数记录减1
runningThread--;
// 若正在运行的线程为0,即所有的下载线程均已结束,则删除下载进度记录文件
if (runningThread == 0) {
for (int i = 0; i < threadCount; i++) {
File recordFile = new File(getFileName(path) + i + ".txt");
recordFile.delete();
}
}
}
}
} catch (MalformedURLException e) {
LOGGER.error("请求的URL协议不合法或请求路径错误,错误信息 :" + e.getLocalizedMessage());
} catch (ProtocolException e) {
LOGGER.error("底层协议错误,错误信息 : " + e.getLocalizedMessage());
} catch (IOException e) {
LOGGER.error("文件读写错误,错误信息 :" + e.getLocalizedMessage());
}
}
}
/**
*
* @Description: 通过截取下载请求获取文件名
* @param @param
* path 下载请求路径
* @param @return
* @return String 下载的文件名
*/
public static String getFileName(String path) {
int lastIndex = path.lastIndexOf("/");
return path.substring(lastIndex + 1);
}