Finger 的演变:构建一个简单的 Finger 服务

介绍

这是 Twisted 教程 从零开始的 Twisted,或 Finger 的演变 的第一部分。

如果您不熟悉“finger”,可能是因为它现在不像以前那样经常使用。基本上,如果您运行 finger nailfinger [email protected],目标计算机将输出一些关于名为 nail 的用户的信息。例如

Login: nail                           Name: Nail Sharp
Directory: /home/nail                 Shell: /usr/bin/sh
Last login Wed Mar 31 18:32 2004 (PST)
New mail received Thu Apr  1 10:50 2004 (PST)
     Unread since Thu Apr  1 10:50 2004 (PST)
No Plan.

如果目标计算机没有运行 fingerd 守护进程,您将收到“连接被拒绝”错误。偏执的系统管理员会关闭 fingerd 或限制输出以阻止黑客和骚扰者。上述格式是标准 fingerd 的默认格式,但其他实现可以输出任何它想要的东西,例如组织中每个人的自动化责任状态。您还可以定义伪“用户”,它们本质上是关键字。

本教程的这一部分使用了工厂和协议,如 编写 TCP 服务器 HOWTO 中介绍的那样,以及 Deferreds,如 使用 Deferreds生成 Deferreds 中介绍的那样。服务和应用程序在 使用 Twisted 应用程序框架 中讨论。

在本教程的这一部分结束时,我们的 Finger 服务器将在端口 1079 上响应 TCP Finger 请求,并将从 Web 读取数据。

拒绝连接

finger01.py

from twisted.internet import reactor

reactor.run()

此示例仅运行 Reactor。它几乎不会消耗任何 CPU 资源。由于它没有监听任何端口,因此无法响应网络请求 - 在我们中断程序之前什么都不会发生。此时,如果您运行 finger nailtelnet localhost 1079,您将收到“连接被拒绝”错误,因为没有运行的守护进程来响应。可能不太有用 - 但这是 Twisted 程序将要生长的骨架。

如上所述,在本教程的各个部分,您需要观察正在开发的服务器的行为。除非您有一个可以使用备用端口的 Finger 程序,否则最简单的方法是使用 Telnet 客户端。 telnet localhost 1079 将连接到本地主机上的端口 1079,在那里最终将有一个 Finger 服务器在监听。

Reactor

您不会调用 Twisted,Twisted 会调用您。 reactor 是 Twisted 的主事件循环,类似于 Python 中其他工具包(Qt、wx 和 Gtk)中的主循环。在任何运行的 Twisted 应用程序中都只有一个 Reactor。一旦启动,它就会不断循环,响应网络事件并对代码进行计划调用。

请注意,实际上有几种不同的 Reactor 可供选择; from twisted.internet import reactor 返回当前的 Reactor。如果您还没有选择 Reactor 类,它会自动选择默认的 Reactor。有关更多信息,请参见 Reactor 基础知识 HOWTO

不做任何事

finger02.py

from twisted.internet import endpoints, protocol, reactor


class FingerProtocol(protocol.Protocol):
    pass


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory())
reactor.run()

这里我们使用 endpoints.serverFromString 创建一个 Twisted 端点。端点是 Twisted 的一个概念,它封装了连接的一端。客户端和服务器有不同的端点。端点的一个巨大优势是,它们可以使用一种特定于领域的语言以文本方式描述。例如,这里,我们要求 Twisted 为服务器创建一个 TCP 端点,使用字符串 "tcp:1079"。这与对 serverFromString 的调用一起,告诉 Twisted 查找一个 TCP 端点,并将端口 1079 传递给它。从该函数返回的端点可以调用 listen() 方法,这会导致 Twisted 开始监听端口 1079。(数字 1079 是一个提醒,我们最终想要在端口 79 上运行,这是 finger 服务器的标准端口。)有关端点的更多详细信息,请查看 使用端点连接

