You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何通过ISAPI DLL增量返回大文件?避免全量加载至内存

解决ISAPI DLL增量返回大文件片段的内存问题

嘿,针对你这个用Delphi开发的ISAPI返回大文件片段的内存问题,我来给你捋捋思路!

首先得澄清一个点:你当前的代码其实已经是增量读取并发送的模式了——每次只从文件里读8KB到缓冲区,发送完再读下一批,理论上不会把$xx到$yy的所有字节都加载到内存里。不过可能你是遇到了某些场景下的内存异常,或者想让这个逻辑更健壮、适配客户端的断点续传需求?

下面给你几个优化和验证的方向:

1. 确保文件流是直接操作物理磁盘

你用的AStream一定要是TFileStream,而不是TMemoryStream或者其他会把文件预加载到内存的流类型。TFileStreamRead方法是直接从磁盘读取指定大小的数据到缓冲区,不会把整个文件(甚至整个请求区间)都塞进内存。

2. 优化WriteClient的调用逻辑

当前代码没有检查WriteClient的返回值和实际发送的字节数,这可能导致无效的磁盘读取或者内存浪费。比如客户端中途断开连接,你的代码还会继续读文件,这就没必要了。改进后的代码应该是这样:

procedure SendPartialFile(ECB: PEXTENSION_CONTROL_BLOCK; const DataFilePath: string; StartOffset, EndOffset: Int64);
var
  FileStream: TFileStream;
  Buffer: array[0..8191] of Byte;
  BytesToRead, BytesSent: DWORD;
  BytesLeft: Int64;
begin
  FileStream := TFileStream.Create(DataFilePath, fmOpenRead or fmShareDenyWrite);
  try
    FileStream.Position := StartOffset;
    BytesLeft := EndOffset - StartOffset;

    while BytesLeft > 0 do
    begin
      // 每次读取不超过缓冲区大小的数据
      BytesToRead := DWORD(Min(BytesLeft, SizeOf(Buffer)));
      BytesToRead := FileStream.Read(Buffer, BytesToRead);
      if BytesToRead = 0 then Break; // 文件读取完毕

      // 发送数据,必须检查发送结果
      if not ECB^.WriteClient(ECB^.ConnID, @Buffer, BytesToRead, BytesSent) then
      begin
        // 发送失败(比如客户端断开),直接终止循环
        Break;
      end;

      Dec(BytesLeft, BytesSent);
      // 如果实际发送的字节数小于预期,说明客户端接收慢,适当延迟避免频繁读盘
      if BytesSent < BytesToRead then
      begin
        Sleep(10);
      end;
    end;
  finally
    FileStream.Free;
  end;
end;

3. 支持HTTP范围请求(Range Requests)

这是处理大文件增量返回的核心优化,视频播放器、浏览器这类客户端通常会发送Range请求头,只获取文件的某一部分,这样你可以按需返回对应偏移的内容,还支持断点续传。实现步骤:

  • 在ISAPI的入口函数里解析请求头中的Range字段(比如Range: bytes=1024-2047
  • 计算对应的StartOffsetEndOffset,注意要处理超出文件大小的情况
  • 设置响应状态码为206 Partial Content,并添加Content-RangeContent-Length响应头:
    // 设置206部分内容状态码
    ECB^.SetStatus(ECB^.ConnID, 206);
    
    // 构造Content-Range头,格式为 bytes start-end/totalSize
    ECB^.AddResponseHeader(ECB^.ConnID, 
      PChar('Content-Range: bytes ' + IntToStr(StartOffset) + '-' + IntToStr(EndOffset-1) + '/' + IntToStr(TotalFileSize)), 
      nil);
    
    // 设置本次发送的字节数
    ECB^.AddResponseHeader(ECB^.ConnID, 
      PChar('Content-Length: ' + IntToStr(EndOffset - StartOffset)), 
      nil);
    

总结

你最初的思路是对的,分批读取+发送就是增量返回的正确方式,只要确保文件流直接操作磁盘、处理好WriteClient的返回值,再加上对HTTP范围请求的支持,就能实现低内存占用的大文件片段返回逻辑。

内容的提问来源于stack exchange,提问作者zeus

火山引擎 最新活动