- 浏览: 277383 次
- 性别:
- 来自: 北京
-
文章分类
最新评论
-
java老大爷:
技术扶持 技术交流 汇聚程序员精英 群里有马士兵2017 ...
大数据数据 -
yeruowei:
遇到同样的问题,特意登录点赞
导入数据库时出现ORA-01435: 用户不存在 -
1017974387:
特意登陆来给你赞。。。。。。
导入数据库时出现ORA-01435: 用户不存在 -
sgy1103:
您好:看了苯苯熊家庭记帐注册码破解很羡慕。我在用,不过换电脑后 ...
笨笨家庭记账本—破解 -
shellbye:
居然真是这个原因。。。哈哈
maven设置中ERROR: JAVA_HOME is set to an invalid directory
/**
* author:annegu
* date:2009-07-16
*/
annegu做了一个简单的Http多线程的下载程序,来讨论一下多线程并发下载以及断点续传的问题。
这个程序的功能,就是可以分多个线程从目标地址上下载数据,每个线程负责下载一部分,并可以支持断点续传和超时重连。
下载的方法是download(),它接收两个参数,分别是要下载的页面的url和编码方式。在这个负责下载的方法中,主要分了三个步骤。第一步是用来设置断点续传时候的一些信息的,第二步就是主要的分多线程来下载了,最后是数据的合并。
1、多线程下载:
Java代码
1. public String download(String urlStr, String charset) {
2. this.charset = charset;
3. long contentLength = 0;
4.① CountDownLatch latch = new CountDownLatch(threadNum);
5. long[] startPos = new long[threadNum];
6. long endPos = 0;
7.
8. try {
9. // 从url中获得下载的文件格式与名字
10. this.fileName = urlStr.substring(urlStr.lastIndexOf("/") + 1, urlStr.lastIndexOf("?")>0 ? urlStr.lastIndexOf("?") : urlStr.length());
11. if("".equalsIgnoreCase(this.fileName)){
12. this.fileName = UUID.randomUUID().toString();
13. }
14.
15. this.url = new URL(urlStr);
16. URLConnection con = url.openConnection();
17. setHeader(con);
18. // 得到content的长度
19. contentLength = con.getContentLength();
20. // 把context分为threadNum段的话,每段的长度。
21. this.threadLength = contentLength / threadNum;
22.
23. // 第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。在第4点中说明。
24. startPos = setThreadBreakpoint(fileDir, fileName, contentLength, startPos);
25.
26. //第二步,分多个线程下载文件
27. ExecutorService exec = Executors.newCachedThreadPool();
28. for (int i = 0; i < threadNum; i++) {
29. // 创建子线程来负责下载数据,每段数据的起始位置为(threadLength * i + 已下载长度)
30. startPos[i] += threadLength * i;
31.
32. /*设置子线程的终止位置,非最后一个线程即为(threadLength * (i + 1) - 1)
33. 最后一个线程的终止位置即为下载内容的长度*/
34. if (i == threadNum - 1) {
35. endPos = contentLength;
36. } else {
37. endPos = threadLength * (i + 1) - 1;
38. }
39. // 开启子线程,并执行。
40. ChildThread thread = new ChildThread(this, latch, i, startPos[i], endPos);
41. childThreads[i] = thread;
42. exec.execute(thread);
43. }
44.
45. try {
46. // 等待CountdownLatch信号为0,表示所有子线程都结束。
47.② latch.await();
48. exec.shutdown();
49.
50. // 第三步,把分段下载下来的临时文件中的内容写入目标文件中。在第3点中说明。
51. tempFileToTargetFile(childThreads);
52.
53. } catch (InterruptedException e) {
54. e.printStackTrace();
55. }
56. }
public String download(String urlStr, String charset) {
this.charset = charset;
long contentLength = 0;
① CountDownLatch latch = new CountDownLatch(threadNum);
long[] startPos = new long[threadNum];
long endPos = 0;
try {
// 从url中获得下载的文件格式与名字
this.fileName = urlStr.substring(urlStr.lastIndexOf("/") + 1, urlStr.lastIndexOf("?")>0 ? urlStr.lastIndexOf("?") : urlStr.length());
if("".equalsIgnoreCase(this.fileName)){
this.fileName = UUID.randomUUID().toString();
}
this.url = new URL(urlStr);
URLConnection con = url.openConnection();
setHeader(con);
// 得到content的长度
contentLength = con.getContentLength();
// 把context分为threadNum段的话,每段的长度。
this.threadLength = contentLength / threadNum;
// 第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。在第4点中说明。
startPos = setThreadBreakpoint(fileDir, fileName, contentLength, startPos);
//第二步,分多个线程下载文件
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) {
// 创建子线程来负责下载数据,每段数据的起始位置为(threadLength * i + 已下载长度)
startPos[i] += threadLength * i;
/*设置子线程的终止位置,非最后一个线程即为(threadLength * (i + 1) - 1)
最后一个线程的终止位置即为下载内容的长度*/
if (i == threadNum - 1) {
endPos = contentLength;
} else {
endPos = threadLength * (i + 1) - 1;
}
// 开启子线程,并执行。
ChildThread thread = new ChildThread(this, latch, i, startPos[i], endPos);
childThreads[i] = thread;
exec.execute(thread);
}
try {
// 等待CountdownLatch信号为0,表示所有子线程都结束。
② latch.await();
exec.shutdown();
// 第三步,把分段下载下来的临时文件中的内容写入目标文件中。在第3点中说明。
tempFileToTargetFile(childThreads);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
首先来看最主要的步骤:多线程下载。
首先从url中提取目标文件的名称,并在对应的目录创建文件。然后取得要下载的文件大小,根据分成的下载线程数量平均分配每个线程需要下载的数据量,就是threadLength。然后就可以分多个线程来进行下载任务了。
在这个例子中,并没有直接显示的创建Thread对象,而是用Executor来管理Thread对象,并且用CachedThreadPool来创建的线程池,当然也可以用FixedThreadPool。CachedThreadPool在程序执行的过程中会创建与所需数量相同的线程,当程序回收旧线程的时候就停止创建新线程。FixedThreadPool可以预先新建参数给定个数的线程,这样就不用在创建任务的时候再来创建线程了,可以直接从线程池中取出已准备好的线程。下载线程的数量是通过一个全局变量threadNum来控制的,默认为5。
好了,这5个子线程已经通过Executor来创建了,下面它们就会各自为政,互不干涉的执行了。线程有两种实现方式:实现Runnable接口;继承Thread类。
ChildThread就是子线程,它作为DownloadTask的内部类,继承了Thread,它的构造方法需要5个参数,依次是一个对DownloadTask的引用,一个CountDownLatch,id(标识线程的id号),startPosition(下载内容的开始位置),endPosition(下载内容的结束位置)。
这个CountDownLatch是做什么用的呢?
现在我们整理一下思路,要实现分多个线程来下载数据的话,我们肯定还要把这多个线程下载下来的数据进行合。主线程必须等待所有的子线程都执行结束之后,才能把所有子线程的下载数据按照各自的id顺序进行合并。CountDownLatch就是来做这个工作的。
CountDownLatch用来同步主线程,强制主线程等待所有的子线程执行的下载操作完成。在主线程中,CountDownLatch对象被设置了一个初始计数器,就是子线程的个数5个,代码①处。在新建了5个子线程并开始执行之后,主线程用CountDownLatch的await()方法来阻塞主线程,直到这个计数器的值到达0,才会进行下面的操作,代码②处。
对每个子线程来说,在执行完下载指定区间与长度的数据之后,必须通过调用CountDownLatch的countDown()方法来把这个计数器减1。
2、在全面开启下载任务之后,主线程就开始阻塞,等待子线程执行完毕,所以下面我们来看一下具体的下载线程ChildThread。
Java代码
1.public class ChildThread extends Thread {
2. public static final int STATUS_HASNOT_FINISHED = 0;
3. public static final int STATUS_HAS_FINISHED = 1;
4. public static final int STATUS_HTTPSTATUS_ERROR = 2;
5. private DownloadTask task;
6. private int id;
7. private long startPosition;
8. private long endPosition;
9. private final CountDownLatch latch;
10. // private RandomAccessFile tempFile = null;
11. private File tempFile = null;
12. //线程状态码
13. private int status = ChildThread.STATUS_HASNOT_FINISHED;
14.
15. public ChildThread(DownloadTask task, CountDownLatch latch, int id, long startPos, long endPos) {
16. super();
17. this.task = task;
18. this.id = id;
19. this.startPosition = startPos;
20. this.endPosition = endPos;
21. this.latch = latch;
22.
23. try {
24. tempFile = new File(this.task.fileDir + this.task.fileName + "_" + id);
25. if(!tempFile.exists()){
26. tempFile.createNewFile();
27. }
28. } catch (IOException e) {
29. e.printStackTrace();
30. }
31.
32. }
33.
34. public void run() {
35. System.out.println("Thread " + id + " run ...");
36. HttpURLConnection con = null;
37. InputStream inputStream = null;
38. BufferedOutputStream outputStream = null;
39. long count = 0;
40. long threadDownloadLength = endPosition - startPosition;
41.
42. try {
43. outputStream = new BufferedOutputStream(new FileOutputStream(tempFile.getPath(), true));
44. } catch (FileNotFoundException e2) {
45. e2.printStackTrace();
46. }
47.
48.③ for(int k = 0; k < 10; k++){
49. if(k > 0)
50. System.out.println("Now thread " + id + "is reconnect, start position is " + startPosition);
51. try {
52. //打开URLConnection
53. con = (HttpURLConnection) task.url.openConnection();
54. setHeader(con);
55. con.setAllowUserInteraction(true);
56. //设置连接超时时间为10000ms
57.④ con.setConnectTimeout(10000);
58. //设置读取数据超时时间为10000ms
59. con.setReadTimeout(10000);
60.
61. if(startPosition < endPosition){
62. //设置下载数据的起止区间
63. con.setRequestProperty("Range", "bytes=" + startPosition + "-"
64. + endPosition);
65. System.out.println("Thread " + id + " startPosition is " + startPosition);
66. System.out.println("Thread " + id + " endPosition is " + endPosition);
67.
68. //判断http status是否为HTTP/1.1 206 Partial Content或者200 OK
69. //如果不是以上两种状态,把status改为STATUS_HTTPSTATUS_ERROR
70.⑤ if (con.getResponseCode() != HttpURLConnection.HTTP_OK
71. && con.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
72. System.out.println("Thread " + id + ": code = "
73. + con.getResponseCode() + ", status = "
74. + con.getResponseMessage());
75. status = ChildThread.STATUS_HTTPSTATUS_ERROR;
76. this.task.statusError = true;
77. outputStream.close();
78. con.disconnect();
79. System.out.println("Thread " + id + " finished.");
80. latch.countDown();
81. break;
82. }
83.
84. inputStream = con.getInputStream();
85.
86. int len = 0;
87. byte[] b = new byte[1024];
88. while ((len = inputStream.read(b)) != -1) {
89. outputStream.write(b, 0, len);
90. count += len;
91.⑥ startPosition += len;
92. //每读满4096个byte(一个内存页),往磁盘上flush一下
93. if(count % 4096 == 0){
94.⑦ outputStream.flush();
95. }
96. }
97.
98. System.out.println("count is " + count);
99. if (count >= threadDownloadLength) {
100. status = ChildThread.STATUS_HAS_FINISHED;
101. }
102.⑧ outputStream.flush();
103. outputStream.close();
104. inputStream.close();
105. con.disconnect();
106. } else {
107. status = ChildThread.STATUS_HAS_FINISHED;
108. }
109.
110. System.out.println("Thread " + id + " finished.");
111. latch.countDown();
112. break;
113. } catch (IOException e) {
114. try {
115.⑨ outputStream.flush();
116.⑩ TimeUnit.SECONDS.sleep(getSleepSeconds());
117. } catch (InterruptedException e1) {
118. e1.printStackTrace();
119. } catch (IOException e2) {
120. e2.printStackTrace();
121. }
122. continue;
123. }
124. }
125. }
126.}
public class ChildThread extends Thread {
public static final int STATUS_HASNOT_FINISHED = 0;
public static final int STATUS_HAS_FINISHED = 1;
public static final int STATUS_HTTPSTATUS_ERROR = 2;
private DownloadTask task;
private int id;
private long startPosition;
private long endPosition;
private final CountDownLatch latch;
// private RandomAccessFile tempFile = null;
private File tempFile = null;
//线程状态码
private int status = ChildThread.STATUS_HASNOT_FINISHED;
public ChildThread(DownloadTask task, CountDownLatch latch, int id, long startPos, long endPos) {
super();
this.task = task;
this.id = id;
this.startPosition = startPos;
this.endPosition = endPos;
this.latch = latch;
try {
tempFile = new File(this.task.fileDir + this.task.fileName + "_" + id);
if(!tempFile.exists()){
tempFile.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void run() {
System.out.println("Thread " + id + " run ...");
HttpURLConnection con = null;
InputStream inputStream = null;
BufferedOutputStream outputStream = null;
long count = 0;
long threadDownloadLength = endPosition - startPosition;
try {
outputStream = new BufferedOutputStream(new FileOutputStream(tempFile.getPath(), true));
} catch (FileNotFoundException e2) {
e2.printStackTrace();
}
③ for(int k = 0; k < 10; k++){
if(k > 0)
System.out.println("Now thread " + id + "is reconnect, start position is " + startPosition);
try {
//打开URLConnection
con = (HttpURLConnection) task.url.openConnection();
setHeader(con);
con.setAllowUserInteraction(true);
//设置连接超时时间为10000ms
④ con.setConnectTimeout(10000);
//设置读取数据超时时间为10000ms
con.setReadTimeout(10000);
if(startPosition < endPosition){
//设置下载数据的起止区间
con.setRequestProperty("Range", "bytes=" + startPosition + "-"
+ endPosition);
System.out.println("Thread " + id + " startPosition is " + startPosition);
System.out.println("Thread " + id + " endPosition is " + endPosition);
//判断http status是否为HTTP/1.1 206 Partial Content或者200 OK
//如果不是以上两种状态,把status改为STATUS_HTTPSTATUS_ERROR
⑤ if (con.getResponseCode() != HttpURLConnection.HTTP_OK
&& con.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
System.out.println("Thread " + id + ": code = "
+ con.getResponseCode() + ", status = "
+ con.getResponseMessage());
status = ChildThread.STATUS_HTTPSTATUS_ERROR;
this.task.statusError = true;
outputStream.close();
con.disconnect();
System.out.println("Thread " + id + " finished.");
latch.countDown();
break;
}
inputStream = con.getInputStream();
int len = 0;
byte[] b = new byte[1024];
while ((len = inputStream.read(b)) != -1) {
outputStream.write(b, 0, len);
count += len;
⑥ startPosition += len;
//每读满4096个byte(一个内存页),往磁盘上flush一下
if(count % 4096 == 0){
⑦ outputStream.flush();
}
}
System.out.println("count is " + count);
if (count >= threadDownloadLength) {
status = ChildThread.STATUS_HAS_FINISHED;
}
⑧ outputStream.flush();
outputStream.close();
inputStream.close();
con.disconnect();
} else {
status = ChildThread.STATUS_HAS_FINISHED;
}
System.out.println("Thread " + id + " finished.");
latch.countDown();
break;
} catch (IOException e) {
try {
⑨ outputStream.flush();
⑩ TimeUnit.SECONDS.sleep(getSleepSeconds());
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (IOException e2) {
e2.printStackTrace();
}
continue;
}
}
}
}
在ChildThread的构造方法中,除了设置一些从主线程中带来的id, 起始位置之外,就是新建了一个临时文件用来存放当前线程的下载数据。临时文件的命名规则是这样的:下载的目标文件名+”_”+线程编号。
现在让我们来看看从网络中读数据是怎么读的。我们通过URLConnection来获得一个http的连接。有些网站为了安全起见,会对请求的http连接进行过滤,因此为了伪装这个http的连接请求,我们给httpHeader穿一件伪装服。下面的setHeader方法展示了一些非常常用的典型的httpHeader的伪装方法。比较重要的有:Uer-Agent模拟从Ubuntu的firefox浏览器发出的请求;Referer模拟浏览器请求的前一个触发页面,例如从skycn站点来下载软件的话,Referer设置成skycn的首页域名就可以了;Range就是这个连接获取的流文件的起始区间。
Java代码
1.private void setHeader(URLConnection con) {
2. con.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3");
3. con.setRequestProperty("Accept-Language", "en-us,en;q=0.7,zh-cn;q=0.3");
4. con.setRequestProperty("Accept-Encoding", "aa");
5. con.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
6. con.setRequestProperty("Keep-Alive", "300");
7. con.setRequestProperty("Connection", "keep-alive");
8. con.setRequestProperty("If-Modified-Since", "Fri, 02 Jan 2009 17:00:05 GMT");
9. con.setRequestProperty("If-None-Match", "\"1261d8-4290-df64d224\"");
10. con.setRequestProperty("Cache-Control", "max-age=0");
11. con.setRequestProperty("Referer", "http://www.dianping.com");
12.}
private void setHeader(URLConnection con) {
con.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3");
con.setRequestProperty("Accept-Language", "en-us,en;q=0.7,zh-cn;q=0.3");
con.setRequestProperty("Accept-Encoding", "aa");
con.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
con.setRequestProperty("Keep-Alive", "300");
con.setRequestProperty("Connection", "keep-alive");
con.setRequestProperty("If-Modified-Since", "Fri, 02 Jan 2009 17:00:05 GMT");
con.setRequestProperty("If-None-Match", "\"1261d8-4290-df64d224\"");
con.setRequestProperty("Cache-Control", "max-age=0");
con.setRequestProperty("Referer", "http://www.dianping.com");
}
另外,为了避免线程因为网络原因而阻塞,设置了ConnectTimeout和ReadTimeout,代码④处。setConnectTimeout设置的连接的超时时间,而setReadTimeout设置的是读取数据的超时时间,发生超时的话,就会抛出socketTimeout异常,两个方法的参数都是超时的毫秒数。
这里对超时的发生,采用的是等候一段时间重新连接的方法。整个获取网络连接并读取下载数据的过程都包含在一个循环之中(代码③处),如果发生了连接或者读取数据的超时,在抛出的异常里面就会sleep一定的时间(代码⑩处),然后continue,再次尝试获取连接并读取数据,这个时间可以通过setSleepSeconds()方法来设置。我们在迅雷等下载工具的使用中,经常可以看到状态栏会输出类似“连接超时,等待*秒后重试”的话,这个就是通过ConnectTimeout,ReadTimeout来实现的。
连接建立好之后,我们要检查一下返回响应的状态码。常见的Http Response Code有以下几种:
a) 200 OK 一切正常,对GET和POST请求的应答文档跟在后面。
b) 206 Partial Content 客户发送了一个带有Range头的GET请求,服务器完成。
c) 404 Not Found 无法找到指定位置的资源。这也是一个常用的应答。
d) 414 Request URI Too Long URI太长。
e) 416 Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头。
f) 500 Internal Server Error 服务器遇到了意料不到的情况,不能完成客户的请求。
g) 503 Service Unavailable 服务器由于维护或者负载过重未能应答。例如,Servlet可能在数据库连接池已满的情况下返回503。
在这些状态里面,只有200与206才是我们需要的正确的状态。所以在代码⑤处,进行了状态码的判断,如果返回不符合要求的状态码,则结束线程,返回主线程并提示报错。
假设一切正常,下面我们就要考虑从网络中读数据了。正如我之前在分析mysql的数据库驱动中看的一样,网络中发送数据都是以数据包的形式来发送的,也就是说不管是客户端向服务器发出的请求数据,还是从服务器返回给客户端的响应数据,都会被拆分成若干个小型数据包在网络中传递,等数据包到达了目的地,网络接口会依据数据包的编号来组装它们,成为完整的比特数据。因此,我们可以想到在这里也是一样的,我们用inputStream的read方法来通过网卡从网络中读取数据,并不一定一次就能把所有的数据包都读完,所以我们要不断的循环来从inputStream中读取数据。Read方法有一个int型的返回值,表示每次从inputStream中读取的字节数,如果把这个inputStream中的数据读完了,那么就返回-1。
Read方法最多可以有三个参数,byte b[]是读取数据之后存放的目标数组,off标识了目标数组中存储的开始位置,len是想要读取的数据长度,这个长度必定不能大于b[]的长度。
public synchronized int read(byte b[], int off, int len);
我们的目标是要把目标地址的内容下载下来,现在分了5个线程来分段下载,那么这些分段下载的数据保存在哪里呢?如果把它们都保存在内存中是非常糟糕的做法,如果文件相当之大,例如是一个视频的话,难道把这么大的数据都放在内存中吗,这样的话,万一连接中断,那前面下载的东西就都没有了?我们当然要想办法及时的把下载的数据刷到磁盘上保存下来。当用bt下载视频的时候,通常都会有个临时文件,当视频完全下载结束之后,这个临时文件就会被删除,那么下次继续下载的时候,就会接着上次下载的点继续下载。所以我们的outputStream就是往这个临时文件来输出了。
OutputStream的write方法和上面InputStream的read方法有类似的参数,byte b[]是输出数据的来源,off标识了开始位置,len是数据长度。
public synchronized void write(byte b[], int off, int len) throws IOException;
在往临时文件的outputStream中写数据的时候,我会加上一个计数器,每满4096个比特就往文件中flush一下(代码⑦处)。
对于输出流的flush,有些要注意的地方,在程序中有三个地方调用了outputStream.flush()。第一个是在循环的读取网络数据并往outputStream中写入的时候,每满4096个byte就flush一下(代码⑦处);第二个是循环之后(代码⑧处),这时候正常的读取写入操作已经完成,但是outputStream中还有没有刷入磁盘的数据,所以要flush一下才能关闭连接;第三个就是在异常中的flush(代码⑨处),因为如果发生了连接超时或者读取数据超时的话,就会直接跑到catch的exception中去,这个时候outputStream中的数据如果不flush的话,重新连接的时候这部分数据就会丢失了。另外,当抛出异常,重新连接的时候,下载的起始位置也要重新设置,所以在代码⑥处,即每次从inputStream中读取数据之后,startPosition就要重新设置,count标识了已经下载的字节数。
3、现在每个分段的下载线程都顺利结束了,也都创建了相应的临时文件,接下来在主线程中会对临时文件进行合并,并写入目标文件,最后删除临时文件。这部分很简单,就是一个对所有下载线程进行遍历的过程。这里outputStream也有两次flush,与上面类似,不再赘述。
Java代码
1.private void tempFileToTargetFile(ChildThread[] childThreads) {
2. try {
3. BufferedOutputStream outputStream = new BufferedOutputStream(
4. new FileOutputStream(fileDir + fileName));
5.
6. // 遍历所有子线程创建的临时文件,按顺序把下载内容写入目标文件中
7. for (int i = 0; i < threadNum; i++) {
8. if (statusError) {
9. for (int k = 0; k < threadNum; k++) {
10. if (childThreads[k].tempFile.length() == 0)
11. childThreads[k].tempFile.delete();
12. }
13. System.out.println("本次下载任务不成功,请重新设置线程数。");
14. break;
15. }
16.
17. BufferedInputStream inputStream = new BufferedInputStream(
18. new FileInputStream(childThreads[i].tempFile));
19. System.out.println("Now is file " + childThreads[i].id);
20. int len = 0;
21. long count = 0;
22. byte[] b = new byte[1024];
23. while ((len = inputStream.read(b)) != -1) {
24. count += len;
25. outputStream.write(b, 0, len);
26. if ((count % 4096) == 0) {
27. outputStream.flush();
28. }
29.
30. // b = new byte[1024];
31. }
32.
33. inputStream.close();
34. // 删除临时文件
35. if (childThreads[i].status == ChildThread.STATUS_HAS_FINISHED) {
36. childThreads[i].tempFile.delete();
37. }
38. }
39.
40. outputStream.flush();
41. outputStream.close();
42. } catch (FileNotFoundException e) {
43. e.printStackTrace();
44. } catch (IOException e) {
45. e.printStackTrace();
46. }
47.}
private void tempFileToTargetFile(ChildThread[] childThreads) {
try {
BufferedOutputStream outputStream = new BufferedOutputStream(
new FileOutputStream(fileDir + fileName));
// 遍历所有子线程创建的临时文件,按顺序把下载内容写入目标文件中
for (int i = 0; i < threadNum; i++) {
if (statusError) {
for (int k = 0; k < threadNum; k++) {
if (childThreads[k].tempFile.length() == 0)
childThreads[k].tempFile.delete();
}
System.out.println("本次下载任务不成功,请重新设置线程数。");
break;
}
BufferedInputStream inputStream = new BufferedInputStream(
new FileInputStream(childThreads[i].tempFile));
System.out.println("Now is file " + childThreads[i].id);
int len = 0;
long count = 0;
byte[] b = new byte[1024];
while ((len = inputStream.read(b)) != -1) {
count += len;
outputStream.write(b, 0, len);
if ((count % 4096) == 0) {
outputStream.flush();
}
// b = new byte[1024];
}
inputStream.close();
// 删除临时文件
if (childThreads[i].status == ChildThread.STATUS_HAS_FINISHED) {
childThreads[i].tempFile.delete();
}
}
outputStream.flush();
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
4、最后,说说断点续传,前面为了实现断点续传,在每个下载线程中都创建了一个临时文件,现在我们就要利用这个临时文件来设置断点的位置。由于临时文件的命名方式都是固定的,所以我们就专门找对应下载的目标文件的临时文件,临时文件中已经下载的字节数就是我们需要的断点位置。startPos是一个数组,存放了每个线程的已下载的字节数。
Java代码
1.//第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。
2.private long[] setThreadBreakpoint(String fileDir2, String fileName2,
3. long contentLength, long[] startPos) {
4. File file = new File(fileDir + fileName);
5. long localFileSize = file.length();
6.
7. if (file.exists()) {
8. System.out.println("file " + fileName + " has exists!");
9. // 下载的目标文件已存在,判断目标文件是否完整
10. if (localFileSize < contentLength) {
11. System.out.println("Now download continue ... ");
12.
13. // 遍历目标文件的所有临时文件,设置断点的位置,即每个临时文件的长度
14. File tempFileDir = new File(fileDir);
15. File[] files = tempFileDir.listFiles();
16. for (int k = 0; k < files.length; k++) {
17. String tempFileName = files[k].getName();
18. // 临时文件的命名方式为:目标文件名+"_"+编号
19. if (tempFileName != null && files[k].length() > 0
20. && tempFileName.startsWith(fileName + "_")) {
21. int fileLongNum = Integer.parseInt(tempFileName
22. .substring(tempFileName.lastIndexOf("_") + 1,
23. tempFileName.lastIndexOf("_") + 2));
24. // 为每个线程设置已下载的位置
25. startPos[fileLongNum] = files[k].length();
26. }
27. }
28. }
29. } else {
30. // 如果下载的目标文件不存在,则创建新文件
31. try {
32. file.createNewFile();
33. } catch (IOException e) {
34. e.printStackTrace();
35. }
36. }
37.
38. return startPos;
39.}
//第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。
private long[] setThreadBreakpoint(String fileDir2, String fileName2,
long contentLength, long[] startPos) {
File file = new File(fileDir + fileName);
long localFileSize = file.length();
if (file.exists()) {
System.out.println("file " + fileName + " has exists!");
// 下载的目标文件已存在,判断目标文件是否完整
if (localFileSize < contentLength) {
System.out.println("Now download continue ... ");
// 遍历目标文件的所有临时文件,设置断点的位置,即每个临时文件的长度
File tempFileDir = new File(fileDir);
File[] files = tempFileDir.listFiles();
for (int k = 0; k < files.length; k++) {
String tempFileName = files[k].getName();
// 临时文件的命名方式为:目标文件名+"_"+编号
if (tempFileName != null && files[k].length() > 0
&& tempFileName.startsWith(fileName + "_")) {
int fileLongNum = Integer.parseInt(tempFileName
.substring(tempFileName.lastIndexOf("_") + 1,
tempFileName.lastIndexOf("_") + 2));
// 为每个线程设置已下载的位置
startPos[fileLongNum] = files[k].length();
}
}
}
} else {
// 如果下载的目标文件不存在,则创建新文件
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
return startPos;
}
5、测试
Java代码
1.public class DownloadStartup {
2. private static final String encoding = "utf-8";
3. public static void main(String[] args) {
4. DownloadTask downloadManager = new DownloadTask();
5. String urlStr = "http://apache.freelamp.com/velocity/tools/1.4/velocity-tools-1.4.zip";
6. downloadManager.setSleepSeconds(5);
7. downloadManager.download(urlStr, encoding);
8. }
9.}
public class DownloadStartup {
private static final String encoding = "utf-8";
public static void main(String[] args) {
DownloadTask downloadManager = new DownloadTask();
String urlStr = "http://apache.freelamp.com/velocity/tools/1.4/velocity-tools-1.4.zip";
downloadManager.setSleepSeconds(5);
downloadManager.download(urlStr, encoding);
}
}
测试从apache下载一个velocity的压缩包,临时文件保留,看一下下载结果:
另:在测试从skycn下载软件的过程中,碰到了一个错误:
java.io.IOException: Server returned HTTP response code: 416 for URL: http://www.skycn.com/
上网查了一下:416 Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头,于是把threadNum改为1就可以了。
这个下载功能现在只是完成了很基础的一部分,最初的初衷就是为了演练一下CountdownLatch。CountdownLatch就是一个计数器,就像一个拦截的栅栏,用await()方法来把栅栏关上,线程就跑不下去了,只有等计数器减为0的时候,栅栏才会自动打开,被暂停的线程才会继续运行。CountdownLatch的应用场景可以有很多,分段下载就是一个很好的例子。
* author:annegu
* date:2009-07-16
*/
annegu做了一个简单的Http多线程的下载程序,来讨论一下多线程并发下载以及断点续传的问题。
这个程序的功能,就是可以分多个线程从目标地址上下载数据,每个线程负责下载一部分,并可以支持断点续传和超时重连。
下载的方法是download(),它接收两个参数,分别是要下载的页面的url和编码方式。在这个负责下载的方法中,主要分了三个步骤。第一步是用来设置断点续传时候的一些信息的,第二步就是主要的分多线程来下载了,最后是数据的合并。
1、多线程下载:
Java代码
1. public String download(String urlStr, String charset) {
2. this.charset = charset;
3. long contentLength = 0;
4.① CountDownLatch latch = new CountDownLatch(threadNum);
5. long[] startPos = new long[threadNum];
6. long endPos = 0;
7.
8. try {
9. // 从url中获得下载的文件格式与名字
10. this.fileName = urlStr.substring(urlStr.lastIndexOf("/") + 1, urlStr.lastIndexOf("?")>0 ? urlStr.lastIndexOf("?") : urlStr.length());
11. if("".equalsIgnoreCase(this.fileName)){
12. this.fileName = UUID.randomUUID().toString();
13. }
14.
15. this.url = new URL(urlStr);
16. URLConnection con = url.openConnection();
17. setHeader(con);
18. // 得到content的长度
19. contentLength = con.getContentLength();
20. // 把context分为threadNum段的话,每段的长度。
21. this.threadLength = contentLength / threadNum;
22.
23. // 第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。在第4点中说明。
24. startPos = setThreadBreakpoint(fileDir, fileName, contentLength, startPos);
25.
26. //第二步,分多个线程下载文件
27. ExecutorService exec = Executors.newCachedThreadPool();
28. for (int i = 0; i < threadNum; i++) {
29. // 创建子线程来负责下载数据,每段数据的起始位置为(threadLength * i + 已下载长度)
30. startPos[i] += threadLength * i;
31.
32. /*设置子线程的终止位置,非最后一个线程即为(threadLength * (i + 1) - 1)
33. 最后一个线程的终止位置即为下载内容的长度*/
34. if (i == threadNum - 1) {
35. endPos = contentLength;
36. } else {
37. endPos = threadLength * (i + 1) - 1;
38. }
39. // 开启子线程,并执行。
40. ChildThread thread = new ChildThread(this, latch, i, startPos[i], endPos);
41. childThreads[i] = thread;
42. exec.execute(thread);
43. }
44.
45. try {
46. // 等待CountdownLatch信号为0,表示所有子线程都结束。
47.② latch.await();
48. exec.shutdown();
49.
50. // 第三步,把分段下载下来的临时文件中的内容写入目标文件中。在第3点中说明。
51. tempFileToTargetFile(childThreads);
52.
53. } catch (InterruptedException e) {
54. e.printStackTrace();
55. }
56. }
public String download(String urlStr, String charset) {
this.charset = charset;
long contentLength = 0;
① CountDownLatch latch = new CountDownLatch(threadNum);
long[] startPos = new long[threadNum];
long endPos = 0;
try {
// 从url中获得下载的文件格式与名字
this.fileName = urlStr.substring(urlStr.lastIndexOf("/") + 1, urlStr.lastIndexOf("?")>0 ? urlStr.lastIndexOf("?") : urlStr.length());
if("".equalsIgnoreCase(this.fileName)){
this.fileName = UUID.randomUUID().toString();
}
this.url = new URL(urlStr);
URLConnection con = url.openConnection();
setHeader(con);
// 得到content的长度
contentLength = con.getContentLength();
// 把context分为threadNum段的话,每段的长度。
this.threadLength = contentLength / threadNum;
// 第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。在第4点中说明。
startPos = setThreadBreakpoint(fileDir, fileName, contentLength, startPos);
//第二步,分多个线程下载文件
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) {
// 创建子线程来负责下载数据,每段数据的起始位置为(threadLength * i + 已下载长度)
startPos[i] += threadLength * i;
/*设置子线程的终止位置,非最后一个线程即为(threadLength * (i + 1) - 1)
最后一个线程的终止位置即为下载内容的长度*/
if (i == threadNum - 1) {
endPos = contentLength;
} else {
endPos = threadLength * (i + 1) - 1;
}
// 开启子线程,并执行。
ChildThread thread = new ChildThread(this, latch, i, startPos[i], endPos);
childThreads[i] = thread;
exec.execute(thread);
}
try {
// 等待CountdownLatch信号为0,表示所有子线程都结束。
② latch.await();
exec.shutdown();
// 第三步,把分段下载下来的临时文件中的内容写入目标文件中。在第3点中说明。
tempFileToTargetFile(childThreads);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
首先来看最主要的步骤:多线程下载。
首先从url中提取目标文件的名称,并在对应的目录创建文件。然后取得要下载的文件大小,根据分成的下载线程数量平均分配每个线程需要下载的数据量,就是threadLength。然后就可以分多个线程来进行下载任务了。
在这个例子中,并没有直接显示的创建Thread对象,而是用Executor来管理Thread对象,并且用CachedThreadPool来创建的线程池,当然也可以用FixedThreadPool。CachedThreadPool在程序执行的过程中会创建与所需数量相同的线程,当程序回收旧线程的时候就停止创建新线程。FixedThreadPool可以预先新建参数给定个数的线程,这样就不用在创建任务的时候再来创建线程了,可以直接从线程池中取出已准备好的线程。下载线程的数量是通过一个全局变量threadNum来控制的,默认为5。
好了,这5个子线程已经通过Executor来创建了,下面它们就会各自为政,互不干涉的执行了。线程有两种实现方式:实现Runnable接口;继承Thread类。
ChildThread就是子线程,它作为DownloadTask的内部类,继承了Thread,它的构造方法需要5个参数,依次是一个对DownloadTask的引用,一个CountDownLatch,id(标识线程的id号),startPosition(下载内容的开始位置),endPosition(下载内容的结束位置)。
这个CountDownLatch是做什么用的呢?
现在我们整理一下思路,要实现分多个线程来下载数据的话,我们肯定还要把这多个线程下载下来的数据进行合。主线程必须等待所有的子线程都执行结束之后,才能把所有子线程的下载数据按照各自的id顺序进行合并。CountDownLatch就是来做这个工作的。
CountDownLatch用来同步主线程,强制主线程等待所有的子线程执行的下载操作完成。在主线程中,CountDownLatch对象被设置了一个初始计数器,就是子线程的个数5个,代码①处。在新建了5个子线程并开始执行之后,主线程用CountDownLatch的await()方法来阻塞主线程,直到这个计数器的值到达0,才会进行下面的操作,代码②处。
对每个子线程来说,在执行完下载指定区间与长度的数据之后,必须通过调用CountDownLatch的countDown()方法来把这个计数器减1。
2、在全面开启下载任务之后,主线程就开始阻塞,等待子线程执行完毕,所以下面我们来看一下具体的下载线程ChildThread。
Java代码
1.public class ChildThread extends Thread {
2. public static final int STATUS_HASNOT_FINISHED = 0;
3. public static final int STATUS_HAS_FINISHED = 1;
4. public static final int STATUS_HTTPSTATUS_ERROR = 2;
5. private DownloadTask task;
6. private int id;
7. private long startPosition;
8. private long endPosition;
9. private final CountDownLatch latch;
10. // private RandomAccessFile tempFile = null;
11. private File tempFile = null;
12. //线程状态码
13. private int status = ChildThread.STATUS_HASNOT_FINISHED;
14.
15. public ChildThread(DownloadTask task, CountDownLatch latch, int id, long startPos, long endPos) {
16. super();
17. this.task = task;
18. this.id = id;
19. this.startPosition = startPos;
20. this.endPosition = endPos;
21. this.latch = latch;
22.
23. try {
24. tempFile = new File(this.task.fileDir + this.task.fileName + "_" + id);
25. if(!tempFile.exists()){
26. tempFile.createNewFile();
27. }
28. } catch (IOException e) {
29. e.printStackTrace();
30. }
31.
32. }
33.
34. public void run() {
35. System.out.println("Thread " + id + " run ...");
36. HttpURLConnection con = null;
37. InputStream inputStream = null;
38. BufferedOutputStream outputStream = null;
39. long count = 0;
40. long threadDownloadLength = endPosition - startPosition;
41.
42. try {
43. outputStream = new BufferedOutputStream(new FileOutputStream(tempFile.getPath(), true));
44. } catch (FileNotFoundException e2) {
45. e2.printStackTrace();
46. }
47.
48.③ for(int k = 0; k < 10; k++){
49. if(k > 0)
50. System.out.println("Now thread " + id + "is reconnect, start position is " + startPosition);
51. try {
52. //打开URLConnection
53. con = (HttpURLConnection) task.url.openConnection();
54. setHeader(con);
55. con.setAllowUserInteraction(true);
56. //设置连接超时时间为10000ms
57.④ con.setConnectTimeout(10000);
58. //设置读取数据超时时间为10000ms
59. con.setReadTimeout(10000);
60.
61. if(startPosition < endPosition){
62. //设置下载数据的起止区间
63. con.setRequestProperty("Range", "bytes=" + startPosition + "-"
64. + endPosition);
65. System.out.println("Thread " + id + " startPosition is " + startPosition);
66. System.out.println("Thread " + id + " endPosition is " + endPosition);
67.
68. //判断http status是否为HTTP/1.1 206 Partial Content或者200 OK
69. //如果不是以上两种状态,把status改为STATUS_HTTPSTATUS_ERROR
70.⑤ if (con.getResponseCode() != HttpURLConnection.HTTP_OK
71. && con.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
72. System.out.println("Thread " + id + ": code = "
73. + con.getResponseCode() + ", status = "
74. + con.getResponseMessage());
75. status = ChildThread.STATUS_HTTPSTATUS_ERROR;
76. this.task.statusError = true;
77. outputStream.close();
78. con.disconnect();
79. System.out.println("Thread " + id + " finished.");
80. latch.countDown();
81. break;
82. }
83.
84. inputStream = con.getInputStream();
85.
86. int len = 0;
87. byte[] b = new byte[1024];
88. while ((len = inputStream.read(b)) != -1) {
89. outputStream.write(b, 0, len);
90. count += len;
91.⑥ startPosition += len;
92. //每读满4096个byte(一个内存页),往磁盘上flush一下
93. if(count % 4096 == 0){
94.⑦ outputStream.flush();
95. }
96. }
97.
98. System.out.println("count is " + count);
99. if (count >= threadDownloadLength) {
100. status = ChildThread.STATUS_HAS_FINISHED;
101. }
102.⑧ outputStream.flush();
103. outputStream.close();
104. inputStream.close();
105. con.disconnect();
106. } else {
107. status = ChildThread.STATUS_HAS_FINISHED;
108. }
109.
110. System.out.println("Thread " + id + " finished.");
111. latch.countDown();
112. break;
113. } catch (IOException e) {
114. try {
115.⑨ outputStream.flush();
116.⑩ TimeUnit.SECONDS.sleep(getSleepSeconds());
117. } catch (InterruptedException e1) {
118. e1.printStackTrace();
119. } catch (IOException e2) {
120. e2.printStackTrace();
121. }
122. continue;
123. }
124. }
125. }
126.}
public class ChildThread extends Thread {
public static final int STATUS_HASNOT_FINISHED = 0;
public static final int STATUS_HAS_FINISHED = 1;
public static final int STATUS_HTTPSTATUS_ERROR = 2;
private DownloadTask task;
private int id;
private long startPosition;
private long endPosition;
private final CountDownLatch latch;
// private RandomAccessFile tempFile = null;
private File tempFile = null;
//线程状态码
private int status = ChildThread.STATUS_HASNOT_FINISHED;
public ChildThread(DownloadTask task, CountDownLatch latch, int id, long startPos, long endPos) {
super();
this.task = task;
this.id = id;
this.startPosition = startPos;
this.endPosition = endPos;
this.latch = latch;
try {
tempFile = new File(this.task.fileDir + this.task.fileName + "_" + id);
if(!tempFile.exists()){
tempFile.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void run() {
System.out.println("Thread " + id + " run ...");
HttpURLConnection con = null;
InputStream inputStream = null;
BufferedOutputStream outputStream = null;
long count = 0;
long threadDownloadLength = endPosition - startPosition;
try {
outputStream = new BufferedOutputStream(new FileOutputStream(tempFile.getPath(), true));
} catch (FileNotFoundException e2) {
e2.printStackTrace();
}
③ for(int k = 0; k < 10; k++){
if(k > 0)
System.out.println("Now thread " + id + "is reconnect, start position is " + startPosition);
try {
//打开URLConnection
con = (HttpURLConnection) task.url.openConnection();
setHeader(con);
con.setAllowUserInteraction(true);
//设置连接超时时间为10000ms
④ con.setConnectTimeout(10000);
//设置读取数据超时时间为10000ms
con.setReadTimeout(10000);
if(startPosition < endPosition){
//设置下载数据的起止区间
con.setRequestProperty("Range", "bytes=" + startPosition + "-"
+ endPosition);
System.out.println("Thread " + id + " startPosition is " + startPosition);
System.out.println("Thread " + id + " endPosition is " + endPosition);
//判断http status是否为HTTP/1.1 206 Partial Content或者200 OK
//如果不是以上两种状态,把status改为STATUS_HTTPSTATUS_ERROR
⑤ if (con.getResponseCode() != HttpURLConnection.HTTP_OK
&& con.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
System.out.println("Thread " + id + ": code = "
+ con.getResponseCode() + ", status = "
+ con.getResponseMessage());
status = ChildThread.STATUS_HTTPSTATUS_ERROR;
this.task.statusError = true;
outputStream.close();
con.disconnect();
System.out.println("Thread " + id + " finished.");
latch.countDown();
break;
}
inputStream = con.getInputStream();
int len = 0;
byte[] b = new byte[1024];
while ((len = inputStream.read(b)) != -1) {
outputStream.write(b, 0, len);
count += len;
⑥ startPosition += len;
//每读满4096个byte(一个内存页),往磁盘上flush一下
if(count % 4096 == 0){
⑦ outputStream.flush();
}
}
System.out.println("count is " + count);
if (count >= threadDownloadLength) {
status = ChildThread.STATUS_HAS_FINISHED;
}
⑧ outputStream.flush();
outputStream.close();
inputStream.close();
con.disconnect();
} else {
status = ChildThread.STATUS_HAS_FINISHED;
}
System.out.println("Thread " + id + " finished.");
latch.countDown();
break;
} catch (IOException e) {
try {
⑨ outputStream.flush();
⑩ TimeUnit.SECONDS.sleep(getSleepSeconds());
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (IOException e2) {
e2.printStackTrace();
}
continue;
}
}
}
}
在ChildThread的构造方法中,除了设置一些从主线程中带来的id, 起始位置之外,就是新建了一个临时文件用来存放当前线程的下载数据。临时文件的命名规则是这样的:下载的目标文件名+”_”+线程编号。
现在让我们来看看从网络中读数据是怎么读的。我们通过URLConnection来获得一个http的连接。有些网站为了安全起见,会对请求的http连接进行过滤,因此为了伪装这个http的连接请求,我们给httpHeader穿一件伪装服。下面的setHeader方法展示了一些非常常用的典型的httpHeader的伪装方法。比较重要的有:Uer-Agent模拟从Ubuntu的firefox浏览器发出的请求;Referer模拟浏览器请求的前一个触发页面,例如从skycn站点来下载软件的话,Referer设置成skycn的首页域名就可以了;Range就是这个连接获取的流文件的起始区间。
Java代码
1.private void setHeader(URLConnection con) {
2. con.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3");
3. con.setRequestProperty("Accept-Language", "en-us,en;q=0.7,zh-cn;q=0.3");
4. con.setRequestProperty("Accept-Encoding", "aa");
5. con.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
6. con.setRequestProperty("Keep-Alive", "300");
7. con.setRequestProperty("Connection", "keep-alive");
8. con.setRequestProperty("If-Modified-Since", "Fri, 02 Jan 2009 17:00:05 GMT");
9. con.setRequestProperty("If-None-Match", "\"1261d8-4290-df64d224\"");
10. con.setRequestProperty("Cache-Control", "max-age=0");
11. con.setRequestProperty("Referer", "http://www.dianping.com");
12.}
private void setHeader(URLConnection con) {
con.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3");
con.setRequestProperty("Accept-Language", "en-us,en;q=0.7,zh-cn;q=0.3");
con.setRequestProperty("Accept-Encoding", "aa");
con.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
con.setRequestProperty("Keep-Alive", "300");
con.setRequestProperty("Connection", "keep-alive");
con.setRequestProperty("If-Modified-Since", "Fri, 02 Jan 2009 17:00:05 GMT");
con.setRequestProperty("If-None-Match", "\"1261d8-4290-df64d224\"");
con.setRequestProperty("Cache-Control", "max-age=0");
con.setRequestProperty("Referer", "http://www.dianping.com");
}
另外,为了避免线程因为网络原因而阻塞,设置了ConnectTimeout和ReadTimeout,代码④处。setConnectTimeout设置的连接的超时时间,而setReadTimeout设置的是读取数据的超时时间,发生超时的话,就会抛出socketTimeout异常,两个方法的参数都是超时的毫秒数。
这里对超时的发生,采用的是等候一段时间重新连接的方法。整个获取网络连接并读取下载数据的过程都包含在一个循环之中(代码③处),如果发生了连接或者读取数据的超时,在抛出的异常里面就会sleep一定的时间(代码⑩处),然后continue,再次尝试获取连接并读取数据,这个时间可以通过setSleepSeconds()方法来设置。我们在迅雷等下载工具的使用中,经常可以看到状态栏会输出类似“连接超时,等待*秒后重试”的话,这个就是通过ConnectTimeout,ReadTimeout来实现的。
连接建立好之后,我们要检查一下返回响应的状态码。常见的Http Response Code有以下几种:
a) 200 OK 一切正常,对GET和POST请求的应答文档跟在后面。
b) 206 Partial Content 客户发送了一个带有Range头的GET请求,服务器完成。
c) 404 Not Found 无法找到指定位置的资源。这也是一个常用的应答。
d) 414 Request URI Too Long URI太长。
e) 416 Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头。
f) 500 Internal Server Error 服务器遇到了意料不到的情况,不能完成客户的请求。
g) 503 Service Unavailable 服务器由于维护或者负载过重未能应答。例如,Servlet可能在数据库连接池已满的情况下返回503。
在这些状态里面,只有200与206才是我们需要的正确的状态。所以在代码⑤处,进行了状态码的判断,如果返回不符合要求的状态码,则结束线程,返回主线程并提示报错。
假设一切正常,下面我们就要考虑从网络中读数据了。正如我之前在分析mysql的数据库驱动中看的一样,网络中发送数据都是以数据包的形式来发送的,也就是说不管是客户端向服务器发出的请求数据,还是从服务器返回给客户端的响应数据,都会被拆分成若干个小型数据包在网络中传递,等数据包到达了目的地,网络接口会依据数据包的编号来组装它们,成为完整的比特数据。因此,我们可以想到在这里也是一样的,我们用inputStream的read方法来通过网卡从网络中读取数据,并不一定一次就能把所有的数据包都读完,所以我们要不断的循环来从inputStream中读取数据。Read方法有一个int型的返回值,表示每次从inputStream中读取的字节数,如果把这个inputStream中的数据读完了,那么就返回-1。
Read方法最多可以有三个参数,byte b[]是读取数据之后存放的目标数组,off标识了目标数组中存储的开始位置,len是想要读取的数据长度,这个长度必定不能大于b[]的长度。
public synchronized int read(byte b[], int off, int len);
我们的目标是要把目标地址的内容下载下来,现在分了5个线程来分段下载,那么这些分段下载的数据保存在哪里呢?如果把它们都保存在内存中是非常糟糕的做法,如果文件相当之大,例如是一个视频的话,难道把这么大的数据都放在内存中吗,这样的话,万一连接中断,那前面下载的东西就都没有了?我们当然要想办法及时的把下载的数据刷到磁盘上保存下来。当用bt下载视频的时候,通常都会有个临时文件,当视频完全下载结束之后,这个临时文件就会被删除,那么下次继续下载的时候,就会接着上次下载的点继续下载。所以我们的outputStream就是往这个临时文件来输出了。
OutputStream的write方法和上面InputStream的read方法有类似的参数,byte b[]是输出数据的来源,off标识了开始位置,len是数据长度。
public synchronized void write(byte b[], int off, int len) throws IOException;
在往临时文件的outputStream中写数据的时候,我会加上一个计数器,每满4096个比特就往文件中flush一下(代码⑦处)。
对于输出流的flush,有些要注意的地方,在程序中有三个地方调用了outputStream.flush()。第一个是在循环的读取网络数据并往outputStream中写入的时候,每满4096个byte就flush一下(代码⑦处);第二个是循环之后(代码⑧处),这时候正常的读取写入操作已经完成,但是outputStream中还有没有刷入磁盘的数据,所以要flush一下才能关闭连接;第三个就是在异常中的flush(代码⑨处),因为如果发生了连接超时或者读取数据超时的话,就会直接跑到catch的exception中去,这个时候outputStream中的数据如果不flush的话,重新连接的时候这部分数据就会丢失了。另外,当抛出异常,重新连接的时候,下载的起始位置也要重新设置,所以在代码⑥处,即每次从inputStream中读取数据之后,startPosition就要重新设置,count标识了已经下载的字节数。
3、现在每个分段的下载线程都顺利结束了,也都创建了相应的临时文件,接下来在主线程中会对临时文件进行合并,并写入目标文件,最后删除临时文件。这部分很简单,就是一个对所有下载线程进行遍历的过程。这里outputStream也有两次flush,与上面类似,不再赘述。
Java代码
1.private void tempFileToTargetFile(ChildThread[] childThreads) {
2. try {
3. BufferedOutputStream outputStream = new BufferedOutputStream(
4. new FileOutputStream(fileDir + fileName));
5.
6. // 遍历所有子线程创建的临时文件,按顺序把下载内容写入目标文件中
7. for (int i = 0; i < threadNum; i++) {
8. if (statusError) {
9. for (int k = 0; k < threadNum; k++) {
10. if (childThreads[k].tempFile.length() == 0)
11. childThreads[k].tempFile.delete();
12. }
13. System.out.println("本次下载任务不成功,请重新设置线程数。");
14. break;
15. }
16.
17. BufferedInputStream inputStream = new BufferedInputStream(
18. new FileInputStream(childThreads[i].tempFile));
19. System.out.println("Now is file " + childThreads[i].id);
20. int len = 0;
21. long count = 0;
22. byte[] b = new byte[1024];
23. while ((len = inputStream.read(b)) != -1) {
24. count += len;
25. outputStream.write(b, 0, len);
26. if ((count % 4096) == 0) {
27. outputStream.flush();
28. }
29.
30. // b = new byte[1024];
31. }
32.
33. inputStream.close();
34. // 删除临时文件
35. if (childThreads[i].status == ChildThread.STATUS_HAS_FINISHED) {
36. childThreads[i].tempFile.delete();
37. }
38. }
39.
40. outputStream.flush();
41. outputStream.close();
42. } catch (FileNotFoundException e) {
43. e.printStackTrace();
44. } catch (IOException e) {
45. e.printStackTrace();
46. }
47.}
private void tempFileToTargetFile(ChildThread[] childThreads) {
try {
BufferedOutputStream outputStream = new BufferedOutputStream(
new FileOutputStream(fileDir + fileName));
// 遍历所有子线程创建的临时文件,按顺序把下载内容写入目标文件中
for (int i = 0; i < threadNum; i++) {
if (statusError) {
for (int k = 0; k < threadNum; k++) {
if (childThreads[k].tempFile.length() == 0)
childThreads[k].tempFile.delete();
}
System.out.println("本次下载任务不成功,请重新设置线程数。");
break;
}
BufferedInputStream inputStream = new BufferedInputStream(
new FileInputStream(childThreads[i].tempFile));
System.out.println("Now is file " + childThreads[i].id);
int len = 0;
long count = 0;
byte[] b = new byte[1024];
while ((len = inputStream.read(b)) != -1) {
count += len;
outputStream.write(b, 0, len);
if ((count % 4096) == 0) {
outputStream.flush();
}
// b = new byte[1024];
}
inputStream.close();
// 删除临时文件
if (childThreads[i].status == ChildThread.STATUS_HAS_FINISHED) {
childThreads[i].tempFile.delete();
}
}
outputStream.flush();
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
4、最后,说说断点续传,前面为了实现断点续传,在每个下载线程中都创建了一个临时文件,现在我们就要利用这个临时文件来设置断点的位置。由于临时文件的命名方式都是固定的,所以我们就专门找对应下载的目标文件的临时文件,临时文件中已经下载的字节数就是我们需要的断点位置。startPos是一个数组,存放了每个线程的已下载的字节数。
Java代码
1.//第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。
2.private long[] setThreadBreakpoint(String fileDir2, String fileName2,
3. long contentLength, long[] startPos) {
4. File file = new File(fileDir + fileName);
5. long localFileSize = file.length();
6.
7. if (file.exists()) {
8. System.out.println("file " + fileName + " has exists!");
9. // 下载的目标文件已存在,判断目标文件是否完整
10. if (localFileSize < contentLength) {
11. System.out.println("Now download continue ... ");
12.
13. // 遍历目标文件的所有临时文件,设置断点的位置,即每个临时文件的长度
14. File tempFileDir = new File(fileDir);
15. File[] files = tempFileDir.listFiles();
16. for (int k = 0; k < files.length; k++) {
17. String tempFileName = files[k].getName();
18. // 临时文件的命名方式为:目标文件名+"_"+编号
19. if (tempFileName != null && files[k].length() > 0
20. && tempFileName.startsWith(fileName + "_")) {
21. int fileLongNum = Integer.parseInt(tempFileName
22. .substring(tempFileName.lastIndexOf("_") + 1,
23. tempFileName.lastIndexOf("_") + 2));
24. // 为每个线程设置已下载的位置
25. startPos[fileLongNum] = files[k].length();
26. }
27. }
28. }
29. } else {
30. // 如果下载的目标文件不存在,则创建新文件
31. try {
32. file.createNewFile();
33. } catch (IOException e) {
34. e.printStackTrace();
35. }
36. }
37.
38. return startPos;
39.}
//第一步,分析已下载的临时文件,设置断点,如果是新的下载任务,则建立目标文件。
private long[] setThreadBreakpoint(String fileDir2, String fileName2,
long contentLength, long[] startPos) {
File file = new File(fileDir + fileName);
long localFileSize = file.length();
if (file.exists()) {
System.out.println("file " + fileName + " has exists!");
// 下载的目标文件已存在,判断目标文件是否完整
if (localFileSize < contentLength) {
System.out.println("Now download continue ... ");
// 遍历目标文件的所有临时文件,设置断点的位置,即每个临时文件的长度
File tempFileDir = new File(fileDir);
File[] files = tempFileDir.listFiles();
for (int k = 0; k < files.length; k++) {
String tempFileName = files[k].getName();
// 临时文件的命名方式为:目标文件名+"_"+编号
if (tempFileName != null && files[k].length() > 0
&& tempFileName.startsWith(fileName + "_")) {
int fileLongNum = Integer.parseInt(tempFileName
.substring(tempFileName.lastIndexOf("_") + 1,
tempFileName.lastIndexOf("_") + 2));
// 为每个线程设置已下载的位置
startPos[fileLongNum] = files[k].length();
}
}
}
} else {
// 如果下载的目标文件不存在,则创建新文件
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
return startPos;
}
5、测试
Java代码
1.public class DownloadStartup {
2. private static final String encoding = "utf-8";
3. public static void main(String[] args) {
4. DownloadTask downloadManager = new DownloadTask();
5. String urlStr = "http://apache.freelamp.com/velocity/tools/1.4/velocity-tools-1.4.zip";
6. downloadManager.setSleepSeconds(5);
7. downloadManager.download(urlStr, encoding);
8. }
9.}
public class DownloadStartup {
private static final String encoding = "utf-8";
public static void main(String[] args) {
DownloadTask downloadManager = new DownloadTask();
String urlStr = "http://apache.freelamp.com/velocity/tools/1.4/velocity-tools-1.4.zip";
downloadManager.setSleepSeconds(5);
downloadManager.download(urlStr, encoding);
}
}
测试从apache下载一个velocity的压缩包,临时文件保留,看一下下载结果:
另:在测试从skycn下载软件的过程中,碰到了一个错误:
java.io.IOException: Server returned HTTP response code: 416 for URL: http://www.skycn.com/
上网查了一下:416 Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头,于是把threadNum改为1就可以了。
这个下载功能现在只是完成了很基础的一部分,最初的初衷就是为了演练一下CountdownLatch。CountdownLatch就是一个计数器,就像一个拦截的栅栏,用await()方法来把栅栏关上,线程就跑不下去了,只有等计数器减为0的时候,栅栏才会自动打开,被暂停的线程才会继续运行。CountdownLatch的应用场景可以有很多,分段下载就是一个很好的例子。
- download.rar (4 KB)
- 下载次数: 3
- download2.rar (3.5 KB)
- 下载次数: 3
相关推荐
全国大学生智能汽车竞赛自2006年起,由教育部高等教育司委托高等学校自动化类教学指导委员会举办,旨在加强学生实践、创新能力和培养团队精神的一项创意性科技竞赛。该竞赛至今已成功举办多届,吸引了众多高校学生的积极参与,此文件为智能车竞赛介绍
字卡v4.3.4 原版 三种UI+关键字卡控制+支持获取用户信息+支持强制关注 集卡模块从一开始的版本到助力版本再到现在的新规则版本。 集卡模块难度主要在于 如何控制各种不同的字卡组合 被粉丝集齐的数量。 如果不控制那么一定会出现超过数量的粉丝集到指定的字卡组合,造成奖品不够的混乱,如果大奖价值高的话,超过数量的粉丝集到大奖后,就造成商家的活动费用超支了。我们冥思苦想如何才能限制集到指定字卡组合的粉丝数,后我们想到了和支付宝一样的选一张关键字卡来进行规则设置的方式来进行限制,根据奖品所需的关键字卡数,设定规则就可以控制每种奖品所需字卡组合被粉丝集到的数量,规则可以在活动进行中根据需要进行修改,活动规则灵活度高。新版的集卡规则,在此次政府发布号的活动中经受了考验,集到指定字卡组合的粉丝没有超出规则限制。有了这个规则限制后,您无需盯着活动,建好活动后就无人值守让活动进行就行了,您只需要时不时来看下蹭蹭上涨的活动数据即可。 被封? 无需担心,模块内置有防封功能,支持隐藏主域名,显示炮灰域名,保护活动安全进行。 活动准备? 只需要您有一个认证服务号即可,支持订阅号借用认证服务号来做活动。如果您
出口设备线体程序详解:PLC通讯下的V90控制与开源FB284工艺对象实战指南,出口设备线体程序详解:PLC通讯与V90控制集成,工艺对象与FB284协同工作,开源学习V90控制技能,出口设备1200线体程序,多个plc走通讯,内部有多个v90,采用工艺对象与fb284 共同控制,功能快全部开源,能快速学会v90的控制 ,出口设备; 1200线体程序; PLC通讯; 多个V90; 工艺对象; FB284; 功能开源; V90控制。,V90工艺控制:开源功能快,快速掌握1200线体程序与PLC通讯
基于Arduino与DAC8031的心电信号模拟器资料:心电信号与正弦波的双重输出应用方案,Arduino与DAC8031心电信号模拟器:生成心电信号与正弦波输出功能详解,基于arduino +DAC8031的心电信号模拟器资料,可输出心电信号,和正弦波 ,基于Arduino;DAC8031;心电信号模拟器;输出心电信号;正弦波输出;模拟器资料,基于Arduino与DAC8031的心电信号模拟器:输出心电与正弦波
MATLAB口罩检测的基本流程 图像采集:通过摄像头或其他图像采集设备获取包含面部的图像。 图像预处理:对采集到的图像进行灰度化、去噪、直方图均衡化等预处理操作,以提高图像质量,便于后续的人脸检测和口罩检测。 人脸检测:利用Haar特征、LBP特征等经典方法或深度学习模型(如MTCNN、FaceBoxes等)在预处理后的图像中定位人脸区域。 口罩检测:在检测到的人脸区域内,进一步分析是否佩戴口罩。这可以通过检测口罩的边缘、纹理等特征,或使用已经训练好的口罩检测模型来实现。 结果输出:将检测结果以可视化方式展示,如在图像上标注人脸和口罩区域,或输出文字提示是否佩戴口罩。
1、文件内容:kernel-debug-devel-3.10.0-1160.119.1.el7.rpm以及相关依赖 2、文件形式:tar.gz压缩包 3、安装指令: #Step1、解压 tar -zxvf /mnt/data/output/kernel-debug-devel-3.10.0-1160.119.1.el7.tar.gz #Step2、进入解压后的目录,执行安装 sudo rpm -ivh *.rpm 4、更多资源/技术支持:公众号禅静编程坊
该文档提供了一个关于供应链管理系统开发的详细指南,重点介绍了项目安排、技术实现和框架搭建的相关内容。 文档分为以下几个关键部分: 项目安排:主要步骤包括搭建框架(1天),基础数据模块和权限管理(4天),以及应收应付和销售管理(5天)。 供应链概念:供应链系统的核心流程是通过采购商品放入仓库,并在销售时从仓库提取商品,涉及三个主要订单:采购订单、销售订单和调拨订单。 大数据的应用:介绍了数据挖掘、ETL(数据抽取)和BI(商业智能)在供应链管理中的应用。 技术实现:讲述了DAO(数据访问对象)的重用、服务层的重用、以及前端JS的继承机制、jQuery插件开发等技术细节。 系统框架搭建:包括Maven环境的配置、Web工程的创建、持久化类和映射文件的编写,以及Spring配置文件的实现。 DAO的需求和功能:供应链管理系统的各个模块都涉及分页查询、条件查询、删除、增加、修改操作等需求。 泛型的应用:通过示例说明了在Java语言中如何使用泛型来实现模块化和可扩展性。 文档非常技术导向,适合开发人员参考,用于构建供应链管理系统的架构和功能模块。
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
C#与VB实现欧姆龙PLC的Fins TCP通信案例源码:调用动态链接库进行数据读写,定时器与计数器数据区的简洁读写操作示例,C#与VB实现欧姆龙PLC的Fins TCP通信案例源码:调用动态链接库进行读写操作,涵盖定时器计数器数据区学习案例,C#欧姆龙plc Fins Tcp通信案例上位机源码,有c#和VB的Demo,c#上位机和欧姆龙plc通讯案例源码,调用动态链接库,可以实现上位机的数据连接,可以简单实现D区W区定时器计数器等数据区的读写,是一个非常好的学习案例 ,C#; 欧姆龙PLC; Fins Tcp通信; 上位机源码; 动态链接库; 数据连接; D区W区读写; 定时器计数器; 学习案例,C#实现欧姆龙PLC Fins Tcp通信上位机源码,读写数据区高效学习案例
可调谐石墨烯超材料吸收体的FDTD仿真模拟研究报告:吸收光谱的化学势调节策略与仿真源文件解析,可调谐石墨烯超材料吸收体:化学势调节光谱的FDTD仿真模拟研究,可调谐石墨烯超材料吸收体FDTD仿真模拟 【案例内容】该案例提供了一种可调谐石墨烯超材料吸收体,其吸收光谱可以通过改变施加于石墨烯的化学势来进行调节。 【案例文件】仿真源文件 ,可调谐石墨烯超材料吸收体; FDTD仿真模拟; 化学势调节; 仿真源文件,石墨烯超材料吸收体:FDTD仿真调节吸收光谱案例解析
RBF神经网络控制仿真-第二版
松下PLC与威纶通触摸屏转盘设备控制:FPWINPRO7与EBPRO智能编程与宏指令应用,松下PLC与威纶通触摸屏转盘设备控制解决方案:FPWINPRO7与EBPRO协同工作,实现多工位转盘加工与IEC编程模式控制,松下PLC+威纶通触摸屏的转盘设备 松下PLC工程使用程序版本为FPWINPRO7 7.6.0.0版本 威纶通HMI工程使用程序版本为EBPRO 6.07.02.410S 1.多工位转盘加工控制。 2.国际标准IEC编程模式。 3.触摸屏宏指令应用控制。 ,松下PLC; 威纶通触摸屏; 转盘设备控制; 多工位加工控制; IEC编程模式; 触摸屏宏指令应用,松下PLC与威纶通HMI联控的转盘设备控制程序解析
基于循环神经网络(RNN)的多输入单输出预测模型(适用于时间序列预测与回归分析,需Matlab 2021及以上版本),基于循环神经网络(RNN)的多输入单输出预测模型(matlab版本2021+),真实值与预测值对比,多种评价指标与线性拟合展示。,RNN预测模型做多输入单输出预测模型,直接替数据就可以用。 程序语言是matlab,需求最低版本为2021及以上。 程序可以出真实值和预测值对比图,线性拟合图,可打印多种评价指标。 PS:以下效果图为测试数据的效果图,主要目的是为了显示程序运行可以出的结果图,具体预测效果以个人的具体数据为准。 2.由于每个人的数据都是独一无二的,因此无法做到可以任何人的数据直接替就可以得到自己满意的效果。 这段程序主要是一个基于循环神经网络(RNN)的预测模型。它的应用领域可以是时间序列预测、回归分析等。下面我将对程序的运行过程进行详细解释和分析。 首先,程序开始时清空环境变量、关闭图窗、清空变量和命令行。然后,通过xlsread函数导入数据,其中'数据的输入'和'数据的输出'是两个Excel文件的文件名。 接下来,程序对数据进行归一化处理。首先使用ma
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
旅游管理系统中的功能模块主要是实现管理员;首页、个人中心、用户管理、旅游方案管理、旅游购买管理、系统管理,用户;首页、个人中心、旅游方案管理、旅游购买管理、我的收藏管理。前台首页;首页、旅游方案、旅游资讯、个人中心、后台管理等功能。经过认真细致的研究,精心准备和规划,最后测试成功,系统可以正常使用。分析功能调整与旅游管理系统实现的实际需求相结合,讨论了Java开发旅游管理系统的使用。 从上面的描述中可以基本可以实现软件的功能: 1、开发实现旅游管理系统的整个系统程序; 2、管理员;首页、个人中心、用户管理、旅游方案管理、旅游购买管理、系统管理等。 3、用户:首页、个人中心、旅游方案管理、旅游购买管理、我的收藏管理。 4、前台首页:首页、旅游方案、旅游资讯、个人中心、后台管理等相应操作; 5、基础数据管理:实现系统基本信息的添加、修改及删除等操作,并且根据需求进行交流查看及回复相应操作。
Boost二级升压光伏并网结构的Simulink建模与MPPT最大功率点追踪:基于功率反馈的扰动观察法调整电压方向研究,Boost二级升压光伏并网结构的Simulink建模与MPPT最大功率点追踪:基于功率反馈的扰动观察法调整电压方向研究,Boost二级升压光伏并网结构,Simulink建模,MPPT最大功率点追踪,扰动观察法采用功率反馈方式,若ΔP>0,说明电压调整的方向正确,可以继续按原方向进行“干扰”;若ΔP<0,说明电压调整的方向错误,需要对“干扰”的方向进行改变。 ,Boost升压;光伏并网结构;Simulink建模;MPPT最大功率点追踪;扰动观察法;功率反馈;电压调整方向。,光伏并网结构中Boost升压MPPT控制策略的Simulink建模与功率反馈扰动观察法
运行GUI版本,可二开
Deepseek相关主题资源及行业影响
WP Smush Pro 是一款专为 WordPress 网站设计的图像优化插件。 一、主要作用 图像压缩 它能够在不影响图像质量的前提下,大幅度减小图像文件的大小。例如,对于一些高分辨率的产品图片或者风景照片,它可以通过先进的压缩算法,去除图像中多余的数据。通常 JPEG 格式的图像经过压缩后,文件大小可以减少 40% – 70% 左右。这对于网站性能优化非常关键,因为较小的图像文件可以加快网站的加载速度。 该插件支持多种图像格式的压缩,包括 JPEG、PNG 和 GIF。对于 PNG 图像,它可以在保留透明度等关键特性的同时,有效地减小文件尺寸。对于 GIF 图像,也能在一定程度上优化文件大小,减少动画 GIF 的加载时间。 懒加载 WP Smush Pro 实现了图像懒加载功能。懒加载是一种延迟加载图像的技术,当用户滚动页面到包含图像的位置时,图像才会加载。这样可以避免一次性加载大量图像,尤其是在页面内容较多且包含许多图像的情况下。例如,在一个新闻网站的长文章页面,带有大量配图,懒加载可以让用户在浏览文章开头部分时,不需要等待所有图片加载,从而提高页面的初始加载速度,同时也能
Could not create share link. Missing file: C:\Users\xx\.conda\envs\omni\Lib\site-packages\gradio\frpc_windows_amd64_v0.3 1. Download this file: https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_windows_amd64.exe 2. Rename the downloaded file to: frpc_windows_amd64_v0.3 3. Move the file to this location: C:\Users\xx\.conda\envs\omni\Lib\site-packages\gradio