接上篇【多文件自平衡云传输 四】断点续传其实所有核心功能已经完成了。现在需要我想要做一个界面让我可以直观的看到其接收的内容,进度。基本想法是,让外部想要获取多文件资源时会启动接收端服务器,我想在启动接收端服务器的过程中显示进度条界面。当接收完毕(也即接收端成功拥有该资源)时关闭进度条。
实现一个什么样的进度条?需要什么数据支持?
用进度条来显示某个文件的接收进度。所以最基本我需要明确一个文件的大小。这个数据是动态显示进度的基础,当然还需要明确某一个文件每次写的片段为多大,这两个量决定了进度条的动态增长。在每个进度条上面还想显示这个是属于哪个文件的进度条,即进度条上标明文件路径信息。而且当某个文件接收完毕,他所属的进度条能够自动消失。而且既然是多文件传输,还想显示一下当前总接收任务数,让他也能动态增长。
基础上述想法,明确目的之后制作
首先还是基础组件的准备,需要一个表示进度条的类
public class ReceiveFileProgress extends JPanel {
private static final long serialVersionUID = -3000076402767238085L;
public static final int HEIGHT = 30; //默认高度30px
private long receivedLen; //用于标记进度条进度
private long fileSize; //文件的大小
/**
* 可视化显示某些任务进度的组件。 随着任务进行到完成,进度条显示任务的完成百分比。
* 该百分比通常由一个开始空的矩形视觉表示,并随着任务的进行逐渐填充。 此外,进度条可以显示此百分比的文本表示。
*/
private JProgressBar jpgbReceiveFile;
public ReceiveFileProgress(JPanel jpnlParent, String fileName, long fileSize) {
this(jpnlParent, fileName, 0L, fileSize);
}
public ReceiveFileProgress(JPanel jpnlParent, String fileName, long receivedLen, long fileSize) {
this.fileSize = fileSize;
this.receivedLen = receivedLen;
this.setLayout(new GridLayout(0, 1));
int parentWidth = jpnlParent.getWidth();
this.setSize(parentWidth, HEIGHT);
JLabel jlblFileName = new JLabel(fileName, JLabel.CENTER);
jlblFileName.setFont(IMecView.normalFont);
this.add(jlblFileName);
this.jpgbReceiveFile = new JProgressBar();
this.jpgbReceiveFile.setFont(IMecView.normalFont);
this.jpgbReceiveFile.setStringPainted(true);
//这个指的是进度条进度满的时候的值(进度最大值)
this.jpgbReceiveFile.setMaximum((int) this.fileSize);
//进度条会根据 当前进度值 / 进度最大值 来显示进度
this.jpgbReceiveFile.setValue((int) this.receivedLen);
this.add(this.jpgbReceiveFile);
}
//每接收到一个文件片段之后调整进度
public void receiveFileSection(int newReceiveLen) {
this.receivedLen += newReceiveLen;
this.jpgbReceiveFile.setValue((int) this.receivedLen);
}
}
此外还需要一个界面来容纳这些进度条。采用模态框作为界面。
public abstract class ReceiveProgressDialog extends JDialog implements IMecView {
private static final long serialVersionUID = -8166199591766665433L;
public static final int WIDTH = 340; //界面初始宽度
public static final int MIN_HEIGHT = 170; //界面最小高度
public static final String RECEIVE_FILE_TOTAL_COUNT = "本次共接收#个文件";
private int receiveFileCount; //接收文件的个数。用此数据可初始化进度条个数
private JLabel jlblReceiveFileCount; //用于可视化显示接收文件个数
private JProgressBar jpgbReceiveFileCount;
//一个画布容器用于收纳所有显示的进度条
private JPanel jpnlFiles;
//键:文件编号 值:进度条对象
private Map<Integer, ReceiveFileProgress> receiveFileProgressPool;
private static final AtomicBoolean isFirst = new AtomicBoolean(true);
//我需要利用此变量来标记已经完成接收的文件数,以便动态显示总任务进程
private static final AtomicInteger tmpCount = new AtomicInteger(0);
//进度条中需要保存ResourcePool信息。因为这里面有未接收片段信息
public ReceiveProgressDialog(JFrame parent, int receiveFileCount) {
super(parent, true); //模态框
this.receiveFileCount = receiveFileCount;
this.receiveFileProgressPool = new HashMap<>();
initView(); //初始化界面
}
//移除接收成功的进度条
public void removeReceiveFile(int fileNo) {
ReceiveFileProgress receiveFileProgress = this.receiveFileProgressPool.get(fileNo);
if (receiveFileProgress != null) {
this.jpnlFiles.remove(receiveFileProgress);
receiveFileProgressPool.remove(fileNo);
//处理总任务进度
final int count = tmpCount.incrementAndGet();
this.jpgbReceiveFileCount.setString(count + " / " + this.receiveFileCount);
jpgbReceiveFileCount.setValue(count);
//删除一个进度条后重新调整界面大小
resizeDialog();
}
}
//判断进度条容器中是否存在该文件对应的进度条
public boolean receiveFileExist(int fileNo) {
return this.receiveFileProgressPool.containsKey(fileNo);
}
//每接收到一个文件片段之后调整进度
public void receiveFileSection(int fileNo, int receiveLen) {
ReceiveFileProgress receiveFileProgress = this.receiveFileProgressPool.get(fileNo);
receiveFileProgress.receiveFileSection(receiveLen);
}
//添加接收文件---即添加一个进度条
public void addReceiveFile(int fileNo, String fileName, long fileSize) {
addReceiveFile(fileNo, fileName, 0L, fileSize);
}
//将进度条添加到容器和可视化画布中。 也即根据文件信息初始化进度条
public void addReceiveFile(int fileNo, String fileName, long receivedSize, long fileSize) {
ReceiveFileProgress receiveFileProgress = new ReceiveFileProgress(jpnlFiles, fileName, receivedSize, fileSize);
this.receiveFileProgressPool.put(fileNo, receiveFileProgress);
this.jpnlFiles.add(receiveFileProgress);
//添加完之后重新调整界面大小
resizeDialog();
}
//调整界面大小
private void resizeDialog() {
int receiveFileProgressCount = this.receiveFileProgressPool.size();
int height = MIN_HEIGHT
+ receiveFileProgressCount * (ReceiveFileProgress.HEIGHT)
+ (receiveFileProgressCount > 1
? (receiveFileProgressCount-1) * IMecView.PADDING * 3 : 0);
this.setSize(WIDTH, height);
this.setLocationRelativeTo(this.getParent());
}
//调整界面中显示的任务数字符串
private String getFileCountString(int fileCount) {
return RECEIVE_FILE_TOTAL_COUNT.replaceAll("#", String.valueOf(fileCount));
}
@Override
public void reinit() {}
//TODO 进度条显示之后的逻辑。外部可以视情况来扩展
public abstract void afterDialogShow();
@Override
public void dealEvent() {
//重要: 获取焦点事件。本对象获取焦点就是界面显示时
this.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
//在这里出发点是我将权限交给服务器端操作,当界面显示出来时我可以启动服务器
//由于启动服务器只需要启动一次,所以此处借助标记
if(isFirst.get()) {
afterDialogShow();
isFirst.set(false);
}
}
});
}
@Override
public RootPaneContainer getFrame() {
return this;
}
@Override
public void init() {
this.setSize(WIDTH, MIN_HEIGHT);
this.setLocationRelativeTo(this.getParent());
this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
this.setLayout(new BorderLayout());
this.setResizable(false);
JLabel jlblEast = new JLabel(" ");
jlblEast.setFont(smallFont);
this.add(jlblEast, BorderLayout.EAST);
JLabel jlblWest = new JLabel(" ");
jlblWest.setFont(smallFont);
this.add(jlblWest, BorderLayout.WEST);
// 标题和文件接收数量
JPanel jpnlHead = new JPanel(new BorderLayout());
this.add(jpnlHead, BorderLayout.NORTH);
JLabel jlblHeadEast = new JLabel(" ");
jlblHeadEast.setFont(smallFont);
jpnlHead.add(jlblHeadEast, BorderLayout.EAST);
JLabel jlblHeadWest = new JLabel(" ");
jlblHeadWest.setFont(smallFont);
jpnlHead.add(jlblHeadWest, BorderLayout.WEST);
JLabel jlblTopic = new JLabel("文件接收进度", JLabel.CENTER);
jlblTopic.setFont(topicFont);
jlblTopic.setForeground(topicColor);
jpnlHead.add(jlblTopic, BorderLayout.NORTH);
JPanel jpnlFileCount = new JPanel(new GridLayout(0, 1));
jpnlHead.add(jpnlFileCount, BorderLayout.CENTER);
this.jlblReceiveFileCount = new JLabel(getFileCountString(this.receiveFileCount), JLabel.CENTER);
this.jlblReceiveFileCount.setFont(normalFont);
jpnlFileCount.add(this.jlblReceiveFileCount);
this.jpgbReceiveFileCount = new JProgressBar(0, receiveFileCount);
this.jpgbReceiveFileCount.setFont(normalFont);
this.jpgbReceiveFileCount.setStringPainted(true);
this.jpgbReceiveFileCount.setString("0 / " + this.receiveFileCount);
jpnlFileCount.add(this.jpgbReceiveFileCount);
// 文件接收进度
jpnlFiles = new JPanel();
GridLayout gdltFiles = new GridLayout(0, 1);
gdltFiles.setVgap(PADDING);
jpnlFiles.setLayout(gdltFiles);
this.add(jpnlFiles, BorderLayout.CENTER);
}
//关闭进度条主界面
public void closeView() {
try {
exitView();
} catch (FrameIsNullException e) {
e.printStackTrace();
}
}
}
按照前面的想法。既然进度条准备好了,那么就在服务器启动让其“伴随着”接收端服务器启动而显示。所以我可以在界面显示的瞬间启动服务器等待接收。先按照逻辑定义方法,如下:
public void enabledReceiveProgress(JFrame parent, int receiveFileCount) {
this.receiveProgressDialog =
new ReceiveProgressDialog(parent, receiveFileCount) {
private static final long serialVersionUID = -1890866265362858227L;
//注意:进度条界面显示之后启动的只是接收侦听线程
@Override
public void afterDialogShow() {
ReceiveServer.this.goon = true;
new Thread(ReceiveServer.this, "接收服务器").start();
}
};
((FileReadWriteAction) this.fileReadWriteAction).setProgressDialog(receiveProgressDialog);
}
但是在Swing模态框中有一个重点问题:当模态框显示出来后,在没有关闭界面之前他会阻塞当前线程。所以一个大前提:我显示模态框需要开辟独立的线程去显示,不能冒然显示。所以先定义显示模态框的线程
class ReceiveProgressShower implements Runnable {
private ReceiveProgressDialog receiveProgressDialog;
public ReceiveProgressShower(ReceiveProgressDialog receiveProgressDialog) {
this.receiveProgressDialog = receiveProgressDialog;
}
@Override
public void run() {
try {
receiveProgressDialog.showView();
} catch (FrameIsNullException e) {
e.printStackTrace();
}
}
}
按照之前的思路,在外部调用服务器启动方法时显示模态框。如下:
public void startUp() {
if(goon) {
return;
}
try {
server = new ServerSocket(port);
//这个方法会初始化界面数据
enabledReceiveProgress(null, receiveFileCount);
ReceiveProgressShower shower = new ReceiveProgressShower(receiveProgressDialog);
new Thread(shoer).start();
//此处当其显示时会启动接收端服务器
} catch (IOException e) {
e.printStackTrace();
}
}
在服务器关闭时关闭模态框界面:
public void shutDown() {
if(!goon) {
return;
}
this.goon = false;
if(this.server != null && !this.server.isClosed()) {
try {
this.server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
this.receiveProgressDialog.closeView();
}
什么时候创建进度条?
界面可以显示了,至于进度条,首先每一个文件接收都需要动态显示其进度,前提是我需要创造一个进度条出来,所以选择的时机是在每个文件接收到之后在写入磁盘之前判断该片段对应的文件进度条是否创建出来,如果没有就为其创建。在之前写操作中提供的可供扩展的前置拦截方法中完成即可。而且基于前面的说法创建进度条需要对应文件的详细信息(路径、大小)。基于之前的ResourceServer类没有保存文件的详细信息。所以只需增加成员即可。
public class ReceiveServer implements Runnable{
//...
//保存多文件详细信息,因为我要通过文件的详细信息初始化进度条中的一些数据
private SourceFileList sourceFileList;
private ReceiveProgressDialog receiveProgressDialog;
//保存接收文件个数
private int receiveFileCount;
public void setReceiveFilePool(SourceFileList sourceFileList) {
this.sourceFileList = sourceFileList;
List<FileInfo> fileList = sourceFileList.getFileInfoList();
this.receiveFileCount = fileList.size();
this.receiveFilePool.addFileList(sourceFileList);
}
//...
}
这些数据的初始化只会在第一次建立通信前执行,再来看一遍之前ResourceReceiver中的逻辑:
private void firstReceive(SourceFileList sourceFileList) throws ResourceNotExistException{
List<DefaultNetNode> senderList = getSenderList();
int senderCount = senderList.size();
List<List<FileSectionInfo>> fileSectionInfoListList = fileDistribution.distribution(sourceFileList, senderCount);
this.receiveServer.setReceiveFilePool(this.sourceFileList);
this.receiveServer.setSenderCount(senderCount);
//此处启动服务器时会实例化模态框对象并显示它
this.receiveServer.startUp();
send(senderCount, fileSectionInfoListList, senderList);
}
什么时候更新进度条进度?
当然是接收到一个片段之后更新,所以利用之前提供的接口,就可以在写操作之后更新其进度。并且还需要判断该文件是否全部写入磁盘,如果的确完全接收到,就需要立刻让表示该文件的进度条消失。
具体实现如下:
class FileReadWriteAction extends FileReadWriteIntercepterAdapter{
private ReceiveProgressDialog progressDialog;
public FileReadWriteAction() {}
public void setProgressDialog(ReceiveProgressDialog progressDialog) {
this.progressDialog = progressDialog;
}
//每次写的时候需要判断该文件片段是否有生成进度条。如果没有则生成
@Override
public void beforeWrite(String filePath, FileSectionInfo section) {
if (this.progressDialog == null) {
return;
}
int fileNo = section.getFileNo();
//如果该文件还没有生成相应的进度条
if (!this.progressDialog.receiveFileExist(fileNo)) {
//获取该文件的大小
long fileSize = sourceFileList.getFileSizeByFileNo(fileNo);
//根据文件信息初始化进度条
this.progressDialog.addReceiveFile(fileNo, filePath, fileSize);
}
}
//文件片段写入磁盘之后执行。注意这个参数是当前已经接收到并写入到磁盘文件中去的判断
@Override
public void afterWrite(FileSectionInfo section) {
int fileNo = section.getFileNo();
//获取该文件对应的未接收片段
UnreceiveSection unsection = receiveFilePool.getUnreceiveSection(fileNo);
try {
unsection.updataSection(section.getOffset(), section.getLen());
if (unsection.isReceived()) {
FileReadWrite readWrite = receiveFilePool.getAcceptFrw(fileNo);
receiveFilePool.removeUnreceiveSection(fileNo);
readWrite.close();
}
if (this.progressDialog != null) {
this.progressDialog.receiveFileSection(fileNo, section.getLen());
//如果一个文件的片段全部接收到那么就删除进度条显示
if (unsection.isReceived()) {
this.progressDialog.removeReceiveFile(fileNo);
}
}
} catch (ReceiveSectionOutOfRangeException e) {
e.printStackTrace();
}
}
}
测试结果(两个发送端,传输过程中一个发送端掉线的情况):
接收到的文件:
上面进度条显示的时候没有体现并发性,即没有多个文件一起接受的感觉。其实并非程序出现问题。而是我在一台电脑上测试再加上分配策略导致无法显现出这种效果。分配文件片段时是这样做的:
public List<List<FileSectionInfo>> distribution(SourceFileList sourceFileList, int count){
//ArrayList有序的
List<FileSectionInfo> sectionList = new ArrayList<FileSectionInfo>();
//获取该资源下的所有文件集合,注意这个是ArrayList。有序的
List<FileInfo> fileList = sourceFileList.getFileInfoList();
//这里将每个文件都切分成 众多"片段" 的集合存储进sectionList中
for(FileInfo fileInfo : fileList) {
int fileNo = fileInfo.getFileNo();
long fileSize = fileInfo.getFileSize();
int len;
long offset = 0;
while(fileSize > 0) {
//将该文件分成---最大为maxSectionSize长度的片段,只是分成片段了还没有设置该片段的具体内容。
len = (int) (fileSize > maxSectionSize ? maxSectionSize : fileSize);
FileSectionInfo sectionInfo = new FileSectionInfo(fileNo, offset, len);
sectionList.add(sectionInfo);
fileSize -= len;
offset += len;
}
}
return getFileSectionInfoListList(sectionList, count);
}
上面一段是将多个文件柔和在一起打散成小片段集合。由于从始至终不论是存储文件信息还是上面切片之后重组的集合都是有序的,这就造成了上面说的显示的问题。假如现在有两个文件,假如一个文件可以切分成6片,那么执行切片的逻辑,最后重新组装的集合一定是前6个元素都属于1号文件,后6个元素都属于2号元素。
再执行下面一段将这些文件片段按照发送端个数来均衡地分配。
default List<List<FileSectionInfo>> distributionFileSection(List<FileSectionInfo> sectionList, int count){
List<List<FileSectionInfo>> result = new ArrayList<List<FileSectionInfo>>();
//注意此处是按照发送端个数来开辟元素个数的
for(int i = 0; i < count; i++) {
result.add(new ArrayList<FileSectionInfo>());
}
//如下操作每个元素分配的片段个数是一样的,误差为1
int index = 0;
for(FileSectionInfo section : sectionList) {
List<FileSectionInfo> oneSectionList = result.get(index);
oneSectionList.add(section);
index = (index + 1) % count;
}
return result;
}
首先这样分配之后就result而言:有两个元素(两个发送端)第一个元素中:1号文件3个片段,2号文件3个片段。同样的,第一个元素也一样。然后接收端拿到分配好的result。将第一个元素交给发送端1,将第二个元素交给发送端2。那么也就是说发送端1拿到的文件号与其对应的文件片段数 和 发送端2的一模一样。在发送时他们各自有按照顺序拿出来这些小片段,通过两个线程来并行发送。当发送端1发送完自己所得到的1号文件片段之后,发送端2也发送完了。因为我是在一台电脑上操作,所以速度几乎可以视为一致。所以他们就一起发送文件1,再一起发送文件2。然而进度条的创建又是基于它遇到一个新的文件片段之后在开辟一个进度条。所以这就造成了进度条也是有序地创建出来,给人以串行接收的感觉。但是当一段掉线之后,假如发送端2掉线了,此刻发送端1还在执行发送任务,当他发完自己手中的文件1的片段时文件1的进度就会卡在那里,然后等待自己发送完自己手里文件2的片段。 当发送端1执行完发送任务之后接收端发现断点,处理续传问题。在如上分配,所以又出现一种齐头并进的感觉。