多线程下载技术是很常见的一种下载方案,这种方式充分利用了多线程的优势,在同一时间段内通过多个线程发起下载请求,将需要下载的数据分割成多个部分,每一个线程只负责下载其中一个部分,然后将下载后的数据组装成完整的数据文件,这样便大大加快了下载效率。
原理很清楚,但是其中涉及到几个关键的问题:
- 1.需要请求的数据如何分段。
- 2.分段下载的数据如何组装成完整的数据文件。
- 3.断点的数据如何保存,再次下载如何继续上次保存的下载进度继续下载。
话不多说,下面是JAVA代码实现
1.首先启动Nginx服务器(或者其他静态资源服务器都可以,主要为了方便资源下载)
public static HttpURLConnection http(String urls,String method){
try {
HttpURLConnection http = (HttpURLConnection) new URL(urls).openConnection();
http.setRequestMethod(method);
// http.setDoOutput(true);
// http.setDoInput(true);
http.setConnectTimeout(1000*60);
http.setReadTimeout(1000*60);
// http.setRequestProperty("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
// http.setRequestProperty("Accept-Encoding","gzip, deflate, br");
// http.setRequestProperty("Accept-Language","zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7");
// http.setRequestProperty("Cache-Control","max-age=0");
http.setRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36");
return http;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private static boolean isrange = false;
private static long range = 0,all = 0;
//private static ForkJoinPool pools = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
public static void thread_download(JTextArea pane,Integer number,String... line){
try {
File desktop = FileSystemView.getFileSystemView().getHomeDirectory();
File log_file = new File(desktop,"data.log");
RandomAccessFile log = new RandomAccessFile(log_file,"rwd");
if(log_file.length()>0){String url = log.readUTF();range = log.readLong();all = log.readLong();if(url!=null && range>0 && all>0){
System.out.println("url "+url);
System.out.println("range "+range);
System.out.println("all "+all);
line = new String[]{url};isrange = true;}}
for (String urls : line) {
String filename = urls.substring(urls.lastIndexOf("/")+1);
HttpURLConnection http = http(urls,"GET");
if(isrange)http.setRequestProperty("Range", "bytes=" + range + "-" + all);
if(!String.valueOf(http.getResponseCode()).matches("(206|200)"))continue;
long content_length = http.getContentLengthLong();
File file = new File(desktop,filename);
if(!file.exists()){
RandomAccessFile raf = new RandomAccessFile(file,"rwd");
raf.setLength(content_length);
raf.close();}
long blockSize = content_length / number;
CompletableFuture<?>[] future = new CompletableFuture[number];
AtomicLong array = new AtomicLong(0);
CompletableFuture.runAsync(()->{
try {
while(true){
log.seek(0);
log.writeUTF(urls);
log.writeLong(isrange?array.get()+range:array.get());
log.writeLong(isrange?all:content_length);
long last_size = array.get();
TimeUnit.SECONDS.sleep(1);
long now = array.get(),length = now - last_size;
long second = (content_length - now)/(length==0?1:length);
String timer = String.format("%02d:%02d:%02d",second/3600,second%3600/60,second%60);
boolean is_size;
double size = (is_size = length>1024*1024)?length/1024d/1024d:length/1024d;
String time = String.format("%.2f "+(is_size?"MB/S":"KB/S"),size);
long now1 = isrange?now+range:now,length1 = isrange?all:content_length;
pane.setText(String.format("名称:%s%n大小:%d/%dMB 进度:%d%% 速度:%s 时间:%s",
filename,
(int)((isrange?array.get()+range:array.get())/1024d/1024d),
(int)((isrange?all:content_length)/1024d/1024d),
(int)Math.ceil(100d*now1/length1),
time,
timer));
if(array.get()>=content_length)break;
}
log.close();
log_file.delete();
isrange = false;
range = all = 0;
System.err.println("文件删除");
} catch (Exception e) {
e.printStackTrace();
}
});
for (int threadId = 1; threadId <= number; threadId++) {
long index = threadId;
long startIndex = isrange?range+(threadId - 1) * blockSize:(threadId - 1) * blockSize;
AtomicLong endIndex = new AtomicLong(startIndex + blockSize - 1);
if(threadId == number)endIndex.set(isrange?all:content_length);
System.err.println("线程" + threadId + "下载:" + startIndex + "字节~" + endIndex.get() + "字节");
future[threadId - 1] = CompletableFuture.runAsync(()->{
try{
HttpURLConnection http2 = http(urls,"GET");
http2.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex.get());
if(http2.getResponseCode()!=206)return;
InputStream is = http2.getInputStream();//返回资源
RandomAccessFile raf2 = new RandomAccessFile(file,"rwd");
raf2.seek(startIndex);
byte[] buffer = new byte[1024*200];
int len=0;
while((len = is.read(buffer))!=-1){raf2.write(buffer,0,len);array.set(array.get()+len); }
is.close();
raf2.close();
System.err.println("线程 "+index+" 完成");
} catch (Exception e) {
e.printStackTrace();
}
});
}
CompletableFuture.allOf(future).join();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static Thread download_thread = null;
private static volatile int down_status = 1;
public static void main(String[] args) throws Exception {
JDialog jdialog = new JDialog();
jdialog.setTitle("文件下载");
jdialog.setLayout(new FlowLayout(FlowLayout.CENTER,0,100));
jdialog.setAlwaysOnTop(true);
jdialog.setSize(600,500);
jdialog.setResizable(false);
jdialog.setLocationRelativeTo(null);
jdialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
//System.out.println("我是呵呵哈哈哈".replaceAll("(.)\\1+","$1"));
JPopupMenu menu = new JPopupMenu("系统提示");
JTextArea pane = new JTextArea();
pane.setLineWrap(true);
pane.setComponentPopupMenu(menu);
pane.setFont(new Font("微软雅黑",Font.PLAIN,18));
pane.setPreferredSize(new Dimension(550,300));
ActionListener action = e->{
JMenuItem menus = (JMenuItem)e.getSource();
String text = pane.getSelectedText();
switch (menus.getText()) {
case "开始下载":
System.out.println("text "+text);
if(text == null || !text.matches("https?://(.+?)")){JOptionPane.showMessageDialog(jdialog,"URL地址错误!","系统提示",JOptionPane.INFORMATION_MESSAGE);break;
}CompletableFuture.runAsync(()->thread_download(pane,4,text));
break;
case "继续下载":
menus.setText("暂停下载");
//if(pools.isShutdown())pools = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
CompletableFuture.runAsync(()->thread_download(pane,4,text));
break;
case "暂停下载":
menus.setText("继续下载");
//if(!pools.isShutdown())pools.shutdownNow();
break;
}
};
Stream.of("开始下载","暂停下载","继续下载").forEach(a->{
JMenuItem item = new JMenuItem(a);
menu.add(item);
item.setFont(new Font("黑体",Font.BOLD,18));
item.addActionListener(action);
});
jdialog.add(pane);
jdialog.setVisible(true);
System.out.println("4444444444444444444444444444444444444444");
//new Thread(()->JOptionPane.showMessageDialog(pane,pane,"系统提示!",JOptionPane.INFORMATION_MESSAGE)).start();
new DropTarget(pane,DnDConstants.ACTION_COPY_OR_MOVE, new DropTargetAdapter()
{
@Override
public void drop(DropTargetDropEvent dtde)
{
try {
if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
dtde.dropComplete(true);
@SuppressWarnings("unchecked")
List<File> list = (List<File>)dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
for (File file : list){
System.out.println(file);
if(!file.getName().endsWith(".txt"))continue;
String[] line = Files.readAllLines(file.toPath(),StandardCharsets.UTF_8)
.stream().filter(a->a.matches("https?://(.+?)")).toArray(String[]::new);
if(line.length<1){JOptionPane.showMessageDialog(jdialog,"NO URLS","系统提示",JOptionPane.INFORMATION_MESSAGE);continue;}
thread_download(pane,2,line);
}
}else dtde.rejectDrop();
} catch (Exception e) {
e.printStackTrace();
}
}
});
//BaseTools.text_lists("D:\\桌面\\test");
//System.out.println("hj \"4545 455\" kl".replaceAll("(?<=\") (?<!\")","00"));
// Matcher patter2 = Pattern.compile("[A-Z]{1}[a-z]+ (\\w+)",Pattern.DOTALL).matcher("Astring name");
// while(patter2.find())System.out.println(patter2.group(1));
//System.out.println(Arrays.toString(WebMvcConfig.class.getClasses()));
// Files.copy(new URL("https://cdn.staticfile.org/vue/3.2.36/vue.global.min.js")
// .openStream(),Paths.get("E:\\qwer\\qwer14\\NettyServer\\src\\main\\resources\\static\\vue.global.min.js"));
}