这段时间研究java的io与nio框架,一时兴起决定用java实现一个下载工具,主要有下面几个功能
1)支持多任务下载
2)支持多线程下载
3) 支持断点续传
4)错误线程任务的重新调度
用到的技术点
1) http协议的range头技术
2)java的多线程
3)java的网络编程,主要是HttpUrlConnection类
4)java的io文件操作, 如RandomAccessFile类
速度还不错,基本上和浏览器下载差不多,可能比它还要快,我只开了10个线程。
1. package org.blackfoxer.cat;
2.
3. import java.io.File;
4. import java.io.IOException;
5. import java.io.RandomAccessFile;
6. import java.io.UnsupportedEncodingException;
7. import java.net.HttpURLConnection;
8. import java.net.URL;
9. import java.net.URLDecoder;
10. import java.text.DecimalFormat;
11. import java.util.concurrent.CountDownLatch;
12. import java.util.concurrent.TimeUnit;
13. import java.util.regex.Matcher;
14. import java.util.regex.Pattern;
15.
16. public class Job {
17.
18. private int fileSize;
19. private String fileName;
20. private int connectTimeout = 10000;
21. private int readTimeout = 20000;
22. private String url;
23. private String storeDir;
24. private int taskNum;
25. private String jobId;
26.
27. private int[] startIndexes;
28. private int[] endIndexes;
29. private int[] progress;
30. private Task[] tasks;
31.
32. private File storeDirFile;
33. private File dtDirFile;
34. private File localFile;
35.
36. private ThreadLocal<RandomAccessFile> rafLocalTl;
37. private ThreadLocal<RandomAccessFile> rafOffsetTl;
38. private CountDownLatch latch;
39. private ProgressThread pt;
40.
41. public Job(String url, String storeDir, int taskNum) throws IOException {
42. this.url = url;
43. this.storeDir = storeDir;
44. this.taskNum = taskNum;
45. this.startIndexes = new int[taskNum];
46. this.endIndexes = new int[taskNum];
47. this.progress = new int[taskNum];
48. this.tasks = new Task[taskNum];
49. this.latch = new CountDownLatch(taskNum);
50. this.jobId = Math.abs(url.hashCode()) + "_" + taskNum;
51. this.rafLocalTl = new ThreadLocal<RandomAccessFile>();
52. this.rafOffsetTl = new ThreadLocal<RandomAccessFile>();
53. this.pt = new ProgressThread();
54. }
55.
56. public void startJob() throws Exception {
57. long start = System.currentTimeMillis();
58. System.out.println("开始下载文件...");
59. boolean j = fetchFileMetaInfo();
60. if (j) {
61. assignTasks();
62. createFiles();
63. startTasks();
64. openProgressThread();
65. waitForCompeletion();
66. long end = System.currentTimeMillis();
67. System.out.println("下载完毕,全程耗时" + (end - start) + "ms");
68. } else {
69. System.out.println("获取文件长度或文件名失败,请重试");
70. }
71. }
72.
73. private void openProgressThread() {
74. this.pt.start();
75. }
76.
77. private void waitForCompeletion() throws Exception {
78. latch.await();
79. deleteFiles();
80. pt.join();
81. }
82.
83. private void deleteFiles() {
84. if (dtDirFile != null) {
85. File[] subFiles = dtDirFile.listFiles();
86. for (File subFile : subFiles) {
87. subFile.delete();
88. }
89. dtDirFile.delete();
90. }
91. }
92.
93. // 1.fetch file size and file name
94. private boolean fetchFileMetaInfo() throws IOException {
95. HttpURLConnection connection = createConnection();
96. connection.setRequestMethod("GET");
97. if (connection.getResponseCode() == 200) {
98. this.fileSize = connection.getContentLength();
99. String disposition = connection.getHeaderField("Content-Disposition");
100. if (disposition == null) {
101. parseFileNameFromUrl(url);
102. } else {
103. parseFileNameFromDisposition(disposition);
104. }
105. if (this.fileName == null || this.fileSize < 0) {
106. return false;
107. }
108. System.out.println("找到文件资源,长度为" + fileSize + ",资源名称为" + fileName);
109. return true;
110. }
111. return false;
112. }
113.
114. private void parseFileNameFromUrl(String url) throws UnsupportedEncodingException {
115. this.fileName = url.substring(url.lastIndexOf("/") + 1, url.length());
116. if (this.fileName.contains("%")) {
117. this.fileName = URLDecoder.decode(this.fileName, "UTF-8");
118. }
119. }
120.
121. private void parseFileNameFromDisposition(String disposition) throws UnsupportedEncodingException {
122. Pattern pattern = Pattern.compile(".+filename=\"(.+?)\".*");
123. Matcher matcher = pattern.matcher(disposition);
124. if (matcher.matches()) {
125. this.fileName = new String(matcher.group(1).getBytes("ISO-8859-1"), "UTF-8");
126. } else {
127. parseFileNameFromUrl(url);
128. }
129. }
130.
131. public HttpURLConnection createConnection() throws IOException {
132. URL urlObj = new URL(url);
133. HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
134. connection.setConnectTimeout(connectTimeout);
135. connection.setReadTimeout(readTimeout);
136. connection.setRequestProperty("Accept-Charset", "UTF-8");
137. connection.setRequestProperty("contentType", "UTF-8");
138. return connection;
139. }
140.
141. // 2.assign every task start index and end index out of the file
142. private void assignTasks() throws IOException {
143. for (int i = 0; i < taskNum; i++) {
144. int size = fileSize / taskNum;
145. int startIndex = i * size;
146. int endIndex = i == taskNum - 1 ? fileSize - 1 : i * size + size - 1;
147. this.startIndexes[i] = startIndex;
148. this.endIndexes[i] = endIndex;
149. }
150. }
151.
152. // 3.create the local file and temp directory
153. private void createFiles() throws IOException {
154. storeDirFile = new File(storeDir);
155. storeDirFile.mkdirs();
156. localFile = new File(storeDirFile, fileName);
157. dtDirFile = new File(storeDirFile, "." + jobId);
158. dtDirFile.mkdirs();
159. if (!localFile.exists()) {
160. RandomAccessFile raf = new RandomAccessFile(localFile, "rw");
161. raf.setLength(fileSize);
162. raf.close();
163. }
164. }
165.
166. // 4.let the task start to do their work
167. private void startTasks() throws IOException {
168. for (int i = 0; i < taskNum; i++) {
169. Task task = new Task(this, i);
170. tasks[i] = task;
171. task.start();
172. }
173. }
174.
175. private int totalReadBytes() {
176. int totalReadBytes = 0;
177. for (int i = 0; i < progress.length; i++) {
178. totalReadBytes += progress[i];
179. }
180. return totalReadBytes;
181. }
182.
183. public int[] getStartIndexes() {
184. return startIndexes;
185. }
186.
187. public int[] getEndIndexes() {
188. return endIndexes;
189. }
190.
191. public void writeLocalFile(int startIndex, byte[] buf, int off, int len) throws IOException {
192. if (rafLocalTl.get() == null) {
193. RandomAccessFile raf = new RandomAccessFile(localFile, "rw");
194. rafLocalTl.set(raf);
195. }
196. RandomAccessFile raf = rafLocalTl.get();
197. raf.seek(startIndex);
198. raf.write(buf, off, len);
199.
200. }
201.
202. // 5.let task to report their progress
203. public void reportProgress(int index, int readBytes) {
204. progress[index] = readBytes;
205. }
206.
207. public void closeTaskResource(int index) throws IOException {
208. RandomAccessFile raf = rafLocalTl.get();
209. if (raf != null) {
210. raf.close();
211. }
212. raf = rafOffsetTl.get();
213. if (raf != null) {
214. raf.close();
215. }
216. }
217.
218. public void commitOffset(int index, int offset) throws IOException {
219. File offsetFile = new File(dtDirFile, String.valueOf(index));
220. if (rafOffsetTl.get() == null) {
221. RandomAccessFile raf = new RandomAccessFile(offsetFile, "rw");
222. rafOffsetTl.set(raf);
223. }
224. RandomAccessFile raf = rafOffsetTl.get();
225. raf.seek(0);
226. raf.writeInt(offset);
227. }
228.
229. public int readOffset(int index) throws IOException {
230. File offsetFile = new File(dtDirFile, String.valueOf(index));
231. if (offsetFile.exists()) {
232. RandomAccessFile raf = new RandomAccessFile(offsetFile, "rw");
233. raf.seek(0);
234. int offset = raf.readInt();
235. raf.close();
236. return offset;
237. }
238. return 0;
239. }
240.
241. public void reStartTask(int index) throws IOException {
242. Task task = new Task(this, index);
243. tasks[index] = task;
244. task.start();
245. System.out.println("任务" + index + "发生错误,重新调度该任务");
246. }
247.
248. public void taskFinished() {
249. latch.countDown();
250. }
251.
252. private class ProgressThread extends Thread {
253. private DecimalFormat decimalFormat = new DecimalFormat();
254.
255. public void run() {
256. decimalFormat.applyPattern("0.0");
257. while (true) {
258. try {
259. int endPointX = totalReadBytes();
260. TimeUnit.SECONDS.sleep(1);
261. int endPointY = totalReadBytes();
262. int waitSeconds = 1;
263. while (endPointY - endPointX == 0) {
264. TimeUnit.SECONDS.sleep(1);
265. waitSeconds++;
266. endPointY = totalReadBytes();
267. }
268. int speed = (endPointY - endPointX) / waitSeconds;
269. String speedStr = speed > 1024 ? speed/1024+"kb/s":speed+"b/s";
270. String percent = decimalFormat.format(endPointY * 100.0 / fileSize);
271. int remainSeconds = (fileSize - endPointY)/speed;
272. System.out.println("下载完成"+percent+"%,速度"+speedStr+",估计还需要"+remainSeconds+"秒");
273. if("100.0".equals(percent)) {
274. break;
275. }
276. } catch (InterruptedException e) {
277. e.printStackTrace();
278. }
279. }
280. }
281. }
282.
283. }
1. package org.blackfoxer.cat;
2.
3. import java.io.IOException;
4. import java.io.InputStream;
5. import java.net.HttpURLConnection;
6.
7. public class Task extends Thread {
8.
9. private Job owner;
10. private int index;
11. private int readBytes;
12. private int startIndex;
13. private int endIndex;
14.
15. public Task(Job owner,int index) throws IOException {
16. this.owner = owner;
17. this.index = index;
18. if(owner.readOffset(index)!=0) {
19. this.readBytes = owner.readOffset(index)-owner.getStartIndexes()[index];
20. owner.reportProgress(index, readBytes);
21. }
22. this.startIndex = owner.getStartIndexes()[index]+readBytes;
23. this.endIndex = owner.getEndIndexes()[index];
24. }
25.
26. public void run() {
27. InputStream inputStream = null;
28. HttpURLConnection connection = null;
29. try {
30. if(startIndex > endIndex) {
31. owner.taskFinished();
32. return;
33. }
34. connection = owner.createConnection();
35. connection.setRequestMethod("GET");
36. String range = "bytes="+startIndex+"-"+endIndex ;
37. connection.setRequestProperty("Range", range);
38. if(connection.getResponseCode()==206) {
39. inputStream = connection.getInputStream();
40. int len = -1;
41. byte buf[] = new byte[1024];
42. int offset = startIndex;
43. while((len=inputStream.read(buf))!=-1) {
44. owner.writeLocalFile(offset,buf,0,len);
45. readBytes+=len;
46. offset+=len;
47. owner.commitOffset(index,offset);
48. owner.reportProgress(index,readBytes);
49. }
50. owner.taskFinished();
51. }
52. } catch (IOException e) {
53. e.printStackTrace();
54. try {
55. owner.reStartTask(index);
56. } catch (IOException e1) {
57. e1.printStackTrace();
58. }
59. } finally {
60. if(inputStream != null) {
61. try {
62. inputStream.close();
63. } catch (IOException e) {
64. e.printStackTrace();
65. }
66. }
67. if(connection != null) {
68. connection.disconnect();
69. }
70. try {
71. owner.closeTaskResource(index);
72. } catch (IOException e) {
73. e.printStackTrace();
74. }
75. }
76. }
77. }
1. package org.blackfoxer.cat;
2.
3. import java.io.BufferedReader;
4. import java.io.IOException;
5. import java.io.InputStreamReader;
6.
7. public class JavaXunlei {
8.
9. private static final BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
10. private static String storeDir = null;
11. public static void main(String args[]) throws Exception {
12. storeDir = getInput("请先设置你的文件存储目录:");
13. int taskNum = getIntInput("请输入你的开启下载的线程数:");
14. while(true) {
15. String url = getInput("请输入文件链接地址:");
16. Job job = new Job(url,storeDir,taskNum);
17. job.startJob();
18. }
19. }
20.
21.
22. private static int getIntInput(String message) throws IOException {
23. String number = getInput(message);
24. while(!number.matches("\\d+")) {
25. System.out.println("线程数必须是1个整数");
26. number = getInput(message);
27. }
28. return Integer.parseInt(number);
29. }
30.
31. private static String getInput(String message) throws IOException {
32. System.out.print(message);
33. String line = in.readLine();
34. while(line == null || line.trim().length()<1) {
35. System.out.print(message);
36. line = in.readLine();
37. }
38. return line.trim();
39. }
40. }