在Delphi中如何使用TTask并行程序进行多线程下载

        好的,各位小伙伴们!今天我给大家带来一次非常重大的软件工程。

        想必,每个人在制作自己的小软件的时候,总是或多或少的需要从网络上下载一些资源。例如我们的软件自动更新,就需要从网络上下载一次更新包,然后再应用至本地。

        那么,有些文件包在网络上获取的速度属实是有点小慢,如果使用单线程下载,速度简直太感人了。难道不是吗?况且有些情况甚至无法使用浏览器进行Get请求。【打个比方,如果我做一个自动下载器,如果从网络上Get的速度比我用浏览器自己下的速度还要慢,那情何以堪?】

        于是,我通宵达旦,从网络上汲取了一大堆的知识之后,创作出了这篇【原创文章】,如有雷同纯属巧合。大家如果要在别的论坛上引用这一篇文章,记得给这个原本的文章打个标记哦!

        本文章使用的编程语言为Delphi语言,如果有需要使用C#或者Java的话,大家可以借鉴本文章,然后使用你们熟悉的语言变过去就好了。本文章著作的前提是大家知道并且会写、熟悉Delphi这门语言,当然,对于别的编程语言比较熟悉也可以看。

        首先,我们要了解时代的变迁。这一则内容摘自这个部分,正如文章中说道:TThread是单核时代的产物,TTask是多核时代的产物。TTask的执行速度要比TThread快很多。我从网络上看到的一些文章几乎都是在说用TThread制作多线程工具,但我从来不这么认为。我认为软件就应该随着时代的变迁而变得更好。

        我们来看看刚刚文章中对于TTask的描述吧:TTask【任务】,TTask为【任务对象】,ITask为【任务实例】,也就是说,TTask是指的是类,ITask指的是TTask部分方法的返回值。而ITask中也封装了不少的方法。因此,我们来简单的看看网站中对于TTask的描述吧。

        哦,对了,在使用TTask之前,我们需要引用头文件【System.Threading】这个在PPT中有说到。

        1.直接运行一个任务:【TTask.Run(<TProc>)】,参数为一个过程参数。并且这个过程必须是无参数的。这样的话,主线程会运行一次Run,然后生成另外一个线程用于执行Run中的方法。然后主线程不会阻塞在这里,将会接着往下执行。

        2.创建一个任务实例,然后直接运行:【var aTask := TTask.Create(<TProc>); aTask.Start;】,我们只需要了解,以上Run与Create接受的参数均为一个过程。【其实后面还有一个跟着【对象和事件】的参数,】,但我们在这里无需讨论这些。

        我们往下翻一翻这个PPT,里面还有一个array of ITask的方法,通过创建一个任务实例数组,我们可以轻松的对此进行等待输出。

        看到底下,有一个TTask.WaitForAll(<array of TTask>)的一个方法,这是个TTask的方法。请记住,这个方法必须得慎用。如果不慎用的话,可能会造成严重的后果。让我们来看看下面一串代码,给大家看看错误的用法:【相比大家看了刚刚的第一点应该能预料到,TTask可以使主线程不会阻塞,除非遇到了TTask.WaitForAll,看了刚刚文章的PPT应该能够了解,这个WaitForAll的意思就是等待所有的线程全部执行完毕之后再执行以下语句。】

uses Winapi.Windows, System.Threading;
procedure doTask;
var
  alist: array of ITask;
begin
  SetLength(alist, 3);
  var pc: TProc := procedure
  begin
    showmessage('Hello World!');
  end;
  for var I := 0 to 2 do alist[I] := TTask.Run(pc);
  TTask.WaitForAll(alist);
  showmessage('Finish!');
end;

        那么大家看看上面这个用法有什么毛病么?

        毛病晚点再说,刚刚大家看到我这个示例,想必大家应该知道如何创建一个实例了吗?就拿上面var出过程的这个语句中来看,大家看,我显式声明了过程的类型为TProc,为什么要显式声明呢?因为有个很严重的问题,TTask的Run方法中只能输入进一个【没有任何参数的过程方法】,不是函数啊,函数是有返回值的,而这里是没有返回值的。

        而TProc就是一个没有参数的一个过程创建的变量,因此可以被TTask.Run识别。

        那么我们再来看看上述代码有没有什么错误呢?大家对此的第一印象是什么呢?

        是不是会以为程序会首先生成3个Hello World!消息框之后,等待玩家全部点击完毕了,再执行Finish!消息框?大家可以先猜猜看,然后再看下面的解析。

|

|

|

|

|

        但是事实并不是这样的!此程序运行之后,窗口会立即卡死,除非强行退出,否则不会出现任何消息框或者往下执行。记住,是一旦执行到这个方法立刻就会卡死的那种。

        为什么会这样呢?请听我娓娓道来!

        原因其实很简单,TTask.WaitForAll方法是阻塞掉主线程然后用于等待其内部的所有线程全部执行完毕之后再放开主线程。如果所需要执行的线程为信息框这种本身就能够阻塞线程的代码,再加上WaitForAll阻塞主线程,因此,会出现以下情况:

        for循环生成了3个消息框,消息框分别阻塞了三个新生成的线程,然后TTask.WaitForAll迟迟等不到新生成的线程的完结,因此将会一直阻塞着主线程,但是message框弹出的时候是需要时间的,我们这里弹出的消息框太少了,因此消息框还没有弹出来,for循环就执行完毕了,直接开始WaitForAll阻塞主线程,导致了窗口卡死。

        那么解决方案是什么呢?答案是:用另一个TTask的过程包住这部分代码。具体请看以下代码:

