这篇文章主要是关于ftp和torrent实现多线程下载的内容
关于ftp说的还算清楚
但是torrent就很一般了,建议还是先去看我所参考的博客,再去看源代码
实在是比较复杂,很难在一篇博客中说清楚,大家见谅个
一、根据psp表格做出预估
表格中实际耗时由需求完成后进行的统计
PSP 2.1表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | 11580 | 12660 |
· Analysis | · 需求分析 (包括学习新技术) | 4500 | 5020 |
· Design Spec | · 生成设计文档 | 380 | 470 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 40 | 40 |
· Design | · 具体设计 | 450 | 440 |
· Coding | · 具体编码 | 5600 | 6040 |
· Code Review | · 代码复审 | 340 | 400 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 220 |
Reporting | 报告 | 140 | 260 |
· Test Report | · 测试报告 | 80 | 200 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 40 | 40 |
合计 | 11720 | 12920 |
二、思考与学习
这次新增的需求如下所示:
第2阶段 实现批量多协议文件下载功能
指定下载地址时
- 可以在参数中指定多个要下载的文件地址,即允许使用多个url参数
- 可以将这些地址放入一个文本文件中,指定从该文件中读取。参数
--input filename, -i filename filename with multiple URL
- 支持使用正则表达式来生成多个下载地址,
同时,除支持http/https协议外,也能够支持如ftp, bittorrent,磁力链等。
首先要求我们实现多个文件的下载,这个好办,在我们已经实现的单文件的下载的基础上,只要多次执行就可以了
从文本中读取也只是一个Scanner的事,so easy
有规律的生成下载地址,这貌似用不上正则?有点杀鸡扭牛刀的感觉(
看来最后的要求才是重点
同时,除支持http/https协议外,也能够支持如ftp, bittorrent,磁力链等。
这,这是什么…太..太强了,我现在溜还来得及吗
三、内容实现
1、核心思路
整个代码的核心逻辑和之前一样,如下所示
但是我们对代码进行了更高级的抽象,使相同的思路可以适应更广泛的情况
重新进行抽象,对代码重构之后的类图如下所示
我们抽象出了更高层级的协议接口,使用不同的方式实现接口,达到使用不同协议下载的目的
这里由于http已经实现过了,我们就不再赘述了,直接从ftp讲起
2、ftp
首先我们需要知道,对于http来说,传输文件只是其中的一个功能,而ftp则是专门用来传输文件的,换句话说,这位是专业的。不过幸运的是,对于我们这些只需要会怎么下载,怎么多线程下载的人来收,问题简化了不少,我们只需要知道从ftp服务器上下载的步骤就可以了。
通过ftp来下载或上传首先要通过账户密码登录,然后通过特定的命令就可以实现了,而我们使用java的话,Apache的ftpclient
是个不错的选择
考虑到用户使用的便捷性,我们使用一次性输入的原则,只要设置好就可以开始下载。
所以我们的ftp url输入格式如下所示( 我们用splitUrl
方法解析该字符串 ):
ftp://用户名:密码@站点地址
eg: ftp://test1:test1@192.168.1.105:21/game/download.exe
关于一些ftpclient
的基本操作,可以参考这个、这个、还有这个
package Protocol;
import Protocol.Protocols;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class Ftp implements Protocols {
FTPClient ftp = null;
/**
* url格式如下,构造时会进行解析
* ftp://用户名:密码@地址:端口/远程文件存储地址
*/
String url;
String host = null;
int port = 0;
String username = null;
String password = null;
// 远程文件位置
String remote;
public Ftp(String url) {
this.url = url;
splitUrl();
}
@Override
public boolean connect() throws IOException {
ftp = new FTPClient();
// 连接FPT服务器,设置IP及端口
ftp.connect(host, port);
// 设置用户名和密码
ftp.login(username, password);
// 设置连接超时时间,5000毫秒
ftp.setConnectTimeout(5000);
// 设置中文编码集,防止中文乱码
ftp.setControlEncoding("UTF-8");
// 以二进制方式传输
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);
// 检测是否连接成功
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
ftp.disconnect();
return false;
} else {
return true;
}
}
@Override
public long getContentLength() throws IOException {
FTPFile[] files = ftp.listFiles(new String(
remote.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
if (files.length != 1) {
return 0;
}
FTPFile file = files[0];
return file.getSize();
}
@Override
public boolean checkMutiThread() {
return true;
}
@Override
public InputStream getDownloadBlock(long start, long end) throws IOException {
// 设置重新开始位置
ftp.setRestartOffset(start);
InputStream in = ftp.retrieveFileStream(new String(
remote.getBytes(StandardCharsets.UTF_8),
StandardCharsets.ISO_8859_1));
File file = new File(".\\" + this.hashCode());
byte[] bytes = new byte[1024];
OutputStream out = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int readLength;
int downloadSize = 0;
while((readLength=in.read(buffer)) > 0) {
out.write(buffer, 0, readLength);
downloadSize += readLength;
if(downloadSize >= end - start) {
break;
}
}
out.flush();
in.close();
out.close();
ftp.completePendingCommand();
return new FileInputStream(file);
}
@Override
public InputStream getAll() throws IOException {
return null;
}
@Override
public int getType() {
return Protocols.FTP;
}
@Override
public String getSource() {
return url;
}
private void splitUrl() {
String temp = url;
temp = temp.substring(6);
int split = temp.indexOf(':');
username = temp.substring(0, split);
temp = temp.substring(split + 1);
split = temp.indexOf('@');
password = temp.substring(0, split);
temp = temp.substring(split + 1);
split = temp.indexOf(':');
host = temp.substring(0, split);
temp = temp.substring(split + 1);
split = temp.indexOf('/');
port = Integer.parseInt(temp.substring(0, split));
temp = temp.substring(split);
remote = '.' + temp.replace('/', '\\');
}
}
我们会每次用当前类的hashcode为名生成部分文件,在部分文件整合到目标文件后删除部分文件
private void downloadBlock() {
InputStream in = null;
try(RandomAccessFile rfile = new RandomAccessFile(filename, "rwd")) {
rfile.seek(block.getBlockstart());
logger.info("block " + block.getBlockstart() + "-" + block.getBlockend() + " start");
in = protocols.getDownloadBlock(block.getBlockstart(), block.getBlockend());
byte[] buffer = new byte[1024];
int readLength;
while((readLength = in.read(buffer)) > 0) {
rfile.write(buffer, 0, readLength);
}
in.close();
File tmp = new File(".\\" + protocols.hashCode());
if(tmp.exists()) {
Files.deleteIfExists(tmp.toPath());
}
BlockList.getInstance().getList().remove(block);
} catch (IOException e) {
logger.info(String.format("%s-%s failed, has been added into the download list again.",
block.getBlockstart(), block.getBlockend()));
logger.info(e.getMessage());
}
}
3、Torrent
这个就实在是比较复杂了,往下看之前建议先看这两篇博客,可以很好的理解Torrent
https://www.aneasystone.com/
https://blog.jse.li/posts/torrent/
因为它的复杂性,我们又专门为它设计了一个类图,一个流程图
大伙可以先看流程图,从流程图可以很清楚的知道获取Torrent的过程,然后再看类图,就能明白这个设计了
这里也感谢github ttorent:https://github.com/mpetazzoni/ttorrent
这么大个工程没有这个前人项目还真不知道该怎么下手
代码在https://github.com/Jump-Wang-111/MutiThreadDownload
4、Magnet
Magnet实际上是将磁力链转换成torrent文件然后下载的
这里偷懒了没有实现
四、代码质量检测
仍然使用QAPlug进行代码质量检测。
问题很多,咱们一点点解决….
经过数次优化后已经解决了所有的警告
五、软件测试
单元测试使用JUnit4,分支覆盖率使用idea自带测试方法
ftp下载
测试编号 | 输入信息 | 预期结果 |
---|---|---|
201(小文件,1M) | {“url”:”ftp://test1:test1@192.168.1.105:21/FZXGJ_V10.6_XiTongZhiJia.zip”, “ThreadNum”: “8”, “fileName”: “.\\download.zip”} | Success |
202(一般文件,100M) | {“url”:”ftp://test1:test1@192.168.1.105:21/game/download.exe”, “ThreadNum”: “8”, “fileName”: “.\\download.exe”} | Success |
203(大文件,2G) | {“url”:”ftp://test1:test1@192.168.1.105:21/game/android.apk”, “ThreadNum”: “8”, “fileName”: “.\\download.exe”} | Success |
204(设置4线程) | {“url”:”ftp://test1:test1@192.168.1.105:21/game/download.exe”, “ThreadNum”: “4”, “fileName”: “.\\download.exe”} | Success |
205(设置-1线程) | {“url”:”ftp://test1:test1@192.168.1.105:21/game/download.exe”, “ThreadNum”: “-1”, “fileName”: “.\\download.exe”} | Fail |
206(设置100线程) | {“url”:”ftp://test1:test1@192.168.1.105:21/game/download.exe”, “ThreadNum”: “100”, “fileName”: “.\\download.exe”} | Success |
207(非链接) | {“url”:”111222”, “ThreadNum”: “8”, “fileName”: “.\\download.zip”} | Fail |
208(非下载链接) | {“url”:”ftp://test1:test1@192.168.1.105:21/1”, “ThreadNum”: “8”, “fileName”: “.\\download.zip”} | Fail |
209(已存在的文件名) | {“url”:”ftp://test1:test1@192.168.1.105:21/game/download.exe”, “ThreadNum”: “8”, “fileName”: “.\\download.exe”}(执行两次) | Fail |
210(不存在的目录下的文件名) | {“url”:”ftp://test1:test1@192.168.1.105:21/game2/download.exe”, “ThreadNum”: “8”, “fileName”: “.\\new\\download.exe”} | Success |
211(使用单线程下载) | {“url”:”ftp://test1:test1@192.168.1.105:21/game/download.exe”, “ThreadNum”: “8”, “fileName”: “.\\download.exe”}(更改代码使用单线程) | Success |
torrent下载
测试编号 | 输入信息 | 预期结果 |
---|---|---|
301 | {“url”:”.\\【豌豆字幕组&风之圣殿字幕组】★04月新番[鬼灭之刃 Kimetsu_no_Yaiba][01-26][合集][简体][1080P][MP4].torrent”, “ThreadNum”: “8”, “fileName”: “E:\output”} | Success |
302 | {“url”:”.\\BAB1BC7535A1E34E3016152E365488617C6F5C5C.torrent”, “ThreadNum”: “8”, “fileName”: “.\\download.exe”} | false |
303 | {“url”:”.\\【豌豆字幕组&风之圣殿字幕组】★04月新番[鬼灭之刃 Kimetsu_no_Yaiba][01-26][合集][简体][1080P][MP4].torrent”, “ThreadNum”: “4”, “fileName”: “E:\output”} | Success |
304 | {“url”:”.\\【豌豆字幕组&风之圣殿字幕组】★04月新番[鬼灭之刃 Kimetsu_no_Yaiba][01-26][合集][简体][1080P][MP4].torrent”, “ThreadNum”: “-1”, “fileName”: “E:\output”} | Fail |
305 | {“url”:”.\\【豌豆字幕组&风之圣殿字幕组】★04月新番[鬼灭之刃 Kimetsu_no_Yaiba][01-26][合集][简体][1080P][MP4].torrent”, “ThreadNum”: “100”, “fileName”: “E:\output”} | Success |
306 | {“url”:”111222”, “ThreadNum”: “8”, “fileName”: “E:\output”} | Fail |
307 | {“url”:”aaabbb.torrent”, “ThreadNum”: “8”, “fileName”: “.\\download.zip”} | Fail |
308 | {“url”:”.\\【豌豆字幕组&风之圣殿字幕组】★04月新番[鬼灭之刃 Kimetsu_no_Yaiba][01-26][合集][简体][1080P][MP4].torrent”, “ThreadNum”: “100”, “fileName”: “E:\output”}(执行两次) | Fail |
309 | {“url”:”.\\【豌豆字幕组&风之圣殿字幕组】★04月新番[鬼灭之刃 Kimetsu_no_Yaiba][01-26][合集][简体][1080P][MP4].torrent”, “ThreadNum”: “100”, “fileName”: “E:\output\output”} | Success |
magnet下载
测试编号 | 输入信息 | 预期结果 |
---|---|---|
401 | {“url”:”magnet:?xt=urn:btih:f3215557bd5c1dc5dcf222df457ad56fd8dd8eb9”, “ThreadNum”: “8”, “fileName”: “E:\output”} | Success |
402 | {“url”:”magnet:?xt=urn:btih:99d92b6927216c190c72ebfc64d4f343f2a6a05e”, “ThreadNum”: “8”, “fileName”: | Success |
403 | {“url”:”magnet:?xt=urn:btih:99d92b6927216c190c72ebfc64d4f343f2a6a05e”, “ThreadNum”: “4”, “fileName”: “E:\output”} | Success |
404 | {“url”:”magnet:?xt=urn:btih:99d92b6927216c190c72ebfc64d4f343f2a6a05e”, “ThreadNum”: “-1”, “fileName”: “E:\output”} | Fail |
405 | {“url”:”magnet:?xt=urn:btih:99d92b6927216c190c72ebfc64d4f343f2a6a05e”, “ThreadNum”: “8”, “fileName”: “E:\output”} | Success |
406 | {“url”:”111222”, “ThreadNum”: “8”, “fileName”: “E:\output”} | Fail |
407 | {“url”:”aaabbb.torrent”, “ThreadNum”: “8”, “fileName”: “.\\download.zip”} | Fail |
408 | {“url”:”magnet:?xt=urn:btih:99d92b6927216c190c72ebfc64d4f343f2a6a05e”, “ThreadNum”: “8”, “fileName”: “E:\output”}(执行两次) | Success |
409 | {“url”:”magnet:?xt=urn:btih:99d92b6927216c190c72ebfc64d4f343f2a6a05e”, “ThreadNum”: “8”, “fileName”: “E:\output\output”} | Success |
测试实在太多了,我们这里只举出分支覆盖的一个例子
分支覆盖率如下所示
主要用到了download和Protocol中的代码
六、收获
第二阶段比第一阶段不知道按了多少,尤其是torrent的部分,真的很难,但是收获也非常丰厚,包括单例模式和观察者模式,通过这次实践都能够掌握了,而且也训练了阅读和书写大规模代码的能力
另外,重构屎山真的会死人的,想得开的都别试,真的