传递给 listen() 方法的工厂,FingerFactory,用于处理该端口上的传入请求。具体来说,对于每个请求,反应器都会调用工厂的 buildProtocol 方法,在本例中,这会导致实例化 FingerProtocol。由于这里定义的协议实际上没有响应任何事件,因此将接受与 1079 的连接,但会忽略输入。

工厂是您想要使数据对协议实例可用的地方,因为当连接关闭时,协议实例会被垃圾回收。

断开连接

finger03.py

from twisted.internet import endpoints, protocol, reactor


class FingerProtocol(protocol.Protocol):
    def connectionMade(self):
        self.transport.loseConnection()


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory())
reactor.run()

这里,我们在协议中添加了响应开始连接事件(通过终止连接)的能力。这可能不是一个有趣的行为,但它已经接近于按照标准 finger 协议的字面意思进行操作。毕竟,标准中没有要求向远程连接发送任何数据。就标准而言,唯一的问题是我们过早地终止了连接。一个足够慢的客户端会看到他的 send() 的用户名导致错误。

读取用户名,断开连接

finger04.py

from twisted.internet import endpoints, protocol, reactor
from twisted.protocols import basic


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        self.transport.loseConnection()


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory())
reactor.run()

这里,我们让 FingerProtocol 继承自 LineReceiver,这样我们就可以逐行地获得基于数据的事件。我们响应接收行的事件,通过关闭连接来响应。

如果您使用 telnet 客户端与该服务器交互,结果将类似于以下内容

$ telnet localhost 1079
Trying 127.0.0.1...
Connected to localhost.localdomain.
alice
Connection closed by foreign host.

恭喜,这是第一个符合标准的代码版本。但是,通常人们实际上希望传输一些关于用户的信息。

读取用户名,输出错误,断开连接

finger05.py

from twisted.internet import endpoints, protocol, reactor
from twisted.protocols import basic


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        self.transport.write(b"No such user\r\n")
        self.transport.loseConnection()


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory())
reactor.run()

最后,一个有用的版本。当然,它的有用性受到这样一个事实的限制,即该版本只打印出一个“没有此用户”消息。当然,它可以用于在蜜罐(诱饵服务器)中产生毁灭性的影响。

来自空工厂的输出

finger06.py

# Read username, output from empty factory, drop connections

from twisted.internet import endpoints, protocol, reactor
from twisted.protocols import basic


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        self.transport.write(self.factory.getUser(user) + b"\r\n")
        self.transport.loseConnection()


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol

    def getUser(self, user):
        return b"No such user"


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory())
reactor.run()

相同行为,但最终我们看到了工厂的用处:作为不为每个连接构造的东西,它可以负责用户数据库。特别是,如果用户数据库后端发生变化,我们不必更改协议。

来自非空工厂的输出

finger07.py

# Read username, output from non-empty factory, drop connections

from twisted.internet import endpoints, protocol, reactor
from twisted.protocols import basic


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        self.transport.write(self.factory.getUser(user) + b"\r\n")
        self.transport.loseConnection()


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol

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

    def getUser(self, user):
        return self.users.get(user, b"No such user")


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory({b"moshez": b"Happy and well"}))
reactor.run()

最后,一个真正有用的 finger 数据库。虽然它不提供有关已登录用户的信息,但它可以用于分发诸如办公室位置和内部办公室号码之类的东西。如上所述,工厂负责维护用户数据库:请注意,协议实例没有改变。这开始看起来不错:我们真的不必再调整我们的协议了。

使用 Deferreds

finger08.py

# Read username, output from non-empty factory, drop connections
# Use deferreds, to minimize synchronicity assumptions

from twisted.internet import defer, endpoints, protocol, reactor
from twisted.protocols import basic


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        d = self.factory.getUser(user)

        def onError(err):
            return "Internal error in server"

        d.addErrback(onError)

        def writeResponse(message):
            self.transport.write(message + b"\r\n")
            self.transport.loseConnection()

        d.addCallback(writeResponse)


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol

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

    def getUser(self, user):
        return defer.succeed(self.users.get(user, b"No such user"))


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory({b"moshez": b"Happy and well"}))
reactor.run()

