14

我正在使用 Python/Twisted 开发一个 Web 应用程序。

我希望用户能够下载一个非常大的文件(> 100 Mb)。当然,我不想将所有文件加载到(服务器的)内存中。

服务器端我有这个想法:

...
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()

我希望这可以工作,但我遇到了问题:我正在使用 FF 进行测试......似乎浏览器让我等到文件下载完成,然后我有打开/保存对话框。

我希望立即出现对话框,然后进度条正在运行......

也许我必须在 Http 标头中添加一些东西......文件大小之类的东西?

4

4 回答 4

36

您发布的示例代码的两个大问题是它不合作,并且在发送之前将整个文件加载到内存中。

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

请记住,Twisted 使用协作多任务处理来实现任何类型的并发。所以这个片段的第一个问题是它是整个文件内容的while循环(你说它很大)。这意味着在进程中发生任何其他事情之前,整个文件将被读入内存并写入响应。在这种情况下,“ anything ”还包括将内存缓冲区中的字节推送到网络上,因此您的代码还将一次将整个文件保存在内存中,并且只有在此循环完成时才开始删除它.

因此,作为一般规则,您不应该编写用于基于 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

您可以在我的博客http://jcalderone.livejournal.com/50562.htmlNOT_DONE_YET上阅读有关“60 秒内 Twisted Web”系列的更多信息和其他相关想法(具体参见“异步响应”条目)。

于 2009-11-01T14:37:00.767 回答
3

如果这确实是text/plain内容,您应该认真考虑Content-Encoding: gzip在客户表明他们可以处理它时发送它。您应该会看到巨大的带宽节省。此外,如果这是一个静态文件,那么您真正想要做的是使用sendfile(2). 至于浏览器在下载内容方面没有达到您的预期,您可能需要查看Content-Disposition标题。所以无论如何,逻辑是这样的:

如果客户端表明他们可以通过标头(例如或类似)处理gzip编码,则压缩文件,将压缩结果缓存在某处,为响应写入正确的标头(, ,等),然后使用(但是可能或可能尚未在您的环境中提供)以将内容从打开的文件描述符复制到您的响应流中。Accept-EncodingAccept-Encoding: compress;q=0.5, gzip;q=1.0Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0Content-Encoding: gzipContent-Length: nContent-Type: text/plainsendfile(2)

如果他们不接受gzip,请做同样的事情,但不要先进行 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)
于 2009-10-09T17:43:54.273 回答
3

是的,Content-Length 标题将为您提供所需的进度条!

于 2009-10-08T15:45:34.087 回答
0

这是一个使用 urllib2 以块下载文件的示例,您可以从扭曲的函数调用内部使用它

import os
import urllib2
import math

def downloadChunks(url):
    """Helper to download large files
        the only arg is a url
       this file will go to a temp directory
       the file will also be downloaded
       in chunks and print out how much remains
    """

    baseFile = os.path.basename(url)

    #move the file to a more uniq path
    os.umask(0002)
    temp_path = "/tmp/"
    try:
        file = os.path.join(temp_path,baseFile)

        req = urllib2.urlopen(url)
        total_size = int(req.info().getheader('Content-Length').strip())
        downloaded = 0
        CHUNK = 256 * 10240
        with open(file, 'wb') as fp:
            while True:
                chunk = req.read(CHUNK)
                downloaded += len(chunk)
                print math.floor( (downloaded / total_size) * 100 )
                if not chunk: break
                fp.write(chunk)
    except urllib2.HTTPError, e:
        print "HTTP Error:",e.code , url
        return False
    except urllib2.URLError, e:
        print "URL Error:",e.reason , url
        return False

    return file
于 2011-12-04T18:49:32.387 回答