HTTP下载大文件

15 投票
4 回答
10651 浏览
提问于 2025-04-15 14:55

我正在用Python/Twisted开发一个网页应用。

我希望用户能够下载一个非常大的文件(超过100MB)。当然,我不想把整个文件都加载到服务器的内存里。

在服务器端,我有这样的想法:

...
request.setHeader('Content-Type', 'text/plain')
fp = open(fileName, 'rb')
try:
    r = None
    while r != '':
        r = fp.read(1024)
        request.write(r)
finally:
    fp.close()
    request.finish()

我原本以为这样可以正常工作,但我遇到了一些问题:

我在用Firefox测试... 似乎浏览器让我等到文件完全下载后,才会弹出打开/保存的对话框。

我本来希望对话框能立刻出现,然后进度条开始显示下载进度...

也许我需要在HTTP头中添加一些东西... 比如文件的大小?

4 个回答

3

没错,Content-Length这个头信息可以让你实现你想要的进度条!

3

如果你真的在处理 text/plain 类型的内容,建议你在客户端表示可以处理这种格式时,考虑使用 Content-Encoding: gzip 来发送。这会大大节省带宽。而且,如果这是一个静态文件,你其实应该使用 sendfile(2) 来发送它。至于浏览器在下载时没有按照你的预期工作,你可能需要看看 Content-Disposition 这个头部信息。总之,逻辑是这样的:

当客户端通过 Accept-Encoding 头部表示他们可以处理 gzip 编码时(比如 Accept-Encoding: compress;q=0.5, gzip;q=1.0Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 等等),你就应该先压缩文件,然后把压缩后的结果缓存起来,接着写上正确的响应头(比如 Content-Encoding: gzipContent-Length: nContent-Type: text/plain 等等),最后使用 sendfile(2)(具体实现可能因环境而异)将内容从打开的文件描述符复制到你的响应流中。

如果他们不接受 gzip,那就照样做,但不需要先压缩。

另外,如果你有 Apache、Lighttpd 或类似的服务器作为透明代理在你的服务器前面,你可以使用 X-Sendfile 头部,这样会非常快速:

response.setHeader('Content-Type', 'text/plain')
response.setHeader(
  'Content-Disposition',
  'attachment; filename="' + os.path.basename(fileName) + '"'
)
response.setHeader('X-Sendfile', fileName)
response.setHeader('Content-Length', os.stat(fileName).st_size)
37

你发的示例代码有两个大问题,一个是它不够合作,另一个是它在发送之前会把整个文件都加载到内存里。

while r != '':
    r = fp.read(1024)
    request.write(r)

要记住,Twisted使用的是合作式多任务处理来实现并发。所以这个代码片段的第一个问题是它在一个大文件的内容上使用了一个循环(你说这个文件很大)。这意味着整个文件会被读入内存,并在响应之前写入,这样在这个过程中就不能做任何其他事情了。在这种情况下,“任何”的意思也包括把内存中的数据推送到网络上,所以你的代码会一次性把整个文件都放在内存中,只有在这个循环完成后才会开始释放内存。

所以,一般来说,你不应该在基于Twisted的应用中写这样的代码来处理大任务。相反,你需要把大任务拆分成小块,以一种能与事件循环合作的方式来处理。对于通过网络发送文件,最好的方法是使用生产者消费者。这两个相关的API可以高效地移动大量数据,利用缓冲区空事件来避免浪费过多内存。

你可以在这里找到这些API的文档:

http://twistedmatrix.com/projects/core/documentation/howto/producers.html

幸运的是,对于这种非常常见的情况,已经有一个现成的生产者可以使用,而不需要你自己去实现:

http://twistedmatrix.com/documents/current/api/twisted.protocols.basic.FileSender.html

你可能想这样使用它:

from twisted.protocols.basic import FileSender
from twisted.python.log import err
from twisted.web.server import NOT_DONE_YET

class Something(Resource):
    ...

    def render_GET(self, request):
        request.setHeader('Content-Type', 'text/plain')
        fp = open(fileName, 'rb')
        d = FileSender().beginFileTransfer(fp, request)
        def cbFinished(ignored):
            fp.close()
            request.finish()
        d.addErrback(err).addCallback(cbFinished)
        return NOT_DONE_YET

你可以在我的博客上阅读更多关于NOT_DONE_YET和其他相关概念的信息,特别是“Twisted Web in 60 Seconds”系列中的“异步响应”部分,链接在这里:http://jcalderone.livejournal.com/50562.html

撰写回答