Twisted、FTP与“大文件流式传输”

4 投票
1 回答
3812 浏览
提问于 2025-04-16 07:19

我正在尝试实现一个可以称作“将HTTP API转变为FTP接口”的功能。简单来说,就是有一个现成的REST API,可以用来管理网站用户的文件,而我正在搭建一个中介服务器,把这个API重新作为FTP服务器提供服务。这样,你就可以用像Filezilla这样的工具登录,查看你的文件,上传新文件,删除旧文件等等。

我正在使用twisted.protocols.ftp来搭建FTP服务器,使用twisted.web.client来处理HTTP客户端。

我遇到的问题是,当用户尝试下载文件时,如何将这个文件从HTTP响应“流式传输”到我的FTP响应中。上传时也是类似的情况。

最简单的方法是先从HTTP服务器下载整个文件,然后再把内容发送给用户。但这样做的问题是,某些文件可能会非常大(比如磁盘镜像、ISO文件等)。如果采用这种方法,文件的内容会在我从API下载文件和发送给用户之间一直占用内存,这样不好。

所以我的解决方案是尝试“流式传输”——当我从API的HTTP响应中获取到数据块时,我希望能立即将这些数据块发送给FTP用户。看起来很简单。

为了实现我的“自定义FTP功能”,我使用了ftp.FTPShell的一个子类。这个子类的读取方法openForReading会返回一个Deferred对象,这个对象会在实现了IReadFile的情况下被触发。

下面是我关于“流式HTTP”的(初步、简单的)实现。我使用fetch函数来设置HTTP请求,传入的回调函数会在我从响应中获取到每个数据块时被调用。

我原以为可以使用某种双端缓冲对象来在HTTP和FTP之间传输这些数据块,利用这个缓冲对象作为ftp._FileReader所需的类文件对象,但这很快证明行不通,因为send调用的消费者几乎立即关闭了缓冲区(因为它返回了空字符串,因为还没有数据可以读取等等)。因此,在我开始接收HTTP响应数据块之前,我就已经在“发送”空文件了。

我是不是快要成功了,但又缺少了什么?我是不是走错了方向?我想做的事情真的不可能吗(我对此表示怀疑)?

from twisted.web import client
import urlparse

class HTTPStreamer(client.HTTPPageGetter):
    def __init__(self):
        self.callbacks = []

    def addHandleResponsePartCallback(self, callback):
        self.callbacks.append(callback)

    def handleResponsePart(self, data):
        for cb in self.callbacks:
            cb(data)
        client.HTTPPageGetter.handleResponsePart(self, data)

class HTTPStreamerFactory(client.HTTPClientFactory):
    protocol = HTTPStreamer

    def __init__(self, *args, **kwargs):
        client.HTTPClientFactory.__init__(self, *args, **kwargs)
        self.callbacks = []

    def addChunkCallback(self, callback):
        self.callbacks.append(callback)

    def buildProtocol(self, addr):
        p = client.HTTPClientFactory.buildProtocol(self, addr)
        for cb in self.callbacks:
            p.addHandleResponsePartCallback(cb)
        return p

def fetch(url, callback):

    parsed = urlparse.urlsplit(url)

    f = HTTPStreamerFactory(parsed.path)
    f.addChunkCallback(callback)

    from twisted.internet import reactor
    reactor.connectTCP(parsed.hostname, parsed.port or 80, f)

顺便提一下,我才刚开始接触Twisted,昨天大部分时间都在阅读Dave Peticolas的Twisted入门,这对我来说是一个很好的起点,尽管是基于一个较旧版本的Twisted。

话虽如此,我可能做错了什么。

1 个回答

2

我原以为可以用一种双端缓冲区对象来在HTTP和FTP之间传输数据块,想把这个缓冲区当作ftp._FileReader需要的文件对象,但很快发现这样行不通。因为在发送调用时,消费者几乎立刻就关闭了缓冲区(因为返回的是空字符串,说明还没有数据可读等等)。所以,我在开始接收HTTP响应数据块之前,就已经在“发送”空文件了。

与其使用ftp._FileReader,不如找一个可以在你的HTTPStreamer每次收到数据块时执行写入操作的东西。你其实根本不需要从HTTP的缓冲区读取数据,因为没有必要有这样的缓冲区。一旦HTTP的数据到达,就直接写入给消费者。可以像这样...

class FTPStreamer(object):
    implements(IReadFile)

    def __init__(self, url):
        self.url = url

    def send(self, consumer):
        fetch(url, consumer.write)
        # You also need a Deferred to return here, so the 
        # FTP implementation knows when you're done.
        return someDeferred

你可能还想使用Twisted的生产者/消费者接口,这样可以控制数据传输的速度,特别是当你与HTTP服务器的连接速度比用户的FTP连接速度快时,这样做是很有必要的。

撰写回答