基于TDD思想编一个多线程下载的项目(下)

上一讲我们通过两个测试用例,将多线程下载的链接工具类给写好了,当确定可以链接到网络端,接下来我们需要继续通过几个测试用例,驱动我们完成多线程下载任务的代码。
下载的测试用例:

public void testDownload() {
                //String url = "http://www.hinews.cn/pic/0/13/91/26/13912621_821796.jpg";

        String url = "http://images2015.cnblogs.com/blog/610238/201604/610238-20160421154632101-286208268.png";

        FileDownloader downloader = new FileDownloader(url,"e:\\项目练手\\test.jpg");


        ConnectionManager cm = new ConnectionManagerImpl();
        downloader.setConnectionManager(cm);

        downloader.setListener(new DownloadListener() {
            @Override
            public void notifyFinished() {
                downloadFinished = true;
            }

        });


        downloader.execute();

        // 等待多线程下载程序执行完毕
        while (!downloadFinished) {
            try {
                System.out.println("还没有下载完成,休眠五秒");
                //休眠5秒
                Thread.sleep(5000);
            } catch (InterruptedException e) {              
                e.printStackTrace();
            }
        }
        System.out.println("下载完成!");



    }

首先看到这个测试用例,我们给FileDownloader类传入两个参数,就是要获取资源的地址和下载地址得字符串,这样,我们先编写FileDownloader类的构造函数如下:

public FileDownloader(String _url, String localFile) {
        this.url = _url;
        this.localFile = localFile;

    }

然后我们实例化刚才编好的连接类(ConnectionImpl类),将这个类传进去,连接上我们需要下载的URL,

public void setConnectionManager(ConnectionManager ucm){
        this.cm = ucm;
    }

由于是多线程下载, 调用这个类的客户端不知道什么时候结束,所以你需要实现当所有
线程都执行完以后, 调用listener的notifiedFinished方法, 这样客户端就能收到通知。相当于一个监听器。

我们将这个的接口写出来:

package com.coderising.download.api;

public interface DownloadListener {
    public void notifyFinished();
}

如果复写的方法中notifyFinished()返回true,则证明所有的线程下载完毕,这样就能实现我们所需要的功能,

public void setListener(DownloadListener listener) {
        this.listener = listener;
    }

这样,我们就可以实现execute的编写了,
*具体的实现思路:
1. 需要调用ConnectionManager的open方法打开连接, 然后通过Connection.getContentLength方法获得文件的长度
2. 至少启动3个线程下载, 注意每个线程需要先调用ConnectionManager的open方法
然后调用read方法, read方法中有读取文件的开始位置和结束位置的参数, 返回值是byte[]数组
3. 把byte数组写入到文件中
4. 所有的线程都下载完成以后, 需要调用listener的notifiedFinished方法*
首先我们先要介绍几个比较有用的工具类:
首先是CyclicBarrier:
1.CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
2. CyclicBarrier就象它名字的意思一样,可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍。
3. CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行
详细介绍见http://blog.csdn.net/shihuacai/article/details/8856407
execute方法如下

public void execute(){

        CyclicBarrier barrier = new CyclicBarrier(DOWNLOAD_TRHEAD_NUM , new Runnable(){
            public void run(){
                listener.notifyFinished();
            }
        }); 

        Connection conn  = null;
        try {

            conn  = cm.open(this.url);

            int length = conn.getContentLength();   

            createPlaceHolderFile(this.localFile, length);          

            int[][] ranges = allocateDownloadRange(DOWNLOAD_TRHEAD_NUM, length);

            for(int i=0; i< DOWNLOAD_TRHEAD_NUM; i++){


                DownloadThread thread = new DownloadThread(
                        cm.open(url), 
                        ranges[i][0], 
                        ranges[i][1], 
                        localFile, 
                        barrier);

                thread.start();             
            }

        } catch (Exception e) {         
            e.printStackTrace();
        }finally{
            if(conn != null){
                conn.close();
            }
        }

    }

其中,这个方法里抽象出来两个小方法,分别是占位方法和分配线程下载数据字节方法:

private void createPlaceHolderFile(String fileName, int contentLen
) throws IOException{

        RandomAccessFile file = new RandomAccessFile(fileName,"rw");

        for(int i=0; i<contentLen ;i++){
            file.write(0);
        }

        file.close();
    }

    private int[][] allocateDownloadRange(int threadNum, int contentLen){
        int[][] ranges = new int[threadNum][2];

        int eachThreadSize = contentLen / threadNum;// 每个线程需要下载的文件大小
        int left = contentLen % threadNum;// 剩下的归最后一个线程来处理

        for(int i=0;i<threadNum;i++){

            int startPos = i * eachThreadSize;

            int endPos = (i + 1) * eachThreadSize - 1;

            if ((i == (threadNum - 1))) {
                endPos += left;
            }
            ranges[i][0] = startPos;
            ranges[i][1] = endPos;

        }

        return ranges;
    }

其中,线程类的编写如下:

package com.coderising.download;

import java.io.RandomAccessFile;
import java.util.concurrent.CyclicBarrier;

import com.coderising.download.api.Connection;

public class DownloadThread extends Thread{

    Connection conn;
    int startPos;
    int endPos;
    CyclicBarrier barrier;
    String localFile;
    public DownloadThread( Connection conn, int startPos, int endPos, String localFile, CyclicBarrier barrier){

        this.conn = conn;       
        this.startPos = startPos;
        this.endPos = endPos;
        this.localFile = localFile;
        this.barrier = barrier;
    }
    public void run(){  


        try {
            System.out.println("Begin to read [" + startPos +"-"+endPos+"]");

            byte[] data = conn.read(startPos, endPos);      

            RandomAccessFile file = new RandomAccessFile(localFile,"rw");

            file.seek(startPos);    

            file.write(data);

            file.close();

            conn.close();

            barrier.await(); //等待别的线程完成

        } catch (Exception e) {         
            e.printStackTrace();

        } 

    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值