赵雅智_java多线程下载

简介

线程可以理解为下载的通道,一个线程就是一个文件的下载通道,多线程也就是同时开起好几个下载通道.当服务器提供下载服务时,使用下载者是共享带宽的,在优先级相同的情况下,总服务器会对总下载线程进行平均分配。不难理解,如果你线程多的话,那下载的越快。现流行的下载软件都支持多线程。

注意:实现多线程的条件是服务器支持单一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多线程下载

时间: 2024-10-05 10:45:05

赵雅智_java多线程下载的相关文章

赵雅智_android多线程下载带进度条

progressBar说明 在某些操作的进度中的可视指示器,为用户呈现操作的进度,还它有一个次要的进度条,用来显示中间进度,如在流媒体播放的缓冲区的进度.一个进度条也可不确定其进度.在不确定模式下,进度条显示循环动画.这种模式常用于应用程序使用任务的长度是未知的. XML重要属性 android:progressBarStyle:默认进度条样式 android:progressBarStyleHorizontal:水平样式 progressBar重要方法 getMax():返回这个进度条的范围的

赵雅智_java网络编程(4)TCP/IP、Http和Socket的区别

通过java网络编程(1)网络体系结构及通信协议我知道IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层, 三者从本质上来说没有可比性 TPC/IP协议是传输层协议,主要解决数据如何在网络中传输, 而HTTP是应用层协议,主要解决如何包装数据. socket是对TCP/IP协议的封装和应用(程序员层面上). 三者关系 总的来说 传输层的TCP是基于网络层的IP协议的 应用层的HTTP协议又是基于传输层的TCP协议的 Socket本身不算是协议,它只是提供了一个针对TCP或者

赵雅智_java网络编程(5)TCP和udp区别

1.TCP是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,但TCP的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性; 而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的.不可靠的一种数据传输协议. 2.也正由于1所说的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好. 知道了TCP和UD

赵雅智:android教学大纲

带下划线为具体内容链接地址,点击后可跳转,希望给大家尽一些微薄之力,目前还在整理中 教学章节 教学内容 学时安排 备注 1 Android快速入门 2 Android模拟器与常见命令 3 Android用户界面设计 4 Android网络通信及开源框架引用 5 线程与消息处理 6 数据存储及访问 7 Android基本单元应用activity 8 Android应用核心Intent 9 资源访问 10 ContentProvider实现数据共享 11 BroadcastReceiver 12 S

赵雅智_Fragment

当我们需要动态的多界面切换的时候,就需要将UI元素和Activity融合成一个模块.在2.3中我们一般通过各种Activity中进行跳转来实现多界面的跳转和单个界面动态改变.在4.0或以上系统中就可以使用新的特性来方便的达到这个效果--Fragment类.Fragment类似一个嵌套Activity,可以定义自己的layout和自己的生命周期. 多个Fragment可以放在一个Activity中(所以上面讲到类似一个嵌套Activity),而这个类可以对这些Fragment进行配置以适应不同的屏

赵雅智_Intent传值

Intent简介 Android基本的设计理念是鼓励减少组件间的耦合,因此Android提供了Intent (意图) ,Intent提供了一种通用的消息系统,它允许在你的应用程序与其它的应用程序间传递Intent来执行动作和产生事件.使用Intent可以激活Android应用的三个核心组件:活动.服务和广播接收器. 在一个Activity中可以使用系统提供的startActivity(Intent intent)方法打开新的Activity,在打开新的Activity前,你可以决定是否为新的Ac

赵雅智_activity生命周期

Activity有三个状态 运行状态 当它在屏幕前台时(位于当前任务堆栈的顶部),它是激活或运行状态.它就是响应用户操作的Activity. 暂停状态 当它失去焦点但仍然对用户可见时,它处于暂停状态.即在它之上有另外一个Activity.这个Activity也许是透明的,或者没有完全覆盖全屏,所以被暂停的Activity仍对用户可见.暂停的Activity仍然是存活状态(它保留着所有的状态和成员信息并保持和窗口管理器的连接),但系统处于极低内存时仍然可以杀死这个Activity. 停止状态 完全

赵雅智_pull解析xml

Pull 解析器简介 Pull 解析器的运行方式与 SAX 解析器相似.它提供了类似的事件,如: 开始元素和结束元素事件,使用xmlPullParser.next() 可以进入下一个元素并触发相应事件.跟 SAX 不同的 是, Pull 解析器产生的事件是一个数字,而非方法,因此可以使用一个 switch 对事件进行处理.当元素开始解析时,调用 parser.nextText() 方法可以获取下一个 Text 类型节点的值. Pull解析器的源码及文档下载网址:http://www.xmlpul

赵雅智:js知识点汇总

赵雅智:js知识点汇总,布布扣,bubuko.com