简介
线程可以理解为下载的通道,一个线程就是一个文件的下载通道,多线程也就是同时开起好几个下载通道.当服务器提供下载服务时,使用下载者是共享带宽的,在优先级相同的情况下,总服务器会对总下载线程进行平均分配。不难理解,如果你线程多的话,那下载的越快。现流行的下载软件都支持多线程。
注意:实现多线程的条件是服务器支持单一IP多线程下载,如果不支持的话,很有可能封IP或者是只有一个线程能连接成功,多余线程被屏蔽。部分软件提供"用代理下载"方式,这种方式不会封IP。
原理
通常服务器同时与多个用户连接,用户之间共享带宽。如果N个用户的优先级都相同,那么每个用户连接到该服务器上的实际带宽就是服务器带宽的N分之一。可以想象,如果用户数目较多,则每个用户只能占有可怜的一点带宽,下载将会是个漫长的过程。
如果你通过多个线程同时与服务器连接,那么你就可以榨取到较高的带宽了。例如原来有10个用户都通过单一线程与服务器相连,服务器的总带宽假设为56Kbps,则每个用户(每个线程)分到的带宽是5.6Kbps,即0.7K字节/秒。如果你同时打开两个线程与服务器连接,那么共有11个线程与服务器连接,而你获得的带宽将是56/11*2=10.2Kbps,约1.27K字节/秒,将近原来的两倍。你同时打开的线程越多,你所获取的带宽就越大(原来是这样,以后每次我都通过1K个线程连接:P)。当然,这种情况下占用的机器资源也越多。有些号称“疯狂下载”的下载工具甚至可以同时打开100个线程连接服务器。
多线程下载分析
服务器端数据都是以字节返回,服务器端例如有10个字节,字节就是数组,从0开始,将10个字节划分为三块,分为三个线程开始,文件大小为11。
多线程下载的实现过程:
1.得到服务器下载文件的大小,然后在本地设置一个临时文件和服务器端文件大小一致
a) 获得访问网络地址
b) 通过URL对象的openConnection()方法打开连接,返回一个连接对象
c) 设置请求头
i. setRequestMethod
ii. setConnectTimeout
iii. setReadTimeout
d) 判断是否响应成功
e) 获取文件长度(getContentLength())
f) 随机访问文件的读取与写入RandomAccessFile(file, mode)
g) 设置临时文件与服务器文件大小一致(setLength())
h) 关闭临时文件
2.计算出每个线程下载的大小(开始位置,结束位置)
a) 计算出每个线程下载的大小
b) for循环,计算出每个线程的开始、结束位置
c) 最后一个线程处理
3.每创建好一次就要开启线程下载
a) 构造方法
b) 通过URL对象的openConnection()方法打开连接,返回一个连接对象
c) 设置请求头
i. setRequestMethod
ii. setConnectTimeout
d) 判断是否响应成功(206)
e) 获取每个线程返回的流对象
f) 随机访问文件的读取与写入RandomAccessFile(file, mode)
g) 指定开始位置
h) 循环读取
i. 保存每个线程下载位置
ii. 记录每次下载位置
iii. 关闭临时记录位置文件
iv. 随机本地文件写入
v. 记录已下载大小
i) 关闭临时文件
j) 关闭输入流
4.为了杀死线程还能继续下载的情况下,从本地文件上读取已经下载文件的开始位置
a) 创建保存记录结束位置的文件
b) 读取文件
c) 将流转换为字符
d) 获取记录位置
e) 把记录位置赋给开始位置
5.当你的n个线程都下载完毕的时候我进行删除记录下载位置的缓存文件
a) 线程下载完就减去
b) 当没有正在运行的线程时切文件存在时删除文件
源代码
import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; public class DownLoatTest { private int threadNum = 3;// 线程开启的数量 private int threadRunning = 3;// 正在运行的线程 // 下载文件(得到服务器端的文件大小 ) public void downLoadFile() { // 访问网络地址 String spec = "http://127.0.0.1:8080/viedo/DSC_1495.JPG"; try { // 根据下载的地址构建url对象 URL url = new URL(spec); // 通过URL对象的openConnection()方法打开连接,返回一个连接对象 HttpURLConnection httpURLConnection = (HttpURLConnection) url .openConnection(); // 设置请求头 httpURLConnection.setRequestMethod("GET"); httpURLConnection.setConnectTimeout(5000); httpURLConnection.setReadTimeout(5000); // 判断是否响应成功 if (httpURLConnection.getResponseCode() == 200) { /** * 第一步:得到服务器下载文件的大小,然后在本地设置一个临时文件和服务器端文件大小一致 */ // 获取文件长度 int fileLength = httpURLConnection.getContentLength(); System.out.println("文件大小:" + fileLength); // 随机访问文件的读取与写入RandomAccessFile(file, mode) RandomAccessFile accessFile = new RandomAccessFile(new File( "D:\\aaa.JPG"), "rwd"); // 设置临时文件与服务器文件大小一致 accessFile.setLength(fileLength); // 关闭临时文件 accessFile.close(); /** * 第二步:计算出每个线程下载的大小(开始位置,结束位置) */ // 计算出每个线程下载的大小 int threadSize = fileLength / threadNum; // for循环,计算出每个线程的开始和结束位置 for (int threadId = 1; threadId <= 3; threadId++) { int startIndex = (threadId - 1) * threadSize;// 开始位置 int endIndex = threadId * threadSize - 1;// 结束位置 if (threadId == threadNum) {// 最后一个 线程 endIndex = fileLength - 1; } System.out.println("当前线程--" + threadId + "-----开始位置" + startIndex + "----结束位置" + endIndex + "-----线程大小" + threadSize); /** * 第三步:每创建好一次就要开启线程下载 */ new DownLoadThread(threadId, startIndex, endIndex, spec) .start(); } } else { System.out.println("访问响应不成功"); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 每创建好一次就要开启线程下载 * * @author zhaoyazhi * */ class DownLoadThread extends Thread { // 成员变量 private int threadId; private int startIndex; private int endIndex; private String path; /** * * @param threadId * 线程的序号 * @param startIndex * 线程下载开始位置 * @param endIndex * 线程下载结束位置 * @param path * 线程下载保存文件的路径 */ public DownLoadThread(int threadId, int startIndex, int endIndex, String path) { super(); this.threadId = threadId; this.startIndex = startIndex; this.endIndex = endIndex; this.path = path; } @Override public void run() { // 可以通过每个线程去下载文件 try { /** * 第四步:从本地文件上读取已经下载文件的开始位置 */ File recordFile = new File("D:\\" + threadId + ".txt"); if (recordFile.exists()) { // 读取文件 InputStream is = new FileInputStream(recordFile); // 利用工具类转换 String value = StreamTools.streamToStr(is); // 获取记录的位置 int recordIndex = Integer.parseInt(value); // 把记录的位置付给开始位置 startIndex = recordIndex; } // 通过path对象构造URL 对象 URL url = new URL(path); // 通过URL对象openConnection HttpURLConnection httpURLConnection = (HttpURLConnection) url .openConnection(); // 设置请求头 httpURLConnection.setRequestMethod("GET"); httpURLConnection.setConnectTimeout(5000); // 设置下载文件的开始位置和结束位置 httpURLConnection.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex); // 获取状态码 int code = httpURLConnection.getResponseCode(); // System.out.println(code); // 判断是否成功 只要设置"Range"头,返回的状态码就是206 if (code == 206) { // 获取每个线程返回的流对象 InputStream is = httpURLConnection.getInputStream(); // 创建随机访问的对象 RandomAccessFile accessFile = new RandomAccessFile( new File("D:\\aaa.JPG"), "rwd"); // 指定开始位置 accessFile.seek(startIndex); // 定义读取的长度 int len = 0; // 定义缓冲区 byte buffer[] = new byte[1024]; int total = 0; // 循环读取 while ((len = is.read(buffer)) != -1) { System.out.println("当前线程--" + threadId + "-----当前下载的位置是" + (startIndex + total)); // 保存每个线程的下载位置 RandomAccessFile threadFile = new RandomAccessFile( new File("D:\\" + threadId + ".txt"), "rwd"); // 记录每次下载位置 threadFile.writeBytes((startIndex + total) + ""); threadFile.close(); accessFile.write(buffer, 0, len); total += len;// 已经下载大小 } accessFile.close(); is.close(); System.out.println("当前线程" + threadId + "---下载完毕"); /** * 第五步:当你的n个线程都下载完毕 的时候我才进行删除记录下载位置的缓存文件 */ deleteRecordFile(); } else { System.out.println("服务器端返回错误。。。。。。"); } // 设置你下载文件 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * synchronized避免线程同步 下载完删除存储文件下载位置的临时文件 */ public synchronized void deleteRecordFile() { // 线程下载完就减去 threadRunning--; // 当没有正在运行的线程 if (threadRunning == 0) { for (int i = 1; i <= 3; i++) { File recordFile = (new File("D:\\" + i + ".txt")); if (recordFile.exists()) { recordFile.delete(); } } } } public static void main(String[] args) { new DownLoatTest().downLoadFile(); } }
知识点解析
知识点1:RandomAccessFile随机访问文件的读取与写入
正在运行的线程API如下:
从上述API我们得知RandomAccessFile有两个参数:file 和mode
File:该文件对象
Mode:访问模式,包括r(只读),rw(读写,若不存在创建),rws(读写,元数据和内容),rwd(读写,内容)
元数据:文件的来源 ,如下图,文件属性的详细信息
知识点二:服务器数据与文件下标开始的不同
服务器的数据都是以流的形式存在,也就是数组,下标是0开始,而文件长度是以1开始,所以要-1
程序代码实例:
// 最后一个 线程
if (threadId == threadNum) {
endIndex = fileLength - 1;
}
知识点三:使用Http的Range头字段指定每条线程从文件的什么位置开始下载
HttpURLConnection.setRequestProperty("Range", "bytes=2097152-");
设置请求头setRequestProperty是为了设置每一个线程的开始位置与结束位置
知识点四:只要设置"Range"头,返回的状态码就是206。
程序实例代码:if (code == 206)
在每一个线程中,获取的状态码不是200,是206。
知识点五:保存文件
使用RandomAccessFile类指定每条线程从本地文件的什么位置开始写入数据。
RandomAccessFile threadfile = new RandomAccessFile("QQWubiSetup.exe ","rw");
threadfile.seek(2097152);//从文件的什么位置开始写入数据
Write第二个参数是从0 开始还是从startIndex开始?
答案是从0开始,因为创建完随机 读写对象后,已经指定了开始位置(accessFile.seek(startIndex);),此位置就是读的长度,所以从0开始就可以了。
如果从startIndex开始则会报数组越界的错误,如下图
知识点六:避免线程同步在方法返回值前加synchronized
如:public synchronized void deleteRecordFile() {}
知识点七:把一个文件输入流的东西读取出来,保存到ByteArrayOutputStream字节里面,返回字符串
固定方法:
public static String streamToStr(InputStream is) { String value = null; try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int len = 0; byte buffer[] = new byte[1024]; while ((len = is.read(buffer)) != -1) { baos.write(buffer, 0, len); } baos.close(); is.close(); value = new String(baos.toByteArray()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return value; }
输出结果
输出1:当我下载时,程序为我创建的缓存文件
输出2:显示文件大小及线程分别对应的开始位置,结束位置,线程大小
输出3:当我杀死进程时保留的位置
此时我们可以看到
线程1结束位置:557056
线程2结束位置:1574262
线程3结束位置:3246828
当我们在打开的时候
线程1开始位置:557056
线程2开始位置:1574262
线程3开始位置:3246828
输出4:当有一个线程下载完时,记录下载位置的临时文件没被删除;当三个线程都跑完,记录下载位置的文件被删除。
当线程三下载完毕,而其他线程没有下载完:
查看D盘根目录 。临时保存文件尚存在
当线程1,2,3全部下载完成后,缓存文件消失
输出五:图片完整性
通过属性查看源文件的大小:
通过属性查看下载文件的大小
源代码下载地址:http://download.csdn.net/detail/zhaoyazhi2129/7406681
转载请注明出处:http://blog.csdn.net/zhaoyazhi2129/article/details/27174145
赵雅智_java多线程下载