软工大作业——多线程下载(二)

2022-03-14

这篇文章主要是关于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、核心思路

整个代码的核心逻辑和之前一样,如下所示

swimming1.drawio

但是我们对代码进行了更高级的抽象,使相同的思路可以适应更广泛的情况

重新进行抽象,对代码重构之后的类图如下所示

我们抽象出了更高层级的协议接口,使用不同的方式实现接口,达到使用不同协议下载的目的

leitu_2.drawio

这里由于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

这么大个工程没有这个前人项目还真不知道该怎么下手

torrent_class.drawio

torrent_liucheng.drawio

代码在https://github.com/Jump-Wang-111/MutiThreadDownload

4、Magnet

Magnet实际上是将磁力链转换成torrent文件然后下载的

这里偷懒了没有实现

四、代码质量检测

仍然使用QAPlug进行代码质量检测。

问题很多,咱们一点点解决….

image-20220210183822217

经过数次优化后已经解决了所有的警告

image-20220117122911982

五、软件测试

单元测试使用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中的代码

image-20220210193414581

六、收获

​ 第二阶段比第一阶段不知道按了多少,尤其是torrent的部分,真的很难,但是收获也非常丰厚,包括单例模式和观察者模式,通过这次实践都能够掌握了,而且也训练了阅读和书写大规模代码的能力

​ 另外,重构屎山真的会死人的,想得开的都别试,真的