但是,这里我们只是为了好玩而调整它。是的,虽然之前的版本有效,但它确实假设 getUser 的结果总是立即可用的。但是,如果我们不是使用内存中的数据库,而是必须从远程 Oracle 服务器获取结果呢?通过允许 getUser 返回一个 Deferred,我们使异步检索数据变得更容易,这样 CPU 就可以同时用于其他任务。

Deferred HOWTO 中所述,Deferreds 允许程序由事件驱动。例如,如果程序中的一个任务正在等待数据,而不是让 CPU(以及程序!)空闲地等待该数据(通常称为“阻塞”的过程),程序可以在此期间执行其他操作,并等待一些信号表明数据已准备好进行处理,然后再返回到该过程。

简而言之,上面 FingerFactory 中的代码创建了一个 Deferred,我们开始向它附加回调FingerFactory 中的延迟操作实际上是一个快速运行的表达式,它包含一个字典方法 get。由于此操作可以立即执行,FingerFactory.getUser 使用 defer.succeed 创建一个已经具有结果的 Deferred,这意味着它的返回值将立即传递给第一个回调函数,该函数恰好是 FingerProtocol.writeResponse。我们还定义了一个错误回调(恰当地命名为 FingerProtocol.onError),它将在出现错误时调用,而不是调用 writeResponse

在本地运行“finger”

finger09.py

# Read username, output from factory interfacing to OS, drop connections

from twisted.internet import defer, endpoints, protocol, reactor, utils
from twisted.protocols import basic


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        d = self.factory.getUser(user)

        def onError(err):
            return b"Internal error in server"

        d.addErrback(onError)

        def writeResponse(message):
            self.transport.write(message + b"\r\n")
            self.transport.loseConnection()

        d.addCallback(writeResponse)


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol

    def getUser(self, user):
        return utils.getProcessOutput(b"finger", [user])


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory())
reactor.run()

此示例还使用了一个 Deferred。 twisted.internet.utils.getProcessOutput 是 Python 的 commands.getoutput 的非阻塞版本:它运行一个 shell 命令(在本例中为 finger)并捕获其标准输出。但是,getProcessOutput 返回一个 Deferred,而不是输出本身。由于 FingerProtocol.lineReceived 已经期望 getUser 返回一个 Deferred,因此它不需要更改,并且它将标准输出作为 finger 结果返回。

请注意,在本例中,shell 的内置 finger 命令只是使用它所提供的任何参数运行。这可能是危险的,因此您可能不希望真正的服务器在没有对用户输入进行更多验证的情况下执行此操作。这将完全执行标准版本的 finger 服务器所执行的操作。

从 Web 读取状态

网络。这个已经渗透到世界各地家庭的发明终于渗透到我们的发明中。在本例中,我们通过 twisted.web.client.getPage 使用内置的 Twisted web 客户端,它是 Python 的 urllib.urlopen(URL).read 的非阻塞版本。与 getProcessOutput 一样,它返回一个 Deferred,该 Deferred 将使用字符串进行回调,因此可以用作直接替换。

因此,我们有三个不同数据库后端的示例,它们都没有更改协议类。事实上,在我们完成本教程之前,我们不必再更改协议:我们在这里实现了一个真正可用的类。

finger10.py

# Read username, output from factory interfacing to web, drop connections

from twisted.internet import defer, endpoints, protocol, reactor, utils
from twisted.protocols import basic
from twisted.web import client


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        d = self.factory.getUser(user)

        def onError(err):
            return b"Internal error in server"

        d.addErrback(onError)

        def writeResponse(message):
            self.transport.write(message + b"\r\n")
            self.transport.loseConnection()

        d.addCallback(writeResponse)


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol

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

    def getUser(self, user):
        return client.getPage(self.prefix + user)


