基础知识

HTTP206状态码

客户端可以在HTTP头中添加range以获取请求对象的部分内容,如果服务器支持部分内容请求,则会返回206状态码。也可通过研判服务器Response的HTTP头,于Accept-Ranges值来判断服务器是否支持部分内容请求。

这是客户端请求头,可以见到其中的Range:
这是服务器回复,可以见到状态码、类型为部分内容(Partial Content)及内容长度等:

多线程技术

进程是爹妈,管着众多线程儿子。单进程可以看作是当前的应用程序,其下可以有多个线程来执行不同的逻辑,且线程共享整个进程的资源。

线程是CPU调度的基本单元,CPU上每一时刻只有一个线程在执行,而通过系统的线程调度算法可以使线程在宏观上实现类似并发执行的效果,因此一般情况下单个线程不会阻塞应用程序中负责其他功能的线程。用吃饭举个例子,单进程多线程相当于多人在一张桌子上吃饭,他们共享整个餐桌的资源而“同时”进行着各自的工作。

多线程下载的实现

思路

构建一个HTTP请求,依服务器回复判断其是否支持部分内容请求。

  • 如支持,则初始化线程池,构建用户设定的线程数,计算每个线程需要负责的文件流起点与终点,初始化线程开始下载,全部下载完成后依据下载日志合并各线程下载的文件。
  • 如不支持,则初始化一个线程,下载对应文件。

为了节省开支,单线程初始化一定要能复用,即每个线程可以多次使用,避免每次下载都创建新的类与线程造成不必要的开销。

程序设计

程序最核心的内容是多线程下载模块,其简要的UML图如下:
mtdurl
解释:

  • SingleThreadDownloader:单线程下载类,包含一个线程引用,初始化方法等。
  • MTDController:多线程下载控制类,包含一个单线程链表,初始化方法,分配方法等。

MTDController会将传入的参数进行解析,生成一个或多个单线程类,并为其分配计算后的工作(例如:线程0负责0-49字节,线程1负责50-100字节),并让线程们开始进行。
请注意:这只是一个原理Demo,许多功能并未添加。

重要代码

线程运行参数类:
class MTDThreadInfo
{
    public HttpWebRequest MTDRequest { get; set; }
    public string MTDFileName { get; set; }
    public long MTDFileSize { get; set; }
    public string MTDFilePath { get; internal set; }
}
单线程下载类:
class SingleThreadDownoad{
        float progress = 0;
        private Thread mThread;
        private HttpWebRequest mRequest;
        private MTDThreadInfo mMtdThreadInfo;

        public void StartDownLoad()
        {
        //传递下载参数,开始异步执行线程
            mThread.Start(mMtdThreadInfo);
        }

        public void Start(object mtdInfo)
        {
            string sb;
            MTDThreadInfo mMTDInfo = mtdInfo as MTDThreadInfo;
            if (mMTDInfo == null)
            {
                //传入参数错误
                return;
            }
            HttpWebRequest tRequest = mMTDInfo.MTDRequest;
            //设置保存路径
            sb = mMTDInfo.MTDFilePath;

            //获取接受内容的长度
            long fileSize = tRequest.GetResponse().ContentLength;

            if (fileSize > 0)
            {

                //TODO 写入日志模块 文件分段信息

                //创建下载文件
                FileStream fs = File.Open(sb.ToString(), FileMode.Create);

                byte[] mByte = new byte[512];
                //获取返回流
                using (Stream bs = tRequest.GetResponse().GetResponseStream())
                {
                    int nreadsize = bs.Read(mByte, 0, 512);
                    while (nreadsize > 0)
                    {
                        fs.Write(mByte, 0, nreadsize);
                        fs.Flush();
                        nreadsize = bs.Read(mByte, 0, 512);
                    }

                    fs.Close();
                    bs.Close();

                    //TODO 通知完成

                    tRequest.Abort();
                }

            }
        }

        public void Init(string url, string fileName, long start = 0, long end = 0)
        {

            //创建HTTP请求
            mRequest = WebRequest.Create(url) as HttpWebRequest;
            mRequest.Referer = url;
            //添加标头
            if (end != 0)
            {
                mRequest.AddRange(start, end);
            }

            //设置线程信息类参数
            mMtdThreadInfo = new MTDThreadInfo();
            mMtdThreadInfo.MTDRequest = mRequest;
            mMtdThreadInfo.MTDFileSize = end - start;
            mMtdThreadInfo.MTDFileName = fileName;

            mMtdThreadInfo.MTDFilePath = MTDCommonData.IOPath.savePath + fileName;
        //线程创建参数以执行Start方法
            ParameterizedThreadStart parameterStart = new ParameterizedThreadStart(Start);
            //创建线程
            mThread = new Thread(parameterStart);

        }

