因为项目需要,想实现从网络上下载文件,最好能实现断点续传。
搜索了下,发现下载文件还是很简单的,用IdHTTP的Get方法直接写入文件流中就可以了。
不过断点续传就麻烦了,网上是有蛮多demo的。但是几乎都是在续传的时候文件大小会变大,并不是真正的断点续传。
经过各种查找资料和实验,终于发现了问题。
一个是因为很多demo都直接设置
IdHttp.Response.ContentRangeStart 和 IdHttp.Response.ContentRangeEnd 属性来设置需要下载的文件范围,但我试了发现这种方法并不起作用,服务器还是会从头开始传送文件。然后发现直接设置IdHttp.Request.Range属性才可以。
还有是因为要是直接让IdHttp存入文件流
- IdHTTP.Get(aURL,FileStream);
的话即使你中途暂停,下载一半的文件也会把整个文件的空间占了,然后就没法通过读文件大小得知已下载多少了,当然这可以通过保存配置文件得知已下载大小。
然后最后参考一些资料就想到可以一小段一小段的下载文件到内存流中然后在一段一段的存入文件流来使得文件大小能够慢慢增大并实现断点续传。
建个窗体。我这里建的是FMX的窗体,所以这个demo也是可以在移动端用的,不过在Android好像ProgressBar没有实现,反正自己看着改改就行。然后随便拖个Edit两个button和个ProgressBar来显示进度。
顺便说下,Indy是阻塞式的,也就是说调用Indy里的方法,在执行完前界面是会卡住的,想了解Indy的机制的可以点这里看我翻译的Indy入门文档,在VCL下有个TIdAntiFreeze控件可以防止用户界面卡住,而FMX没有,所以要通过其他方式比如多线程来防止界面卡住,这里为了简单,模仿TIdAntiFreeze的实现,通过不停调用Application.ProcessMessages来放弃控制权,给用户界面刷新的时间。
然后按照之前的思想,这是“开始下载”按钮的代码
- procedure THttpGetFileForm.StartButtonClick(Sender: TObject);
- var
- aURL: string;
- MemStream: TMemoryStream;
- FileStream: TFileStream;
- FilePosition: Int64;
- FileSize: integer;
- Filename: string;
- IdHttp: TIdHttp;
- begin
- // 防止多次按按钮
- StartButton.Enabled := False;
- IdHttp := TIdHttp.Create(self);
- Try
- // 设置flag
- Stop := False;
- aURL := FileURLEdit.Text;
- // 处理重定向
- IdHttp.HandleRedirects := True;
- IdHttp.Request.Range := '';
- IdHttp.Head(aURL);
- // 获取重定向后的URL
- aURL := IdHttp.URL.URI;
- // 获取文件名
- Filename := GetURLFileName(aURL);
- // 创建文件流,如果存在,则打开并把指针放文件末尾,否则创建
- if FileExists(Filename) then begin
- try
- FileStream := TFileStream.Create(Filename, fmOpenWrite);
- FileStream.Seek(0, soEnd); // 指针移到末尾
- except
- Showmessage(Format('打开文件 "%s" 失败!', [Filename]));
- exit;
- end;
- end else begin
- FileStream := TFileStream.Create(Filename, fmCreate);
- end;
- // 创建内存流
- MemStream := TMemoryStream.Create;
- // 得到文件大小
- FileSize := IdHttp.Response.ContentLength;
- FilePosition := FileStream.Position;
- // 设置进度条
- ProgressBar.Max := FileSize;
- try
- // 循环下载,每次判断是否暂停
- while (FilePosition < FileSize) and (not Stop) do begin
- // 方法1:不可用
- // IdHttp.Response.ContentRangeStart := FilePosition;
- // IdHttp.Response.ContentRangeEnd := FilePosition + 1024;
- // 方法2:可用
- IdHttp.Request.Range := IntToStr(FilePosition) + '-';
- // 每次下载10240B的文件块,当然可以改成其他大小
- if FilePosition + 10240 < FileSize then
- IdHttp.Request.Range := IdHttp.Request.Range +
- IntToStr(FilePosition + 10239);
- IdHttp.get(aURL, MemStream); // 每段分别存入内存流
- MemStream.SaveToStream(FileStream);
- inc(FilePosition, MemStream.Size);
- // 更新进度条
- ProgressBar.Value := FilePosition;
- // 情况内存流
- MemStream.Clear;
- // 通过调用这个方法防止界面卡死
- Application.ProcessMessages;
- end;
- // 不是由于按了停止而结束的循环说明下载完成了
- if (not Stop) then
- Showmessage(Format('下载文件 "%s" 成功!', [Filename]));
- finally
- MemStream.DisposeOf;
- FileStream.DisposeOf;
- end;
- Finally
- // 最后要记得让按钮可按
- StartButton.Enabled := True;
- IdHttp.DisposeOf;
- End;
- end;
Stop: Boolean;是个全局变量,设在Form的private或public里无所谓
然后停止键其实只干了一件事:把Stop设为了True;
- procedure THttpGetFileForm.StopBtnClick(Sender: TObject);
- begin
- Stop := True;
- end;
奥,还有一个获取文件名的函数,其实很多demo里都用了这个函数
- function THttpGetFileForm.GetURLFileName(aURL: string): string;
- var
- i: integer;
- s: string;
- begin
- s := aURL;
- i := Pos('/', s);
- while i <> 0 do begin
- Delete(s, 1, i);
- i := Pos('/', s);
- end;
- Result := s;
- end;
其实最后还有个问题,这样下载的文件即使没有下完,除了文件大小和下完的不一样,在资源管理器里完全和下好的一模一样没有区别,然后就明白了为什么迅雷下载东西的时候要先存为临时文件,下载完成了后再改回去,所以对以上代码稍微进行修改:
- procedure THttpGetFileForm.StartButtonClick(Sender: TObject);
- var
- aURL: string;
- MemStream: TMemoryStream;
- FileStream: TFileStream;
- FilePosition: Int64;
- FileSize: integer;
- Filename: string;
- IdHttp: TIdHttp;
- begin
- // 防止多次按按钮
- StartButton.Enabled := False;
- IdHttp := TIdHttp.Create(self);
- Try
- // 设置flag
- Stop := False;
- aURL := FileURLEdit.Text;
- // 处理重定向
- IdHttp.HandleRedirects := True;
- IdHttp.Request.Range := '';
- IdHttp.Head(aURL);
- // 获取重定向后的URL
- aURL := IdHttp.URL.URI;
- // 获取文件名
- Filename := GetURLFileName(aURL);
- // 如果文件已存在说明下载完成了
- if FileExists(Filename) then begin
- Showmessage(Format('下载文件 "%s" 成功!', [Filename]));
- exit;
- end;
- // 创建文件流,如果存在临时文件,则打开并把指针放文件末尾,否则创建
- if FileExists(Filename + '.tmp') then begin
- try
- FileStream := TFileStream.Create(Filename + '.tmp', fmOpenWrite);
- FileStream.Seek(0, soEnd); // 指针移到末尾
- except
- Showmessage(Format('打开文件 "%s" 失败!', [Filename + '.tmp']));
- exit;
- end;
- end else begin
- FileStream := TFileStream.Create(Filename + '.tmp', fmCreate);
- end;
- // 创建内存流
- MemStream := TMemoryStream.Create;
- // 得到文件大小
- FileSize := IdHttp.Response.ContentLength;
- FilePosition := FileStream.Position;
- // 设置进度条
- ProgressBar.Max := FileSize;
- try
- // 循环下载,每次判断是否暂停
- while (FilePosition < FileSize) and (not Stop) do begin
- // 方法1:不可用
- // IdHttp.Response.ContentRangeStart := FilePosition;
- // IdHttp.Response.ContentRangeEnd := FilePosition + 1024;
- // 方法2:可用
- IdHttp.Request.Range := IntToStr(FilePosition) + '-';
- // 每次下载10240B的文件块,当然可以改成其他大小
- if FilePosition + 10240 < FileSize then
- IdHttp.Request.Range := IdHttp.Request.Range +
- IntToStr(FilePosition + 10239);
- IdHttp.get(aURL, MemStream); // 每段分别存入内存流
- MemStream.SaveToStream(FileStream);
- inc(FilePosition, MemStream.Size);
- // 更新进度条
- ProgressBar.Value := FilePosition;
- // 情况内存流
- MemStream.Clear;
- // 通过调用这个方法防止界面卡死
- Application.ProcessMessages;
- end;
- // 不是由于按了停止而结束的循环说明下载完成了,修改文件名
- if (not Stop) then begin
- FreeAndNil(FileStream);
- RenameFile(Filename + '.tmp', Filename);
- Showmessage(Format('下载文件 "%s" 成功!', [Filename]));
- end;
- finally
- MemStream.DisposeOf;
- if Assigned(FileStream) then
- FileStream.DisposeOf;
- end;
- Finally
- // 最后要记得让按钮可按
- StartButton.Enabled := True;
- IdHttp.DisposeOf;
- End;
- end;
此段代码在XE10,Windows下测试通过。
其实把这段再修改下,再创建个配置文件,然后把文件按需要分块,多个IdHttp分别下载各个小块然后最后拼起来,就很类似迅雷那种分块下载了。
如果要在移动平台使用,需要在文件名处加上文件路径,然后调整下显示进度的方式。
实际使用时会用多线程的方式在后台下载,过些天有时间了写个用多线程的多平台的demo上来。
需要完整的demo工程的点这里下载,稍微收个1分0.0。