fingerEndpoint = endpoints.serverFromString(reactor, "tcp:1079")
fingerEndpoint.listen(FingerFactory(prefix=b"http://livejournal.com/~"))
reactor.run()

使用应用程序

到目前为止,我们一直在伪造。我们一直使用端口 1079,因为真的,谁想使用 root 权限运行 finger 服务器?好吧,常见的解决方案是“特权剥夺”:在绑定到网络之后,成为一个不同的、权限较低的用户。我们可以自己做到,但 Twisted 有一个内置的方法来做到这一点。我们将创建一个如上所示的代码片段,但现在我们将定义一个应用程序对象。该对象将具有 uidgid 属性。当运行它时(稍后我们将看到如何运行它),它将绑定到端口,剥夺特权,然后运行。

继续阅读以了解如何使用 twistd 实用程序运行此代码。

twistd

这就是运行“Twisted 应用程序”的方式——定义了“应用程序”的文件。守护进程应该遵守某些行为标准,以便标准工具可以停止/启动/查询它们。如果 Twisted 应用程序通过 twistd(TWISTed 守护进程器)运行,所有这些行为问题都将为您处理。twistd 做了守护进程可以做的一切——关闭 stdin/stdout/stderr,断开与终端的连接,甚至可以更改运行时目录,甚至可以更改根文件系统。简而言之,它做了所有事情,以便 Twisted 应用程序开发人员可以专注于编写他们的网络代码。

root% twistd -ny finger11.tac # just like before
root% twistd -y finger11.tac # daemonize, keep pid in twistd.pid
root% twistd -y finger11.tac --pidfile=finger.pid
root% twistd -y finger11.tac --rundir=/
root% twistd -y finger11.tac --chroot=/var
root% twistd -y finger11.tac -l /var/log/finger.log
root% twistd -y finger11.tac --syslog # just log to syslog
root% twistd -y finger11.tac --syslog --prefix=twistedfinger # use given prefix

有多种方法可以告诉 twistd 您的应用程序在哪里;这里展示了如何在 Python 源文件(Twisted 应用程序配置 文件)中使用 application 全局变量来实现。

finger11.tac

# Read username, output from non-empty factory, drop connections
# Use deferreds, to minimize synchronicity assumptions
# Write application. Save in 'finger.tpy'

from twisted.application import service, strports
from twisted.internet import defer, protocol, reactor
from twisted.protocols import basic


class FingerProtocol(basic.LineReceiver):
    def lineReceived(self, user):
        d = self.factory.getUser(user)

        def onError(err):
            return "Internal error in server"

        d.addErrback(onError)

        def writeResponse(message):
            self.transport.write(message + b"\r\n")
            self.transport.loseConnection()

        d.addCallback(writeResponse)


class FingerFactory(protocol.ServerFactory):
    protocol = FingerProtocol

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

    def getUser(self, user):
        return defer.succeed(self.users.get(user, b"No such user"))


application = service.Application("finger", uid=1, gid=1)
factory = FingerFactory({b"moshez": b"Happy and well"})
strports.service("tcp:79", factory, reactor=reactor).setServiceParent(
    service.IServiceCollection(application)
)

与上述示例中使用 endpoints.serverFromString 不同,这里我们使用的是它的应用程序感知对应物 strports.service 。请注意,在实例化时,应用程序对象本身不引用协议或工厂。任何以应用程序为父级的服务(例如,我们使用 strports.service 创建的服务)将在 twistd 启动应用程序时启动。应用程序对象更适合于返回一个支持 IServiceIServiceCollectionIProcesssob.IPersistable 接口的对象,并使用给定的参数;我们将在本教程的下一部分中看到这些。作为我们打开的端点的父级,应用程序允许我们管理端点。

在守护进程在标准 finger 端口上运行后,您可以使用标准 finger 命令对其进行测试: finger moshez