uses Winapi.Windows, System.Threading;
procedure doTask;
var
  alist: array of ITask;
begin
  var wid: TProc := procedure
  begin
    SetLength(alist, 3);
    var pc: TProc := procedure
    begin
      showmessage('Hello World!');
    end;
    for var I := 0 to 2 do alist[I] := TTask.Run(pc);
    TTask.WaitForAll(alist);
    showmessage('Finish!');
  end;
  TTask.Run(wid);
end;

        大家看,我们用了一个新的TTask包住了这一层代码,现在大家可以再次运行看看,是否成功的执行【输出了3个Hello World!消息框,并且当用户全部点击完毕之后,才会弹出Finish框。】

        那么这是为什么呢?因为主线程跳过了方法,执行TTask.Run(wid)的时候,就已经将主线程释放掉了。既然已经释放掉了,TTask里面再怎么执行也再也不会影响窗口锁定了。

        好了,教了大家这么多关于TTask的知识,下面就让我们来正常的敲代码吧!!!

        首先,依旧的,我们需要在文件的最顶部的type下面新建一个类,类名就叫【TDownload】,但是你们可以随意。然后直接class,不需要继承任何类,直接输入end;。当然,你们也可以直接在Form的那个type里面直接建。或者如果你想的话,你甚至可以直接在Form里面写,就无需新建类了。一切都按照你们自己的想法来做就可以了。

        然后的话,我们在里面写几个过程与函数,具体的代码见下面:【代码均标有注释】

TDownload = class
  function GetURLFileName(aURL: string): string; //获取网络文件名称【此函数可有可无,大家酌情选择。参数:下载地址】
  function GetFileSize(aURL: String): Integer; //获取网络文件大小【参数:下载地址】
  function GetHTTPForRange(url: String; tstart, tend: Integer): TStringStream; //在网络上通过start和end分段下载文件的函数【参数:下载地址、起始位置、结束位置】
public
  constructor InitializeDownload(downurl, spath: String; bthr: Integer); //创建一个TDownload的构造函数。【参数:下载地址、保存路径、最大线程】
  procedure StartDownload; //直接开始下载。
private
  url, path: String; //下载地址、保存路径
  thr: Integer; //最大线程
end;

        新建好这些东西之后,我们便可以继续往下写了。此时此刻绝对会有报错的,如果你不是刚刚接触Delphi的新手的话。

        下面,让我们先来写一次GetURLFileName吧,这个函数是我自己构思自己想的,目的就是提取出URL的文件后缀名。让我给大家举个例子:【https://www.offeu.com/utf8.txt】【先切记,这个网址不是我自己传的网址,是我从别人那里找到的。我根本没钱买服务器……】,我们的目的就是提取出这里面的【utf8.txt】这么个名称。

        想必大家已经懂了一大部分,用String提供的Substring就可以了!但是,我还得和大家提点一下以下这种情况【https://www.offeu.com/utf8.txt?q=1234567】,如果我在网址末尾加上一个问号,里面有一些参数,此时该怎么办呢?

        请看我下面的代码【全程自己思考,绝无抄袭!】

function TDownload.GetURLFileName(aURL: string): string;
begin
  if aurl.LastIndexOf('?') = -1 then aurl := Concat(aurl, '?');
  result := aURL.Substring(aurl.LastIndexOf('/') + 1, aurl.Length - aurl.LastIndexOf('/') - 1 - (aurl.Length - aurl.LastIndexOf('?')));
end;

        这个代码的主要含义,其实就是按照网址的一般特性来看的。参数就填入网络url地址就可以了。

        首先,【/】号,整个网址中只有表示http路径时才会有,而?后面的文字是不可能出现/号的。然后?号也是只存在于网址后缀末尾,用于表示网址参数。

        但有些网址末尾根本就没有名字,只有一个斜杠,这种网址一般不是用来下载的,大家也就自己知足就好了!!!

        我们的目的,其实就是截取网址最后一个斜杠与?之间的后缀名。上面的代码我已经截取了。大家看看能理解就好了。

        好了,那么下一个,我们就要获取网络文件大小了。首先,我们创建一个下面这个函数:

function TDownload.GetFileSize(aURL: String): Integer;
begin
  var http := TNetHTTPClient.Create(nil); //建立并且初始化一个TNetHTTPClient。
  result := 0; //设定初始返回值为0
  try
    with http do begin
      AcceptCharSet := 'utf-8'; //设置传输编码为utf-8
      AcceptEncoding := '65001'; //设置传输编码代号为65501
      AcceptLanguage := 'en-US'; //设置传输语言为英语【当然也可以为中文zh-CN,但是不建议。】
      ResponseTimeout := 200000; //设置传输超时为3分20秒。其实就是20万毫秒。
      ConnectionTimeout := 200000; //设置连接超时为3分20秒
      SendTimeout := 200000; //设置发送超时为3分20秒【这里其实不必要设置,因为我们只是获取大小,并不是使用Put传输。Post和Get均不需要设置这个。这里可选哦!】
      SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13]; //设置传输协议,可以写很多个,甚至可以写完!
      HandleRedirects := True;  //可以网址重定向
    end;
    try
      result := http.Head(aURL).ContentLength; //直接获取传输大小
    except
      messagebox(Form1.Handle, '获取网络大小失败,请重试!', '获取网络大小失败', MB_ICONERROR);
      exit;
    end;
  finally
    http.Free; //释放资源
  end;
