一、 程序总体设计
程序的主要程序结构由四个类组成,分别是下载线程、统计线程、参数输入对话框和程序主类。下载线程的作用是建立HTTP连接和进行将数据下载到本地文件;统计线程的作用是随时将所有下载线程当前的下载进度输出到程序窗口;参数输入对话框是特意为新建一个下载时输入各项参数时编写的对话框,在其内部进行URL的输入、下载线程数的输入和下载文件的保存位置的输入或者选择;程序主类的作用主要是搭建主要窗口,以及将参数输入的结果、下载进度进行输出,最重要的是控制所有的线程,还有实现对所有控件操作进行响应。这些类的定义如下:
class ThrDownload extends Thread
class ThrCount extends Thread
class MyDialog extends JDialog implements ActionListener
public class Download
对于所有下载线程的控制方法较为特殊,采用在下载线程中添加下载线程本身的实例数组作为静态数据成员的方式进行统一控制,而下载时的必要参数也都被声明为下载线程的静态数据成员,这样可以在其他类的内部随意通过下载线程类名来访问这些数据成员,以便于统一控制下载线程。
在程序主窗口中设置了四个按钮,分别是“新建下载”、“继续下载”、“开始”、“暂停”。同时添加一个进度条组件和两个文本编辑框组件进行下载参数或者下载进度的显示,其中进度条专门用来显示下载一个文件总体时的下载进度,用百分比以及进度图形的形式进行显示,第一个文本编辑框会分不同时段显示下载文件的URL或者已经下载的字节数,第个文本编辑框会分不同时段显示下载文件的本地保存位置或者下载速度。程序窗口设计如下:
- 点击“新建下载”时会打开参数输入对话框,进行所有的参数的输入,输入完成的参数在点击对话框中的确定按钮后会被复制到下载线程的对应参数中。其中保存路径的输入为了方便添加了FileDialog作为输入选择的一种方式,输入完成并且URL合法性检查通过后,将会调用所有下载线程的初始化函数进行初始化,将所有下载线程需要的参数进行赋值,并且尝试进行建立HTTP连接,如果连接失败则在文本框内进行提示,如果成功就初始化输入参数对应的下载线程数量,并且调用其各自的构造函数进行下载线程的初始化,同时为这一次下载初始化一个统计线程。当初始化工作完成后将在两个文本编辑框中进行输出URL和保存路径的提示信息。
- 点击“继续下载”时,将会弹出一个文件对话框,要求选择需要进行继续下载的配置文件。当选择好配置文件后,将会从选择的文件中读取出所有保存的配置信息,比如URL、下载线程数量、保存路径和各下载线程的已经下载的字节数。之后将用读取出的信息来进行和新建下载中类似的初始化工作,但是此时所有下载线程类的下载开始位置来源是配置文件中读取出的上次结束线程时保存的开始位置,也就是之前已经下载的字节数。
- 点击“开始”时,将会记录当前开始的时间点,并且启动所有的下载线程进行下载,与此同时启动统计线程,随时将下载进度输出到窗口。在统计线程的线程体中,除了随时输出当前下载的进度外,还需要判断当前是否所有线程都已经自动结束,如果是,那么就需要首先判断当前是否是继续下载,是的话就将配置文件删除。
- “暂停”按钮被按下时,检查所有的下载线程,如果还有线程处于活动状态,那么就新建配置文件,将当前各下载线程的下载进度保存到配置文件中,方便下次继续下载,同时将所有的下载线程结束。此时,可以点击窗口关闭按钮结束程序,在下次重新打开程序后,或者直接新建下载,所有参数都和上次一样时,点击开始下载就可以进行继续下载。
二、主要数据结构描述
- 下载线程ThrDownload:
下载线程本身是一个进行HTTP文件下载的线程,对应的私有数据成员是URL名、输入输出流、下载文件的开始位置start和结束位置end、读写缓冲区和已经下载的字节数等用于统计的参数(已下载字节数分为此次开始下载后的总长度len和对于整个文件来说的已下载长度finish),其线程体的操作是将URL对应文件的start到end位置的数据下载到本地文件对应位置,并且实时更新统计参数。此外,为了方便统一控制所有的下载线程,将下载线程的实例定义在线程类内部,声明为静态参数,所以可以直接通过类名来访问。和下载线程实例一样被定义为静态数据成员的还有需要下载文件的总长度、下载线程数、下载文件的URL、保存文件的路径名、统计线程、当前需要下载的文件是否是继续下载isPart、各下载线程下载的开始位置,这些静态数据成员的作用也是为了方便统一控制所有下载线程。下载线程的构造函数中会从静态参数中读取自己需要的参数,如下载文件的开始位置和结束位置,并且建立其文件读写的接口和HTTPURL连接。所有下载线程的初始化如总体设计中所述,会进行所有下载参数的赋值,并且自动确定所以下载线程的下载开始和结束位置。此外,为了方便在其他类中获取已下载的数据长度,定义了两个分别返回len和finish的参数。
下载线程中所有的数据成员如下:
private int no; // 线程号
private RandomAccessFile out;
private InputStream in; // 输入字节流
private URL url; // URL
private final int start; // 该下载线程下载的文件起始和结束位置
private int end;
private byte[] b; // 读写缓冲区
private int len; // 该下载线程开始下载到现在下载的字节数
private int finish; // 该下载线程总下载字节数
// 下载线程共享资源
public static int file_len = 0; // 需要下载的文件总长度
public static int buf_len = 8192; // 缓冲区大小:1MB
public static int num_thread = 1; // 下载线程数
public static String url_name; // 下载文件的url名
public static String save_name; // 保存文件名
public static ThrDownload[] thr_download; // 下载线程
public static ThrCount thr_count; // 统计线程
public static boolean isPart = false; // 当前需下载的文件是否已经下载过一部分,默认为否
public static int[] start_pos; // 从临时文件读取的各下载线程开始位置
- 程序主要类Download:
程序主要类中的数据成员主要是主窗口的各个组件,而主要进行的操作就是所有组件的初始化以及添加到主窗口并显示窗口,最重要的是为了所有按钮添加事件监听程序,不同的按钮对应不同的操作,“新建下载”是输入所有参数和初始化下载线程(可能读取配置文件),“继续下载”是打开一个存在的配置文件并且用其中的信息进行下载线程的初始化,“开始”是启动所有下载线程和统计线程,“暂停”是保存当前下载进度到配置文件并且结束所有下载线程。
三、主要算法描述
- 下载线程的线程体:
用一个整型数据int L记录从URL文件读取一次数据所读出的字节数,循环读取数据到缓冲区,并将缓冲区中数据保存到本地文件中,同时用L更新该线程已下载数据的进度。如果判断到读取出的字节数为-1,说明该下载线程已经下载完成了需要下载的数据,那么关闭输入输出流。
- 下载速度和下载进度的计算:
因为在下载线程中有此次开始下载后已下载的数据长度len和该下载文件总的已下载数据长度finish,并且可以用查询函数获得其值,所以在每次开始下载时记录当前的时间点,然后在之后每隔一段时间获取新的时间点计算出时间间隔,再用所有线程的len之和除以该时间间隔就可以得出下载速度。而下载进度就是将所有线程的finish之和除以下载文件的总大小即可得到。
- 下载线程的下载开始位置确定
在下载线程中添加一个标识本次下载是否是继续下载的布尔变量,以及一个用以保存所有下载线程的下载开始位置的数组,该数组只有在当前是继续下载时才会使用。如果当前是第一次下载,那么所有下载线程的开始位置默认都是从头开始,但如果是继续下载那么其开始位置就是从配置中读取出的开始位置,从配置文件中读取出的开始位置首先爆出到先前提到的数组中,然后在下载线程进行初始化时再赋值给每个线程。
- 配置文件的输入输出:
当点击了暂停按钮。并且当前仍有下载线程处于运行状态,那么就将当前下载的信息进行保存到配置文件。如果点击了继续下载就打开一个配置文件进行读取下载信息。此外,在统计线程检查到当前已经完成了文件下载时就将对应的存在的配置文件进行删除。
在保存时,将当前下载的URL、下载线程数量、保存路径和每个下载线程当前已经下载的总数据长度finish保存到配置文件中。URL保存的表项是“URL”,保存路径的表项是“Dir”,下载线程的数量保存表项是“Thr”,而各下载线程的开始位置统一保存在一个名为“Prog”的表项下,每个长度之间用“@”来进行分隔。同样的,在读取时,也是将所有信息按保存时的顺序读取出来,注意读取开始位置是每隔一个“@”读取一个数据,再分别赋值给每个下载线程的已下载总长度finish,并且将其作为开始位置。
四、程序测试
进入程序时主界面:
点击新建下载弹出参数输入对话框:
点击保存文件路径旁边的按钮进行文件位置选择:
点击确定后回到主界面:
点击开始后下载进度显示:
点击暂停后界面,以及出现的配置文件:
点击继续下载,选择出现的配置文件,点击开始继续下载:
直到完成下载,并查看下载完成的文件(配置文件已删除):
五、源程序附录
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.net.*;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
//下载线程
class ThrDownload extends Thread {
private int no; // 线程号
private RandomAccessFile out;
private InputStream in; // 输入字节流
private URL url; // URL
private final int start; // 该下载线程下载的文件起始和结束位置
private int end;
private byte[] b; // 读写缓冲区
private int len; // 该下载线程开始下载到现在下载的字节数
private int finish; // 该下载线程总下载字节数
// 下载线程共享资源
public static int file_len = 0; // 需要下载的文件总长度
public static int buf_len = 8192; // 缓冲区大小:1MB
public static int num_thread = 1; // 下载线程数
public static String url_name; // 下载文件的url名
public static String save_name; // 保存文件名
public static ThrDownload[] thr_download; // 下载线程
public static ThrCount thr_count; // 统计线程
public static boolean isPart = false; // 当前需下载的文件是否已经下载过一部分,默认为否
public static int[] start_pos; // 从临时文件读取的各下载线程开始位置
// 构造函数
public ThrDownload(final int no) {
final int block_len = file_len / num_thread; // 计算每个下载线程需要下载的数据长度
if (!isPart) // 第一次下载
start = block_len * (no - 1); // 该线程下载的数据开始位置
else { // 不是第一次下载
start = block_len * (no - 1) + start_pos[no - 1];
}
end = (block_len * no) - 1; // 该线程下载的数据结束位置
if (no == num_thread)
end = file_len - 1;
len = 0; // 当前已下载的字节数初始化
finish = start - block_len * (no - 1); // 当前总共下载的字节数
try {
url = new URL(url_name);
final HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestProperty("Range", "byte=" + start + "-" + end);
in = con.getInputStream();
if (con.getResponseCode() >= 300)
new Exception("Http响应问题:" + con.getResponseCode());
out = new RandomAccessFile(save_name, "rw");
this.no = no;
out.seek(start); // 在保存文件中确定保存的位置
b = new byte[buf_len]; // 初始化缓冲区
} catch (final Exception e) {
System.out.println(e.toString());
}
}
// 所有下载线程的初始化函数
public static boolean init(final String url_name, final String save_name, final int num_thread) {
ThrDownload.url_name = url_name;
ThrDownload.save_name = save_name;
ThrDownload.num_thread = num_thread;
try {
final URL url = new URL(url_name);
final HttpURLConnection con = (HttpURLConnection) url.openConnection();
ThrDownload.file_len = con.getContentLength();
} catch (final Exception e) {
return false;
}
if (file_len == -1)
return false; // 为-1说明资源分块传输无Conten_length
thr_download = new ThrDownload[num_thread];
for (int i = 0; i < num_thread; i++) {
thr_download[i] = new ThrDownload(i + 1); // 下载线程的初始化
}
thr_count = new ThrCount(); // 统计线程的初始化
return true;
}
// 下载线程体
public void run() {
int L; // 读取出的字节数,为-1的话已经读取到文件末尾
try {
while (true) {
L = in.read(b); // 读取直接到缓冲区
if (L == -1)
break;
out.write(b, 0, L); // 将缓冲区写到保存文件
len += L;
finish += L;
}
in.close();
out.close();
} catch (final Exception e) {
}
}
// 查询开始下载到现在当前已经下载的数据长度
public int getLen() {
return len;
}
// 查询当前已经保存保存的数据长度
public int getFinish() {
return finish;
}
}
// 统计线程
class ThrCount extends Thread {
public static double begin_time; // 下载开始时间
public static double current_time; // 当前时间
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);// 每隔一秒输出一次进度
} catch (final InterruptedException ex) {
}
current_time = (new Date()).getTime() / 1000.0;
double percent = 0;
double speed = 0; // 计算下载百分比和平均下载速度
for (int i = 0; i < ThrDownload.num_thread; i++) {
percent += ThrDownload.thr_download[i].getFinish();
speed += ThrDownload.thr_download[i].getLen();
}
// 输出当前进度等
DecimalFormat[] df = new DecimalFormat[4]; // 将double格式化
for (int i = 0; i < 4; i++)
df[i] = new DecimalFormat("###.0");
Download.tf1.setText("已下载文件大小:" + df[0].format(percent / 1000) + "KB / "
+ df[1].format(ThrDownload.file_len / 1000) + "KB");
speed = speed / 1000.0 / (current_time - begin_time); // 单位KB/S
percent /= ThrDownload.file_len;
Download.tf2.setText(df[2].format(percent * 100) + "%已下载, 平均下载速度=" + df[3].format(speed) + "KB/S");
Download.pb.setValue((int) (percent * 100)); // 更新进度条的进度
Download.pb.repaint(); // 手动重新绘制进度条
// 判断是否所有下载线程都已经结束,若否则继续下载
int i;
for (i = 0; i < ThrDownload.num_thread; i++)
if (ThrDownload.thr_download[i].isAlive())
break;
if (i >= ThrDownload.num_thread) // 全部线程下载完成
{
for (i = 0; i < ThrDownload.num_thread; i++)
ThrDownload.thr_download[i].stop();
try {
File destroy = new File(ThrDownload.save_name + ".cfg");
if (destroy.exists()) // 存在配置文件
destroy.delete();
} catch (Exception e) {
System.out.println(e.toString());
}
break;
}
}
}
}
// 输入参数对话框
class MyDialog extends JDialog implements ActionListener {
JTextField input_url, input_num, input_pathname;
JButton button_down, button_choose; // 确定按钮
FileDialog save; // 选择保存文件的位置
String url = "", pathname = "";
int num; // 下载线程数
MyDialog(final JFrame f, final String s) { // 传入父容器和对话框标题
super(f, s);
setLayout(new FlowLayout(FlowLayout.LEFT, 50, 20));
setBounds(600, 260, 500, 400); // 设置对话框位置和大小
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
// 添加输入url的组件
final JLabel label1 = new JLabel("输入URL:");
label1.setFont(new Font("宋体", Font.BOLD, 15));
add(label1);
input_url = new JTextField(35);
input_url.setFont(new Font("宋体", Font.BOLD, 20));
input_url.setText("https://qd.myapp.com/myapp/qqteam/pcqq/PCQQ2019.exe");
add(input_url);
// 添加输入下载线程数的组件
final JLabel label2 = new JLabel("输入下载线程数:");
label2.setFont(new Font("宋体", Font.BOLD, 15));
add(label2);
input_num = new JTextField(35);
input_num.setFont(new Font("宋体", Font.BOLD, 20));
input_num.setText("5");
add(input_num);
// 添加输入保存文件路径名的组件
final JLabel label3 = new JLabel("输入保存文件路径名:");
label3.setFont(new Font("宋体", Font.BOLD, 15));
add(label3);
input_pathname = new JTextField(30);
input_pathname.setFont(new Font("宋体", Font.BOLD, 20));
input_pathname.setText("");
add(input_pathname);
// 文件对话框初始化
save = new FileDialog(this, "选择保存位置", FileDialog.SAVE);
button_choose = new JButton("...");
button_choose.addActionListener(this);
button_choose.setPreferredSize(new Dimension(20, 30));
add(button_choose);
// 添加确定按钮
button_down = new JButton("确定");
button_down.addActionListener(this);
button_down.setFont(new Font("宋体", Font.BOLD, 20));
button_down.setPreferredSize(new Dimension(100, 50));
add(button_down);
}
public void actionPerformed(final ActionEvent e) {
switch (e.getActionCommand()) {
case "...":
save.setVisible(true);
input_pathname.setText(save.getDirectory() + save.getFile());
break;
case "确定":
try {
url = input_url.getText();
URL test = new URL(url);
} catch (MalformedURLException ex) { // 检查URL合法性
JOptionPane.showMessageDialog(this, "URL格式错误!", "错误", JOptionPane.ERROR_MESSAGE);
url = "";
return;
}
pathname = input_pathname.getText();
num = Integer.parseInt(input_num.getText());
setVisible(false);
break;
default:
break;
}
}
public String get_url() {
return url;
}
public int get_num() {
return num;
}
public String get_pathname() {
return pathname;
}
}
public class Download {
private static JFrame f;
public static JProgressBar pb; // 进度条
public static JTextField tf1, tf2; // 编辑框组件
private JButton[] bt; // 按钮组件
FileDialog con_download; // 继续下载时打开配置文件的对话框
private MyDialog dialog;
class event_window extends WindowAdapter { // 窗口关闭事件监听
public void windowClosing(final WindowEvent e) {
System.exit(0);
}
}
class event_action implements ActionListener { // 按钮点击事件监听
public void actionPerformed(final ActionEvent e) {
final String s = e.getActionCommand();
switch (s) {
case "新建下载":
dialog.setVisible(true);
ThrDownload.isPart = false;
if (!ThrDownload.init(dialog.get_url(), dialog.get_pathname(), dialog.get_num()))
tf1.setText("下载线程初始化失败!");
if (dialog.get_url() != "")
tf1.setText("URL:" + dialog.get_url());
if (dialog.get_pathname() != "")
tf2.setText("下载文件保存到" + dialog.get_pathname());
break;
case "继续下载":
con_download.setVisible(true);
// 读取配置文件
String url="",save_name="";
int n=0;
try {
ThrDownload.isPart = true;
FileReader propFile = new FileReader(con_download.getDirectory()+con_download.getFile());
Properties prop = new Properties();
prop.load(propFile);
url = prop.getProperty("URL"); // 载入URL
save_name = prop.getProperty("Dir"); // 载入路径
n = Integer.parseInt(prop.getProperty("Thr")); // 载入线程数
ThrDownload.start_pos = new int[n];
String progress = prop.getProperty("Prog"); // 解析每个线程的进度
String[] strProg = progress.split("@");
if (strProg.length != n)
throw new Exception("Error.");
for (int i = 0; i < n; i++)
ThrDownload.start_pos[i] = Integer.parseInt(strProg[i]);
} catch (Exception ex) {
ex.printStackTrace();
}
if (!ThrDownload.init(url, save_name, n))
tf1.setText("下载线程初始化失败!");
if (url != "")
tf1.setText("URL:" + url);
tf2.setText("继续下载:" + save_name);
break;
case "开始":
ThrCount.begin_time = (new Date()).getTime() / 1000.0;
for (int i = 0; i < ThrDownload.num_thread; i++)
ThrDownload.thr_download[i].start(); // 所有线程开始下载
ThrDownload.thr_count.start(); // 开始统计
break;
case "暂停": {
tf2.setText("已暂停下载...");
int i;
for (i = 0; i < ThrDownload.num_thread; i++)
if (ThrDownload.thr_download[i].isAlive())
break;
if (i != ThrDownload.num_thread) { // 退出时还有线程没有下载完成,保存配置文件
try {
Properties prop = new Properties();
prop.setProperty("URL", ThrDownload.url_name);
prop.setProperty("Dir", ThrDownload.save_name);
prop.setProperty("Thr", Integer.toString(ThrDownload.num_thread));
String temp = Integer.toString(ThrDownload.thr_download[0].getLen());
for (int j = 1; j < ThrDownload.num_thread; j++) {
temp = temp + "@" + Integer.toString(ThrDownload.thr_download[j].getFinish());
}
prop.setProperty("Prog", temp);
File propFile = new File(ThrDownload.save_name + ".cfg");
prop.store(new FileWriter(propFile), "");
} catch (Exception ex) {
ex.printStackTrace();
}
}
for (i = 0; i < ThrDownload.num_thread; i++)
ThrDownload.thr_download[i].stop();
}
ThrDownload.thr_count.stop();
break;
default:
break;
}
}
}
public void init() { // 图形界面初始化
// 容器和布局定义
f = new JFrame("多线程下载器");
f.setLayout(null);
// 进度条初始化
pb = new JProgressBar(0, 100);
// 设置进度最小最大值
pb.setValue(0); // 当前值
pb.setStringPainted(true);// 绘制百分比文本(进度条中间显示的百分数)
pb.setIndeterminate(false);
pb.setPreferredSize(new Dimension(510, 20));
f.add(pb);
pb.setBounds(50, 10, 500, 30);
// 对话框初始化
dialog = new MyDialog(f, "输入下载参数");
dialog.setModal(true);
//文件对话框的初始化
con_download = new FileDialog(f,"选择继续下载的配置文件",FileDialog.LOAD);
// 显示框初始化
tf1 = new JTextField(56);
tf1.setFont(new Font("宋体", Font.BOLD, 15));
tf1.setText("URL");
f.add(tf1);
tf1.setBounds(50, 50, 500, 30);
tf2 = new JTextField(56);
tf2.setFont(new Font("宋体", Font.BOLD, 15));
tf2.setText("保存路径");
f.add(tf2);
tf2.setBounds(50, 90, 500, 30);
// 按钮初始化
bt = new JButton[4];
bt[0] = new JButton("新建下载");
bt[1] = new JButton("继续下载");
bt[2] = new JButton("开始");
bt[3] = new JButton("暂停");
for (int i = 0; i < 4; i++) // 设置按钮字体样式
bt[i].setFont(new Font("宋体", Font.BOLD, 17));
for (int i = 0; i < 4; i++)
f.add(bt[i]);
bt[0].setBounds(50, 130, 110, 30);
bt[1].setBounds(180, 130, 110, 30);
bt[2].setBounds(310, 130, 110, 30);
bt[3].setBounds(430, 130, 110, 30);
// 添加事件监听
final event_window e_w = new event_window();
final event_action e_c = new event_action();
f.addWindowListener(e_w);
for (int i = 0; i < 4; i++) {
bt[i].addActionListener(e_c);
}
}
public void display() { // 显示窗口
f.setSize(600, 230);
f.setLocation(700, 350);
f.setVisible(true);
f.setResizable(false);
}
public static void main(final String args[]) {
final Download a = new Download();
a.init();
a.display();
}
}