编程技术分享平台

网站首页 > 技术教程 正文

lazarus、delphi文件HTP下载断点续传的实现

xnh888 2024-12-18 17:46:10 技术教程 49 ℃ 0 评论

下载大文件时,断点续传是很有必要的,特别是网速度慢且不稳定的情况下,很难保证不出意外,一旦意外中断,又要从头下载,会很让人抓狂。断点续传就能很好解决意外中断情况,再次下载时不需要从头下载,从上次中断处继续下载即可,这样下载几G或十几G大小的一个文件都没问题。本文介绍利用miniframe开源Web框架分别在lazarus、delphi下实现文件HTTP下载断点续传的功能。

本文Demo还实现了批量下载文件,同步服务器上的文件到客户端的功能。文件断点续传原理:分块下载,下载后客户端逐一合并,同时保存已下载的位置,当意外中断再次下载时从保存的位置开始下载即可。这其中还要保证,中断后再次下载时服务器上相应的文件如果更新了,还得重新下载,不然下载到的文件是错了。说明:以下代码lazarus或delphi环境下都能使用。全部源码及Demo请到miniframe开源web框架下载: https://www.wyeditor.com/miniframe/或https://github.com/dajingshan/miniframe。

服务器端代码

文件下载断点续传服务器端很简单,只要提供客户端要求下载的开始位置和指定大小的块即可。

以下是服务器获取文件信息和下载一个文件一块的代码:

<%@//Script头、过程和函数定义
program codes;
%>
 
<%!//声明变量
var
  i,lp: integer;
  FileName, RelativePath, FromPath, ErrStr: string;
  json: TminiJson;
  FS: TFileStream;
  
function GetOneDirFileInfo(Json: TminiJson; Path: string): string;
var
  Status: Integer;
  SearchRec: TSearchRec;
  json_sub: TminiJson;