end;

        这个其实非常简单,创立一个TNetHTTPClient,为什么要TNetHTTPClient而不用TIdHttp呢?因为TIdHttp需要Indy附属,而这个附属是两个dll,需要被附带在exe两侧,因此我们不需要用TIdHttp。我们需要无需两个dll的!参数也是填入网络url地址。

        但我们发现,当我们使用TNetHTTPClient的时候,程序会有一次报错,原因也很简单,我们没有引用头文件。我们回到窗口部分,在右下角组件栏中,搜索TNetHTTPClient,然后拖一个组件到窗口上,然后再右键删除这个组件【为什么我们不直接在窗口中使用这个组件呢?因为我们每一次Get请求,我们都需要对Get请求后的资源进行Free释放。释放后的资源除非重新Create,否则一旦调用则报错。这可比我们在代码里初始化一次坏多了。】

        然后,给初始化过的TNetHTTPClient进行一次属性设置。具体设置参数请看我上面代码的注释。with <变量名> do begin end;这个函数但凡是个刚接触的新手都会的,我就不多赘述了。

        紧接着我们用Head对网络文件进行获取长度。然后释放资源。其中,Head的时候需要环绕一个try-except。

        就此,获取网络长度完结!!!

        下面,我们要做关于获取网络文件范围,实现分段下载。请看代码:

function TDownload.GetHTTPForRange(url: String; tstart, tend: Integer): TStringStream;
begin
  var http := TNetHttpClient.Create(nil); //初始化
  var strt := TStringStream.Create('', TEncoding.UTF8, False); //初始化一个流
  result := nil; //将result定义为一个nil,因为返回值就是一个流。
  try
    with http do begin //以下与上面一样。
      AcceptCharSet := 'utf-8';
      AcceptEncoding := '65001';
      AcceptLanguage := 'en-US';
      ResponseTimeout := 200000;
      ConnectionTimeout := 200000;
      SendTimeout := 200000;
      SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13];
      HandleRedirects := True;
    end;
    try
      http.GetRange(url, tstart, tend, strt);  //获取网络文件流。要存储的流放在最后面。start和end放在中间。
    except  
      messagebox(Form1.Handle, '下载失败,请重试!', '下载失败', MB_ICONERROR);
      http.Free;
      abort;
    end;
    result := strt; //将Get后的流作为返回值返回。
  finally
    http.Free; //释放资源
  end;
end;

        此处,我们将此方法的返回值设置为一个流。为什么设置为流呢?因为如果不设置返回值或者返回值为String,可能会导致部分不兼容。不设置返回值的话,只能使用FileStream将文件输出到本地。如果返回值为String或者别的,那么也更加不好存储进本地。因此返回值为一个字符串流是再好不过的了。

        我们用了NetHTTPClient内置的一个函数GetRange进行获取特定位置的HTTP。

        然后,紧接着,我们就可以到另一个阶段了。下一个,TDownload的构造函数!

        这个构造函数无疑是整个程序中最简单的一步了。请看代码:

constructor TDownload.InitializeDownload(downurl, spath: String; bthr: Integer);
begin
  url := downurl; //给url赋值【需要Get的网址】
  path := spath; //给path赋值【需要保存的路径】
  thr := bthr; //给bthr赋值【最大线程数量】
end;

        乍一看,也没那么难。但是下一个,点击按钮开始下载的那一个步骤,就会显得你非常难了在此考虑到有部分使用者是新手,我会尽量的说的简单一点点的!

        还是请看代码:

procedure TDownload.StartDownload;
var
  tmppath: array[0..255] of Char; //创建使用Temp的数组
  DlBiggestTask: array of ITask; //创建一个任务数组,