        public float GetProgress()
        {
        //TODO View层刷新
            return this.progress;
        }

        public void Destroy()
        {
            //线程停止运行
            //TODO 后续处理
            mThread.Abort();
        }
    }
多线程下载控制类关键代码
/// <summary>
        /// 初始化线程,分配分段任务
        /// </summary>
        /// <param name="threadNum"></param>
        public void InitThread(int threadNum)
        {
            //大小索引
            long fileSizeIndex = 0;
            //最后剩余大小
            long lastSize = mFileSize % threadNum;
            //平均大小
            long avgSize = mFileSize / threadNum;
            //TODO 判断服务器不支持分段下载的情况
            for (int i = 0; i < threadNum; i++)
            {
                //初始化每个线程
                if (i == (threadNum - 1) )
                {
                    //最后一个线程
                    mThreads[i].Init(mFileUrl, mFileName + "_" + i, fileSizeIndex, mFileSize - 1);
                    //TODO 日志模块记录如下信息
                    sw.WriteLine("[{3}]线程{0}:{1}字节-{2}字节,开始执行",i, fileSizeIndex, mFileSize - 1, DateTime.Now);
                    sw.Flush();
                    break;   
                }
                mThreads[i].Init(mFileUrl, mFileName + "_" + i, fileSizeIndex, (fileSizeIndex + avgSize - 1));
                //TODO 日志模块记录如下信息
                sw.WriteLine("[{3}]线程{0}:{1}字节-{2}字节,开始执行",i,fileSizeIndex,fileSizeIndex + avgSize - 1, DateTime.Now);
                sw.Flush();
                fileSizeIndex += avgSize;
            }
            sw.Close();
            fs.Close();

            for (int i = 0; i < threadNum; i++)
            {
                //开始执行线程
                mThreads[i].StartDownLoad();
            }
            //TODO 根据对应值更新View

            //TODO 完成后合并文件
        }

        /// <summary>
        /// 添加一个下载线程
        /// </summary>
        /// <param name="url"></param>
        /// <param name="fileName"></param>
        /// <param name="filePath"></param>
        /// <param name="start"></param>
        /// <param name="end"></param>
        public void InitThread(string url,string fileName,long start,long end)
        {
            SingleThreadDownoad st = new SingleThreadDownoad();
            st.Init(url, fileName ,start, end);
            mThreads.Add(st);
        }

初始化线程链表等略,注意如果链表已存在则判断其存储的单线程类数量是否满足要求。

其他要点
  • C#默认HttpWebRequest的最大线程数为2,使用如下代码修改最大连接数
ServicePointManager.DefaultConnectionLimit = 10;
  • 注意文中的TODO工作列表
  • 注意补全MTDcontroller

运行结果

测试代码如下:

MTDController mtdCtrl = new MTDController();
mtdCtrl.Init("http://img.xjh.me/desktop/bg/acg/58102922_p0.jpg", 4);
mtdCtrl.StartDownload();

经过多线程下载在预定位置得到了四个文件(文件合并在TODO列表中,待完善)
结果查看测试用的下载日志
下载日志使用HEX查看分段内容与源文件是否相符
HEX文件合并功能还在TODO列表中,先使用HEX合并四个文件
文件合并预览结果
预览结果

引用资料

  1. プログラミング言語擬人化計画!
  2. 岁月小筑随机图片站

我来吐槽

*

*

8位绅士参与评论

  1. Mashiro03-20 00:39 回复

    说来你可能不信,截图为证,你是不是被盯上了 🙂
    ![miao.gif](https://view.moezx.cc/images/2018/03/20/miao.gif)

    • 野兔03-20 08:09 回复

      没有提供get参数就跳转到公安部…聊胜于无

  2. c0sMx03-18 11:08 回复

    图片拿走~

  3. littleplus03-16 14:48 回复

    吓一跳,看着代码好好的,突然出现了自己网站的URL(阴险)

  4. 老陈网志03-16 14:10 回复

    博主是女博主嘛~

  5. 北海03-11 17:18 回复

    emmm 看你们都这么勤奋 我感觉我的博客都难产了!哈哈哈

  6. 冯小贤03-07 17:47 回复

    惊现一名白学家,打死再说(#滑稽)

  7. mikusa03-07 07:47 回复

    一脸懵逼地路过…