begin
  Path := PathWithSlash(Path);
  SearchRec := TSearchRec.Create;
  Status := FindFirst(Path + '*.*', faAnyFile, SearchRec);
  try
    while Status = 0 do
    begin 
      if SearchRec.Attr and faDirectory = faDirectory then
      begin
        if (SearchRec.name <> '.') and (SearchRec.name <> '..') then
          GetOneDirFileInfo(Json, Path + SearchRec.Name + '\');
      end else
      begin
        FileName := Path + SearchRec.Name;
        try
          if FileExists(FileName) then
          begin 
            json_sub := Pub.GetJson;  
            json_sub.SO; //初始化 或 json.Init;    
            json_sub.S['filename'] := SearchRec.name;
            json_sub.S['RelativePath'] := GetDeliBack(FileName, FromPath);
            json_sub.S['FileTime'] := FileGetFileTimeA(FileName);
            json_sub.I['size'] := SearchRec.Size;
            json.A['list'] := json_sub;
          end;
        except
          //print(ExceptionParam)
        end;//}
      end; 
      Status := FindNext(SearchRec);
    end;
  finally
    FindClose(SearchRec);
    SearchRec.Free;
  end;//*) 
end;
%>
<%
begin
  FromPath := 'D:\code\delphi\sign\发行文件'; //下载源目录
  
  json := Pub.GetJson; //这样创建json对象不需要自己释放,系统自动管理
  json.SO; //初始化 或 json.Init;
  
  // 验证是否登录代码
  {if not Request.IsLogin('Logined') then
  begin 
    json.S['retcode'] := '300';
    json.S['retmsg'] := '你还没有登录(no logined)!'; 
    print(json.AsJson(true));
    exit; 
  end;//} 
  
  json.S['retcode'] := '200';
  json.S['retmsg'] := '成功!';
  if Request.V('opr') = '1' then
  begin //获取服务上指定目录的文件信息
    GetOneDirFileInfo(Json, FromPath);
  end else
  if Request.V('opr') = '2' then
  begin //下载指定文件给定大小的块 
    FromPath := PathWithSlash(FromPath);   
    RelativePath := Request.V('fn');
    FileName := FromPath + RelativePath;
    Fs := Pub.GetFS(FileName, fmShareDenyWrite, ErrStr);
    if trim(ErrStr) <> '' then 
    begin
      json.S['retcode'] := '300';
      json.S['retmsg'] := ErrStr;
      print(json.AsJson(true));  
      exit;
    end;
    Fs.Position := StrToInt(Request.V('pos'));
    Response.ContentStream := TMemoryStream.Create; //注意不能用 Pub.GetMs,这是因为Pub.GetMs创建的对象在动态脚本运行完就释放了
    Response.ContentStream.CopyFrom(Fs, StrToInt(Request.V('size')));
    //返回流数据
    Response.ContentType := 'application/octet-stream';   
  end;
  print(json.AsJson(true));
end;
%>

客户端代码

客户端收到块后,进行合并。全部块下载完成后,还要把新下载的文件的文件修改为与服务器上的文件相同。以下是客户端实现的主代码:

procedure TMainForm.UpgradeBlock_Run(var ThreadRetInfo: TThreadRetInfo);
const
  BlockSize = 1024*1024; //1M
var
  HTML, ToPath, RelativePath, FN, Tmp, TmpFileName, FailFiles, SuccFiles, Newfn, TmpToPath: string;
  Json, TmpJson: TminiJson;
  lp, I, Number, HadUpSize, AllSize, AllBlockCount, MySize, MyNumber: Int64;
  Flag: boolean;
  SL, SLDate, SLSize, SLTmp: TStringlist;
  MS: TMemoryStream;
  Fs: TFileStream;
  procedure HintMsg(Msg: string);
  begin
    FMyMsg := Msg; // '正在获取文件列表。。。';
    ThreadRetInfo.Self.Synchronize(ThreadRetInfo.Self, MyUpdateface); //为什么不直接用匿名,因为laz不支持
  end;
begin
  ToPath := 'D:\superhtml'; //如果是当前程序更新  ExtractFilePath(ParamStr(0))
 
  ThreadRetInfo.Ok := false;
 
  HintMsg('正在获取文件列表。。。');
  if not HttpPost('/接口/同步文件到客户端.html?opr=1',
      '', ThreadRetInfo.ErrStr, ThreadRetInfo.HTML) then exit;
  if Pos('{', ThreadRetInfo.HTML) <> 1 then
  begin
    ThreadRetInfo.ErrStr :='请先检查脚本源码是否配置正确!';
    exit;
  end;
  ToPath := Pub.PathWithSlash(ToPath);
 
  Json := TminiJson.Create;
  SL := TStringlist.Create;
  SLDate := TStringlist.Create;
  SLSize := TStringlist.Create;
  SLTmp := TStringlist.Create;
  try
    Json.LoadFromString(ThreadRetInfo.HTML);
    if json.S['retcode'] = '200' then
    begin
      TmpJson := json.A['list'];
      for lp := 0 to TmpJson.length - 1 do
      begin
        HintMsg(lp.ToString + '/' + TmpJson.length.ToString + '正在检查文件:' + RelativePath);
        RelativePath := TmpJson[lp].S['RelativePath'];
        if trim(RelativePath) = '' then Continue;
        Flag := FileExists(ToPath + RelativePath);
        if Flag then
        begin
          if (PubFile.FileGetFileTimeA(ToPath + RelativePath) = TmpJson[lp].S['FileTime']) and
             (PubFile.FileGetFileSize(ToPath + RelativePath) = TmpJson[lp].I['Size']) then
          else
            Flag := false;
        end;
        if not Flag then //此文件需要更新
        begin
          SL.Add(RelativePath);
          SLDate.Add(TmpJson[lp].S['FileTime']);
          SLSize.Add(TmpJson[lp].S['Size']);
        end;
      end;
 
      //开始下载
      FailFiles := '';
      SuccFiles := '';
      HintMsg('需要更新的文件共有' + IntToStr(SL.Count) + '个。。。');
      for lp := 0 to SL.Count - 1 do
      begin
        RelativePath := SL[lp];
        if RelativePath[1] = '\' then RelativePath := Copy(RelativePath, 2, MaxInt);
        FN := ToPath + RelativePath;
 
        //先计算要分几个包,以处理进度
        Number := 0;
        HadUpSize := 0;
        AllSize := StrToInt64(SLSize[lp]);
        AllBlockCount := 0;
        while true do
        begin
          AllBlockCount := AllBlockCount + 1;
          if AllSize - HadUpSize >= BlockSize then
             MySize := BlockSize
          else
             MySize := AllSize - HadUpSize;
          HadUpSize := HadUpSize + MySize;
          if HadUpSize >= AllSize then
            break;
        end;
 
        //开始分块下载
        Number := 0;
        HadUpSize := 0;
        //AllSize := Fs.Size;
        //TmpToPath := PubFile.FileGetTemporaryPath;
        Newfn := '@_' + PubPWD.GetMd5(SLDate[lp] + SLSize[lp]) + ExtractFileName(FN);  //Pub.GetClientUniqueCode;
 
        if FileExists(ToPath + Newfn) and (FileExists(FN)) then
        begin
          SLTmp.LoadFromFile(ToPath + Newfn);
          MyNumber := StrToInt64(trim(SLTmp.Text));
          Fs := TFileStream.Create(FN, fmOpenWrite);
        end else
        begin
          MyNumber := 0;
          Fs := TFileStream.Create(FN, fmCreate);
        end;
        try
          while true do
          begin
            HintMsg('正在下载文件[' + Pub.GetDeliBack(RelativePath, '@@') + ']第[' + IntToStr(Number + 1) + '/' + IntToStr(AllBlockCount) + ']个包。。。');
 
            if AllSize - HadUpSize >= BlockSize then
               MySize := BlockSize
            else
               MySize := AllSize - HadUpSize;
            Number := Number + 1;
            if (MyNumber = 0) or (Number >= MyNumber) or (HadUpSize + MySize >= AllSize) then
            begin
              for I := 1 to 2 do //意外出错重试一次
              begin
                if not HttpPost('/接口/同步文件到客户端.html?opr=2fn=' + UrlEncode(RelativePath) +
                  'pos=' + UrlEncode(IntToStr(HadUpSize)) + 'size=' + UrlEncode(IntToStr(MySize)),
                  '', ThreadRetInfo.ErrStr, ThreadRetInfo.HTML, MS) then
                begin
                  if I = 2 then
                  begin
                    ThreadRetInfo.ErrStr := Json.S['retmsg'];
                    exit;
                  end else
                    Continue;
                end;
                if Pos('{', ThreadRetInfo.HTML) < 1 then
                begin
                  if I = 2 then
                  begin
                    ThreadRetInfo.ErrStr := Json.S['retmsg'];
                    exit;
                  end else
                    Continue;
                end;
 
                Json.LoadFromString(ThreadRetInfo.HTML);
                if json.S['retcode'] <> '200' then
                begin
                  if I = 2 then
                  begin
                    ThreadRetInfo.ErrStr := Json.S['retmsg'];
                    exit;
                  end else
                    Continue;
                end;
                break;
              end;
 
              if MS = nil then
              begin
                ThreadRetInfo.ErrStr := '没能下载到文件[' + RelativePath + ']!' + json.S['retmsg'];
                exit;
              end else
              begin
                Fs.Position := HadUpSize;
                MS.Position := 0;
                Fs.CopyFrom(MS, MS.Size);
                MS.Free;
                MS := nil;
                SLTmp.Text := Number.ToString;
                try
                  SLTmp.SaveToFile(ToPath + Newfn);
                except
                end;
              end;
            end;
            HadUpSize := HadUpSize + MySize;
 
            if HadUpSize >= AllSize then
            begin //全部下载完成
              Fs.Free;
              Fs := nil;
              Sleep(10);
              PubFile.FileChangeFileDate(Fn, SLDate[lp]);
              DeleteFile(ToPath + Newfn);
              SuccFiles := SuccFiles + #13#10 + RelativePath;
              break;
            end;
          end;
        finally
          if Fs <> nil then
            Fs.Free;
        end;
      end;
      ThreadRetInfo.HTML := '';
      if trim(SuccFiles) <> '' then
        ThreadRetInfo.HTML := '本次更新了以下文件:'#13#10 + SuccFiles;
      //if trim(FailFiles) <> '' then
        //ThreadRetInfo.HTML := trim(ThreadRetInfo.HTML + #13#10'以下文件更新失败:'#13#10 + FailFiles);
    end;
  finally
    SLTmp.Free;
    SLSize.Free;
    SL.Free;
    Json.Free;
    SLDate.Free;
  end;
  ThreadRetInfo.Ok := true;
end;

以下是Demo运行界面:

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表