begin
  var hostthr: TProc := procedure //设置一个最外层Task,用于直接释放主线程,避免卡死。
  begin
    var tme := GetTickCount; //设计一个计时。这里获取到了程序刚刚执行到这里时的时刻。
    fori := true; //设置一个临时true,这里主要是针对于中断下载部分。这里是全局变量啊,是设置在类里面的哦!
    GetTempPath(255, @tmppath); //找到C:/Users/<用户名>/AppData/Local/Temp/,用于存放临时文件。
    SetLength(DlBiggestTask, thr); //给任务数组设置长度。
    var tmp: String := strPas(tmppath); //给Temp文件夹实例化
    var tStream1 := TMemoryStream.Create{('', TEncoding.UTF8, False)}; //创建1号内存流,用于操控保存在内存中的字符串。此种用法类似于TStringStream。
    var tStream2 := TMemoryStream.Create{('', TEncoding.UTF8, False)}; //创建2号内存流。
    var filesize := GetFileSize(url); //获取网络文件大小【这里用到了上面自己写的函数】
    if filesize = 0 then abort; //如果网络文件大小为0的话,则跳出并结束程序。【由于上面的GetFileSize中已经有一个报错信息框了,因此此处不必再输入信息框。】
    if filesize < thr then Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('由于此网站上的文本太小,小于你的最大线程,现在正在判断最佳线程数量以求平衡。'); //当此网络上的文本大小小于最大线程,开始判断。
    for var I := thr downto 0 do begin //此处判断最小线程具体应该是多少。总不可能让网络Get请求一个只有0B数据的文件吧,况且之后的trunc计算也会出错。【这里的downto大家应该理解是什么意思,就不多说了。】
      if filesize < I then begin //如果文件小于循环数,则执行,反之则继续。
        thr := I; //将最大线程重置为循环数
        break; //跳出循环
      end;
    end;
    var fileavg := trunc(filesize / thr); //记录网络文件除以最大线程后的平均值。
    var sc := 0;//定义临时变量1
    var sf := 0;//定义临时变量2
    if not DirectoryExists(Concat(tmppath, 'DownloadTool\')) then //在Temp中新建一个文件夹用于接收临时文件【这个临时文件后面会说到什么意思。】
        ForceDirectories(Concat(tmppath, 'DownloadTool\')); //强制一个新的文件夹
    Form1.ProgressBar1.Max := thr; //将进度条框的最大值设为最大线程。
    var dproc := procedure(dt: Integer) //定义下层过程,这里会运用到之前将的阻塞线程的故事。这里有个参数是为了给线程进行编号。后面有说实现方法。
    begin
      var tstart := fileavg * (dt - 1); //用参数dt给网络获取文件起始值赋值。
      var tend := fileavg * dt - 1; //用参数dt给结束值赋值。
      if dt = thr then tend := filesize; //如果参数dt等于最大线程了,就给结束值直接赋值成文件总长度。
      var svpath := Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(dt), '.tmp'); //这里是定义一个文件保存路径,用到了path与编号。将其命名成【<文件名>-<编号>.tmp】文件保存到Temp文件夹中。
      var stt: TStringStream := GetHttpForRange(url, tstart, tend);//定义一个字符串流,用于直接Get网络上特定位置的流。
      if not fori then exit; //如果fori等于false了的话,则中断。
      if stt = nil then begin //如果流的返回值为nil,则意味着Get失败,将以下的抛出即可。
        Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('分段下载已下载失败:', inttostr(dt))); //给列表框添加一句下载失败。
        for var c := 1 to thr do deleteFile(Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(c), '.tmp')); //删掉所有tmp文件,以保证以后还能接着下载。
        Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('已检测出文件下载并不完整,下载失败!'); //给列表框添加一段新的话。
        abort; //抛出报错返回。
      end else begin
        if FileExists(svpath) then Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('分段下载文件已存在:', inttostr(dt))) //如果此tmp文件已经存在在Temp文件夹中,则显示存在并且继续往下执行。
        else begin //如果既不存在又获取成功的话,则将tmp文件保存在Temp文件夹中。
          stt.SaveToFile(svpath); //此为保存文件。
          Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('分段下载已下载完成:', inttostr(dt))); //此为下载完成的说法
        end;
      end;
      inc(sf); //给临时变量sf自增1,后面只会用到给ProgressBar添加进度条时用。
      Form1.ProgressBar1.Position := sf; //添加进进度条框
      var jd: Currency := 100 * sf / thr; //设置下载进度。这里用100乘以自增sf然后除以最大线程。如果自增达到了与最大线程一样的话,那么就会达到100。
      Form1.Label5.Caption := Concat('下载进度:', floattostr(SimpleRoundTo(jd)), '%');//输出下载进度。给一个标签添加下载进度。使用了保留两位小数。
    end; //以上子任务执行完毕。
    var downp: TProc := procedure
    begin
      inc(sc); //另一个子任务,这里就是给上面的子任务添加参数的方法。而必须使用这种方法。这里给上面的临时变量sc自增1。每次循环都自增1。
      dproc(sc); //这里调用方法,并且将自增后的sc当做参数填入,此处为从1~最大线程的范围。
    end;
    for var I := 0 to thr - 1 do begin //这里用了上面的知识,给任务数组附上ITask的值。这里直接用循环生成【最大线程数量】个任务,进行跑。
      DlBiggestTask[I] := TTask.Run(downp);
    end;
    TTask.WaitForAll(DlBiggestTask); //这里等待所有的线程执行完毕。由于这里是最外层任务的子任务,因此这里的等待不会锁住主线程。
    if not fori then begin //这里添加一个中断下载的判断。如果所有的线程都退出了,并且fori为false,则这里将会显示退出完毕。请记住这个fori为false。后面的中断下载按钮要讲到。
      Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('主线程已退出完毕!可以正常关闭程序了。'); //这里是主线程退出完毕的列表框添加。
      for var I := 1 to thr do deleteFile(Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(I), '.tmp')); //然后直接删掉所有的tmp文件。
      abort; //然后抛出无信息框的报错并返回。
    end;
    try //如果按照以上,你既没有按下中断下载,网络Get请求也没有报错,那么tmp文件则会全部保留下来。
      for var I := 1 to thr do begin //这里直接开始组合文件。需要用到内存流了。用个for循环遍历Temp中的tmp文件,将其输出到内存流中。
        var tmpp := Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(i), '.tmp'); //这里用一个tmpp变量保存所有tmp变量的路径。
        if not FileExists(tmpp) then begin //如果文件不尊在,则输出文件不完整【此处是为了规避某些小问题而准备的。】
          Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('已检测出文件下载并不完整,下载失败!'); //输出文件下载不完整的错误。
          tStream1.Free; //将两个内存流的资源释放掉。
          tStream2.Free;
          exit; //退出函数
        end;
        tStream2.LoadFromFile(tmpp); //如果存在,则将tmpp加载进2号内存流。
        tStream1.Seek(tStream1.Size, soFromBeginning); //然后将1号内存流的大小固定好。
        tStream1.CopyFrom(tStream2, tStream2.Size); //然后将2号内存流的内容复制到1号内存流中。
        tStream2.Clear; //最后将2号内存流的内容清空。
      end; //当所有for循环执行完毕,以下为将1号内存流保存到你需要保存的文件夹当中。
      tStream1.SaveToFile(path); //保存!
    finally
      tStream1.Free; //释放资源
      tstream2.Free;
    end;
    for var I := 1 to thr do deleteFile(Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(I), '.tmp')); //最终删掉所有的tmp文件。
    Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('下载已完成!耗时:', floattostr((GetTickCount - tme) / 1000), '秒')); //这里记录了耗时【但是这里不是很必要,除非你需要耗时。】
    fori := false; //将fori重新设置为false
  end;
  TTask.Run(hostthr); //跑一次最外层任务。
end;

        我的天哪,这个代码也太长了吧!但是不要紧,我都标记了注释的!大家看看就好了。

        这里我也不愿意多说什么了,自己看吧!看完了就动手敲一敲吧!

        对了,还有呢!你以为本文就到此结束了吗?但其实还没有!我们还有窗口没有介绍呢!

       那么这个是我们的窗口,我们使用了众多的Label标签给玩家们做提示。其中还有众多的按钮。其窗口内容简单易学,大家甚至还可以通过学习我的代码做出窗口更加精美的图片。

        首先,我们先来看看,开始下载按钮的代码。我们双击这个按钮,然后调出它的点击事件:

procedure TForm1.Button1Click(Sender: TObject);
begin
  var down := TDownload.InitializeDownload(Edit1.Text, Edit3.Text, strtoint(Edit2.Text));
  if FileExists(Edit3.Text) then begin
    messagebox(Handle, '同级文件夹下已存在相同名字的文件,请删除掉或者移到别的地方后再尝试下载。', '已存在同名文件', MB_ICONERROR);
    exit;
  end;
  if not DirectoryExists(ExtractFilePath(Edit3.Text)) then ForceDirectories(Edit3.Text);
  down.StartDownload;
end;

        这里由于过于简单,我就不打注释了,但凡学过一点点Delphi的小玩家们都知道这些内置函数是什么意思。是的没错,我们创建一个变量调用TDownload中的初始化下载方法,然后传入【Edit1(下载地址)、Edit3(保存位置)、Edit2(最大线程)】

        然后判断同级文件夹下是否存在了相同名字的文件,如果有的话,则说明已存在并要求用户删掉文件之后再下载。

        如果文件夹不存在,则强制生成一个文件夹。

        然后直接调用StartDownload方法,你没听错,直接调用!

        再者我们来看看中断下载的按钮吧:

procedure TForm1.Button2Click(Sender: TObject);
begin
  ListBox1.ItemIndex := ListBox1.Items.Add('主线程正在退出中,如果没有下载任务则装作没有任何事发生。');
  fori := false;
end;

        在列表框里增加一行退出中代码,然后将fori设置为false!你没看错,就是这么简单!!!

        fori是存在于全局变量中的,也就是说,与Form1是存在于同一个位置的!

var
  Form1: TForm1;
  fori: Boolean;

        大致就像这样!因此它是可以被此文件中的所有类共用的!

        接下来我们再来看看退出程序的按钮吧:

procedure TForm1.Button3Click(Sender: TObject);
begin
  Application.Terminate;
end;

        啊哈,一个直接终止所有线程并且强制退出窗口的代码!我感觉就挺棒的!

        好了,接下来我们再来看看清除列表框的按钮:

procedure TForm1.Button4Click(Sender: TObject);
begin
  ListBox1.Items.Clear;
  Label5.Caption := '下载进度:0%';
  ProgressBar1.Position := 0;
end;

        是的,你没看错,就是一个非常简单的将列表框的所有元素清空,然后下载进度换成0,然后进度条的位置换成0。是的,就是这么简单!

        好了那么现在来给大家看看整个程序的完整代码吧:

        

unit TestMethod;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ComCtrls, System.Threading,
  System.Net.URLClient, System.Net.HttpClient, System.Net.HttpClientComponent, System.Math;

type
  TDownload = class
    function GetURLFileName(aURL: string): string;
    function GetFileSize(aURL: String): Integer;
    function GetHTTPForRange(url: String; tstart, tend: Integer): TStringStream;
  public
    constructor InitializeDownload(downurl, spath: String; bthr: Integer);
    procedure StartDownload;
  private
    url, path: String;
    thr: Integer;
  end;
  TForm1 = class(TForm)
	Button1: TButton;
	ProgressBar1: TProgressBar;
	Label1: TLabel;
	Label2: TLabel;
	Edit1: TEdit;
	Edit2: TEdit;
    Edit3: TEdit;
	Label3: TLabel;
	Label4: TLabel;
    Button2: TButton;
    Button3: TButton;
    Label5: TLabel;
    ListBox1: TListBox;
    Button4: TButton;
    NetHTTPClient1: TNetHTTPClient;
    procedure Button4Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure Button3Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure ListBox1Click(Sender: TObject);
  private
  public
  end;
var
  Form1: TForm1;
  fori: Boolean;

implementation

{$R *.dfm}
//获取特定位置的Http流
function TDownload.GetHTTPForRange(url: String; tstart, tend: Integer): TStringStream;
begin
  var http := TNetHttpClient.Create(nil); //初始化
  var strt := TStringStream.Create('', TEncoding.UTF8, False); //初始化一个流
  result := nil; //将result定义为一个nil,因为返回值就是一个流。
  try
    with http do begin //以下与上面一样。
      AcceptCharSet := 'utf-8';
      AcceptEncoding := '65001';
      AcceptLanguage := 'en-US';
      ResponseTimeout := 200000;
      ConnectionTimeout := 200000;
      SendTimeout := 200000;
      SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13];
      HandleRedirects := True;
    end;
    try
      http.GetRange(url, tstart, tend, strt);  //获取网络文件流。要存储的流放在最后面。start和end放在中间。
    except  
      messagebox(Form1.Handle, '下载失败,请重试!', '下载失败', MB_ICONERROR);
      http.Free;
      abort;
    end;
    result := strt; //将Get后的流作为返回值返回。
  finally
    http.Free; //释放资源
  end;
end;
//获取网络文件后缀名
function TDownload.GetURLFileName(aURL: string): string;
begin
  if aurl.LastIndexOf('?') = -1 then aurl := Concat(aurl, '?');
  result := aURL.Substring(aurl.LastIndexOf('/') + 1, aurl.Length - aurl.LastIndexOf('/') - 1 - (aurl.Length - aurl.LastIndexOf('?')));
end;
//获取文件大小的方法
function TDownload.GetFileSize(aURL: String): Integer;
begin
  var http := TNetHTTPClient.Create(nil); //建立并且初始化一个TNetHTTPClient。
  result := 0; //设定初始返回值为0
  try
    with http do begin
      AcceptCharSet := 'utf-8'; //设置传输编码为utf-8
      AcceptEncoding := '65001'; //设置传输编码代号为65501
      AcceptLanguage := 'en-US'; //设置传输语言为英语【当然也可以为中文zh-CN,但是不建议。】
      ResponseTimeout := 200000; //设置传输超时为3分20秒。其实就是20万毫秒。
      ConnectionTimeout := 200000; //设置连接超时为3分20秒
      SendTimeout := 200000; //设置发送超时为3分20秒【这里其实不必要设置,因为我们只是获取大小,并不是使用Put传输。Post和Get均不需要设置这个。这里可选哦!】
      SecureProtocols := [THTTPSecureProtocol.SSL3, THTTPSecureProtocol.TLS12, THTTPSecureProtocol.TLS13]; //设置传输协议,可以写很多个,甚至可以写完!
      HandleRedirects := True;  //可以网址重定向
    end;
    try
      result := http.Head(aURL).ContentLength; //直接获取传输大小
    except
      messagebox(Form1.Handle, '获取网络大小失败,请重试!', '获取网络大小失败', MB_ICONERROR);
      exit;
    end;
  finally
    http.Free; //释放资源
  end;
end;
//开始下载文件
procedure TDownload.StartDownload;
var
  tmppath: array[0..255] of Char; //创建使用Temp的数组
  DlBiggestTask: array of ITask; //创建一个任务数组,
begin
  var hostthr: TProc := procedure //设置一个最外层Task,用于直接释放主线程,避免卡死。
  begin
    var tme := GetTickCount; //设计一个计时。这里获取到了程序刚刚执行到这里时的时刻。
    fori := true; //设置一个临时true,这里主要是针对于中断下载部分。这里是全局变量啊,是设置在类里面的哦!
    GetTempPath(255, @tmppath); //找到C:/Users/<用户名>/AppData/Local/Temp/,用于存放临时文件。
    SetLength(DlBiggestTask, thr); //给任务数组设置长度。
    var tmp: String := strPas(tmppath); //给Temp文件夹实例化
    var tStream1 := TMemoryStream.Create{('', TEncoding.UTF8, False)}; //创建1号内存流,用于操控保存在内存中的字符串。此种用法类似于TStringStream。
    var tStream2 := TMemoryStream.Create{('', TEncoding.UTF8, False)}; //创建2号内存流。
    var filesize := GetFileSize(url); //获取网络文件大小【这里用到了上面自己写的函数】
    if filesize = 0 then abort; //如果网络文件大小为0的话,则跳出并结束程序。【由于上面的GetFileSize中已经有一个报错信息框了,因此此处不必再输入信息框。】
    if filesize < thr then Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('由于此网站上的文本太小,小于你的最大线程,现在正在判断最佳线程数量以求平衡。'); //当此网络上的文本大小小于最大线程,开始判断。
    for var I := thr downto 0 do begin //此处判断最小线程具体应该是多少。总不可能让网络Get请求一个只有0B数据的文件吧,况且之后的trunc计算也会出错。【这里的downto大家应该理解是什么意思,就不多说了。】
      if filesize < I then begin //如果文件小于循环数,则执行,反之则继续。
        thr := I; //将最大线程重置为循环数
        break; //跳出循环
      end;
    end;
    var fileavg := trunc(filesize / thr); //记录网络文件除以最大线程后的平均值。
    var sc := 0;//定义临时变量1
    var sf := 0;//定义临时变量2
    if not DirectoryExists(Concat(tmppath, 'DownloadTool\')) then //在Temp中新建一个文件夹用于接收临时文件【这个临时文件后面会说到什么意思。】
      ForceDirectories(Concat(tmppath, 'DownloadTool\')); //强制一个新的文件夹
    Form1.ProgressBar1.Max := thr; //将进度条框的最大值设为最大线程。
    var dproc := procedure(dt: Integer) //定义下层过程,这里会运用到之前将的阻塞线程的故事。这里有个参数是为了给线程进行编号。后面有说实现方法。
    begin
      var tstart := fileavg * (dt - 1); //用参数dt给网络获取文件起始值赋值。
      var tend := fileavg * dt - 1; //用参数dt给结束值赋值。
      if dt = thr then tend := filesize; //如果参数dt等于最大线程了,就给结束值直接赋值成文件总长度。
      var svpath := Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(dt), '.tmp'); //这里是定义一个文件保存路径,用到了path与编号。将其命名成【<文件名>-<编号>.tmp】文件保存到Temp文件夹中。
      var stt: TStringStream := GetHttpForRange(url, tstart, tend);//定义一个字符串流,用于直接Get网络上特定位置的流。
      if not fori then exit; //如果fori等于false了的话,则中断。
      if stt = nil then begin //如果流的返回值为nil,则意味着Get失败,将以下的抛出即可。
        Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('分段下载已下载失败:', inttostr(dt))); //给列表框添加一句下载失败。
        for var c := 1 to thr do deleteFile(Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(c), '.tmp')); //删掉所有tmp文件,以保证以后还能接着下载。
        Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('已检测出文件下载并不完整,下载失败!'); //给列表框添加一段新的话。
        abort; //抛出报错返回。
      end else begin
        if FileExists(svpath) then Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('分段下载文件已存在:', inttostr(dt))) //如果此tmp文件已经存在在Temp文件夹中,则显示存在并且继续往下执行。
        else begin //如果既不存在又获取成功的话,则将tmp文件保存在Temp文件夹中。
          stt.SaveToFile(svpath); //此为保存文件。
          Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('分段下载已下载完成:', inttostr(dt))); //此为下载完成的说法
        end;
      end;
      inc(sf); //给临时变量sf自增1,后面只会用到给ProgressBar添加进度条时用。
      Form1.ProgressBar1.Position := sf; //添加进进度条框
      var jd: Currency := 100 * sf / thr; //设置下载进度。这里用100乘以自增sf然后除以最大线程。如果自增达到了与最大线程一样的话,那么就会达到100。
      Form1.Label5.Caption := Concat('下载进度:', floattostr(SimpleRoundTo(jd)), '%');//输出下载进度。给一个标签添加下载进度。使用了保留两位小数。
    end; //以上子任务执行完毕。
    var downp: TProc := procedure
    begin
      inc(sc); //另一个子任务,这里就是给上面的子任务添加参数的方法。而必须使用这种方法。这里给上面的临时变量sc自增1。每次循环都自增1。
      dproc(sc); //这里调用方法,并且将自增后的sc当做参数填入,此处为从1~最大线程的范围。
    end;
    for var I := 0 to thr - 1 do begin //这里用了上面的知识,给任务数组附上ITask的值。这里直接用循环生成【最大线程数量】个任务,进行跑。
      DlBiggestTask[I] := TTask.Run(downp);
    end;
    TTask.WaitForAll(DlBiggestTask); //这里等待所有的线程执行完毕。由于这里是最外层任务的子任务,因此这里的等待不会锁住主线程。
    if not fori then begin //这里添加一个中断下载的判断。如果所有的线程都退出了,并且fori为false,则这里将会显示退出完毕。请记住这个fori为false。后面的中断下载按钮要讲到。
      Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('主线程已退出完毕!可以正常关闭程序了。'); //这里是主线程退出完毕的列表框添加。
      for var I := 1 to thr do deleteFile(Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(I), '.tmp')); //然后直接删掉所有的tmp文件。
      abort; //然后抛出无信息框的报错并返回。
    end;
    try //如果按照以上,你既没有按下中断下载,网络Get请求也没有报错,那么tmp文件则会全部保留下来。
      for var I := 1 to thr do begin //这里直接开始组合文件。需要用到内存流了。用个for循环遍历Temp中的tmp文件,将其输出到内存流中。
        var tmpp := Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(i), '.tmp'); //这里用一个tmpp变量保存所有tmp变量的路径。
        if not FileExists(tmpp) then begin //如果文件不尊在,则输出文件不完整【此处是为了规避某些小问题而准备的。】
          Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add('已检测出文件下载并不完整,下载失败!'); //输出文件下载不完整的错误。
          tStream1.Free; //将两个内存流的资源释放掉。
          tStream2.Free;
          exit; //退出函数
        end;
        tStream2.LoadFromFile(tmpp); //如果存在,则将tmpp加载进2号内存流。
        tStream1.Seek(tStream1.Size, soFromBeginning); //然后将1号内存流的大小固定好。
        tStream1.CopyFrom(tStream2, tStream2.Size); //然后将2号内存流的内容复制到1号内存流中。
        tStream2.Clear; //最后将2号内存流的内容清空。
      end; //当所有for循环执行完毕,以下为将1号内存流保存到你需要保存的文件夹当中。
      tStream1.SaveToFile(path); //保存!
    finally
      tStream1.Free; //释放资源
      tstream2.Free;
    end;
    for var I := 1 to thr do deleteFile(Concat(tmppath, 'DownloadTool\', ChangeFileExt(ExtractFileName(path), ''), '-', inttostr(I), '.tmp')); //最终删掉所有的tmp文件。
    Form1.ListBox1.ItemIndex := Form1.ListBox1.Items.Add(Concat('下载已完成!耗时:', floattostr((GetTickCount - tme) / 1000), '秒')); //这里记录了耗时【但是这里不是很必要,除非你需要耗时。】
    fori := false; //将fori重新设置为false
  end;
  TTask.Run(hostthr); //跑一次最外层任务。
end;
//初始化下载
constructor TDownload.InitializeDownload(downurl, spath: String; bthr: Integer);
begin
  url := downurl; //给url赋值【需要Get的网址】
  path := spath; //给path赋值【需要保存的路径】
  thr := bthr; //给bthr赋值【最大线程数量】
end;
//开始下载的按钮
procedure TForm1.Button1Click(Sender: TObject);
begin
  var down := TDownload.InitializeDownload(Edit1.Text, Edit3.Text, strtoint(Edit2.Text));
  if FileExists(Edit3.Text) then begin
    messagebox(Handle, '同级文件夹下已存在相同名字的文件,请删除掉或者移到别的地方后再尝试下载。', '已存在同名文件', MB_ICONERROR);
    exit;
  end;
  if not DirectoryExists(ExtractFilePath(Edit3.Text)) then ForceDirectories(Edit3.Text);
  down.StartDownload;
end;
//中断下载的按钮
procedure TForm1.Button2Click(Sender: TObject);
begin
  ListBox1.ItemIndex := ListBox1.Items.Add('主线程正在退出中,如果没有下载任务则装作没有任何事发生。');
  fori := false;
end;
//退出程序的按钮
procedure TForm1.Button3Click(Sender: TObject);
begin
  Application.Terminate;
end;
//清空列表框的按钮
procedure TForm1.Button4Click(Sender: TObject);
begin
  ListBox1.Items.Clear;
  Label5.Caption := '下载进度:0%';
  ProgressBar1.Position := 0;
end;
//窗口关闭事件
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  canClose := true;
end;
//列表框点击的按钮
procedure TForm1.ListBox1Click(Sender: TObject);
begin
  ListBox1.Hint := ListBox1.Items[ListBox1.ItemIndex];
end;

end.

        总结一下吧

        我犯了一个大错,我本来应该将保存位置的文本框名称为Edit2,最大线程为Edit3的。。。但是我错了。

        现在又有个新的问题了,目前我这个程序里面,保存文件路径是必须填入你需要保存文件路径的绝对路径的,例如【D:/App/Test.zip】,如果大家感兴趣的话,可以用一个按钮打开一个选择保存文件夹的框,然后再放一个文本框填入保存文件名字。【如果不填入则默认为网址后缀。】

        嗷,我忽然发现了,我好想自始至终都没有用过GetURLFileName这个函数诶,我都是直接用Edit3.Text直接就糊弄过去了。

        如果大家感兴趣的话,欢迎来修改我的程序,我非常鼓励大家开发出一款更加美的软件,属于你,也属于大家!

        好了,本文到此结束,谢谢大家!

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Delphi线程池实现多线程FTP分段下载组件 by :renshouren mail:114032666@qq.com QQ:114032666 2019.10.05 使用的组件 1、TIdFTP Indy FTP客户端 2、TThreadsPool 线程池 工作原理及流程 调用本单元,将自动在程序初始化时生成线程池TThreadPoolDown实例 Share_ThreadPool_FTPDown 一、外部调用方法 外部只需要一次性调用 FtpDown() 函数向线程池加入下载任务,下载任务执行的事件会通过调用时注册的 回调函数 AFtpDownEvent 进行通知。 二、内部工作流程 1、FtpDown()函数将调用TThreadPoolDown.AddFtpDown() ,然后调用TADownFileObj.MakeGetFileSizeObj()分配线程任务 本过程,将向回调函数 AFtpDownEvent 触发 HEM_ADDURL 事件通知 2、工作线程调用任务对象TFTPHeadObj.DoThreadExecute 过程获取远程文件大小 备注:该功能实际使用到FTP命令SIZE,该命令一些老版本FTP服务器有可能不支持 本过程,若获取文件大小成功,将向回调函数 AFtpDownEvent 触发 HEM_GETSIZE 事件通知, 若失败,则触发 HEM_ERROR 事件通知 3、得到远程文件大小后,调用TADownFileObj.MakeGetObjs(),分配获取远程文件线程任务 本过程,开始时,将向回调函数 AFtpDownEvent 触发 HEM_WORKBEGIN 事件通知 在接收数据时,向回调函数 AFtpDownEvent 触发 HEM_WORK 事件通知 4、工作线程调用任务对象 TFTPGetObj.DoThreadExecute 实际下载远程文件数据块 每一个数据块下载任务完成后,触发 HEM_BLOCKOK 事件通知 5、所有数据块完成后,将调用 DoDownloadOK 函数,触发 HEM_DOWNOK 事件通知
Delphi,TTask是一个用于处理异步任务的类。它可以让开发者更加方便地处理异步操作,提高程序的并发性能。 TTask使用方法如下: 1.创建TTask对象 可以使用TTask类的静态方法TTask.Run来创建一个TTask对象,例如: ``` var Task: ITask; begin Task := TTask.Run( procedure begin // 处理异步任务 // ... end ); ``` 在上面的代码使用TTask.Run方法创建一个TTask对象,该对象将在后台线程执行指定的匿名方法。 2.等待异步任务完成 如果需要等待异步任务完成后再继续执行后续操作,可以使用TTask.Wait方法等待任务完成,例如: ``` var Task: ITask; begin Task := TTask.Run( procedure begin // 处理异步任务 // ... end ); Task.Wait; // 异步任务完成后的后续操作 // ... end; ``` 在上面的代码使用Task.Wait方法等待异步任务完成后再执行后续操作。 3.异步任务取消 如果需要取消异步任务,可以使用TTask.Cancel方法取消任务,例如: ``` var Task: ITask; begin Task := TTask.Run( procedure begin while not TTask.CurrentTask.Status = TTaskStatus.Canceled do begin // 处理异步任务 // ... end; end ); // 取消异步任务 Task.Cancel; end; ``` 在上面的代码使用Task.Cancel方法取消异步任务。在异步任务的处理程序,需要检查任务是否已被取消,如果已经被取消,应该立即退出任务。 需要注意的是,TTask并不是适用于所有异步操作的最佳方案,有些情况下可能需要使用其他的异步处理方式,例如线程池、异步委托等。在使用TTask时,需要根据具体情况进行选择和优化。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值