Finger 的演变:为 Finger 服务添加功能

简介

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

在本教程的这一部分中,我们的 Finger 服务器将继续增加功能:用户设置 Finger 公告的能力,以及使用我们的 Finger 服务在 Web、IRC 和 XML-RPC 上发送这些公告。资源和 XML-RPC 在 Twisted Web 如何操作 的 Web 应用程序部分中介绍。更多使用 twisted.words.protocols.irc 的示例可以在 编写 TCP 客户端Twisted Words 示例 中找到。

设置本地用户的消息

现在端口 1079 已经空闲了,也许我们可以用一个不同的服务器来使用它,一个可以让用户设置他们消息的服务器。它没有访问控制,因此任何可以登录到机器的人都可以设置任何消息。我们假设这是我们情况下所需的行為。测试它可以通过简单地

% nc localhost 1079   # or telnet localhost 1079
moshez
Giving a tutorial now, sorry!
^D

finger12.tac

# But let's try and fix setting away messages, shall we?
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 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, users):
        self.users = users

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


class FingerSetterProtocol(basic.LineReceiver):
    def connectionMade(self):
        self.lines = []

    def lineReceived(self, line):
        self.lines.append(line)

    def connectionLost(self, reason):
        user = self.lines[0]
        status = self.lines[1]
        self.factory.setUser(user, status)


class FingerSetterFactory(protocol.ServerFactory):
    protocol = FingerSetterProtocol

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

    def setUser(self, user, status):
        self.fingerFactory.users[user] = status


ff = FingerFactory({b"moshez": b"Happy and well"})
fsf = FingerSetterFactory(ff)

application = service.Application("finger", uid=1, gid=1)
serviceCollection = service.IServiceCollection(application)
strports.service("tcp:79", ff).setServiceParent(serviceCollection)
strports.service("tcp:1079", fsf).setServiceParent(serviceCollection)

这个程序有两个协议工厂-TCPServer 对,它们都是应用程序的子服务。具体来说,setServiceParent 方法用于将两个 TCPServer 服务定义为 application 的子级,application 实现 IServiceCollection 。因此,这两个服务都与应用程序一起启动。

使用服务使依赖关系合理

之前的版本让设置器窥视 Finger 工厂的内部。这种策略通常不是一个好主意:这个版本通过让两个工厂都查看同一个对象,使它们都变得对称。当需要一个与特定网络服务器无关的对象时,服务很有用。在这里,我们定义了一个公共服务类,其中包含用于动态创建工厂的方法。该服务还包含工厂将依赖的方法。

工厂创建方法 getFingerFactorygetFingerSetterFactory 遵循以下模式

  1. 实例化一个通用的服务器工厂,twisted.internet.protocol.ServerFactory

  2. 设置协议类,就像我们的工厂类一样。

  3. 将服务方法复制到工厂作为函数属性。该函数将无法访问工厂的 self ,但这没关系,因为它作为绑定方法可以访问服务的 self ,这就是它需要的。对于 getUser ,复制了在服务中定义的自定义方法。对于 setUser ,复制了 users 字典的标准方法。

因此,我们停止了子类化:服务只是将有用的方法和属性放入工厂中。我们在协议设计方面做得越来越好:我们的协议类都不需要更改,在教程结束之前也不需要更改。

作为应用程序服务,这个新的 Finger 服务实现了 IService 接口,可以以标准化的方式启动和停止。我们将在下一个示例中使用它。

finger13.tac

# Fix asymmetry
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 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 FingerSetterProtocol(basic.LineReceiver):
    def connectionMade(self):
        self.lines = []

    def lineReceived(self, line):
        self.lines.append(line)

    def connectionLost(self, reason):
        user = self.lines[0]
        status = self.lines[1]
        self.factory.setUser(user, status)


class FingerService(service.Service):
    def __init__(self, users):
        self.users = users

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

    def setUser(self, user, status):
        self.users[user] = status

    def getFingerFactory(self):
        f = protocol.ServerFactory()
        f.protocol = FingerProtocol
        f.getUser = self.getUser
        return f

    def getFingerSetterFactory(self):
        f = protocol.ServerFactory()
        f.protocol = FingerSetterProtocol
        f.setUser = self.setUser
        return f


application = service.Application("finger", uid=1, gid=1)
f = FingerService({b"moshez": b"Happy and well"})
serviceCollection = service.IServiceCollection(application)
strports.service("tcp:79", f.getFingerFactory()).setServiceParent(serviceCollection)
strports.service("tcp:1079", f.getFingerSetterFactory()).setServiceParent(
    serviceCollection
)

大多数应用程序服务将希望使用 Service 基类,它实现了所有通用的 IService 行为。

读取状态文件

这个版本展示了如何从一个集中管理的文件中读取数据,而不是仅仅让用户设置他们的消息。我们缓存结果,每 30 秒刷新一次。服务对于此类计划任务很有用。

listings/finger/etc.users

finger14.tac

# Read from file
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 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 FingerService(service.Service):
    def __init__(self, filename):
        self.users = {}
        self.filename = filename

    def _read(self):
        with open(self.filename, "rb") as f:
            for line in f:
                user, status = line.split(b":", 1)
                user = user.strip()
                status = status.strip()
                self.users[user] = status
        self.call = reactor.callLater(30, self._read)

    def startService(self):
        self._read()
        service.Service.startService(self)

    def stopService(self):
        service.Service.stopService(self)
        self.call.cancel()

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

    def getFingerFactory(self):
        f = protocol.ServerFactory()
        f.protocol = FingerProtocol
        f.getUser = self.getUser
        return f


application = service.Application("finger", uid=1, gid=1)
f = FingerService("/etc/users")
finger = strports.service("tcp:79", f.getFingerFactory())

finger.setServiceParent(service.IServiceCollection(application))
f.setServiceParent(service.IServiceCollection(application))

由于这个版本从文件读取数据(并且每 30 秒刷新一次数据),因此没有 FingerSetterFactory ,因此没有监听端口 1079 的东西。

在这里,我们覆盖了标准的 startServicestopService 钩子,它们在 Finger 服务中被设置,该服务作为应用程序的子服务在代码的最后一行被设置。 startService 调用 _read ,该函数负责读取数据;然后使用 reactor.callLater 将其安排在每次调用后三十秒后再次运行。 reactor.callLater 返回一个对象,该对象允许我们使用其 cancel 方法在 stopService 中取消计划的运行。

在 Web 上发布,太

同类型的服务也可以生成对其他协议有用的东西。例如,在 twisted.web 中,工厂本身 (Site ) 几乎从不进行子类化 - 相反,它被赋予了一个资源,该资源代表通过 URL 可用的资源树。该层次结构由 Site 导航,并且可以使用 getChild 动态覆盖它。

为了将其集成到 Finger 应用程序中(仅仅因为我们可以),我们设置了一个新的 TCPServer,它调用 Site 工厂并通过 FingerService 的一个新函数 getResource 检索资源。此函数专门返回一个 Resource 对象,该对象具有一个覆盖的 getChild 方法。

finger15.tac

# Read from file, announce on the web!
import html

from twisted.application import service, strports
from twisted.internet import defer, protocol, reactor
from twisted.protocols import basic
from twisted.web import resource, server, static


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 FingerResource(resource.Resource):
    def __init__(self, users):
        self.users = users
        resource.Resource.__init__(self)

    # we treat the path as the username
    def getChild(self, username, request):
        """
        'username' is L{bytes}.
        'request' is a 'twisted.web.server.Request'.
        """
        messagevalue = self.users.get(username)
        if messagevalue:
            messagevalue = messagevalue.decode("ascii")
        if username:
            username = username.decode("ascii")
        username = html.escape(username)
        if messagevalue is not None:
            messagevalue = html.escape(messagevalue)
            text = f"<h1>{username}</h1><p>{messagevalue}</p>"
        else:
            text = f"<h1>{username}</h1><p>No such user</p>"
        text = text.encode("ascii")
        return static.Data(text, "text/html")


class FingerService(service.Service):
    def __init__(self, filename):
        self.filename = filename
        self.users = {}

    def _read(self):
        self.users.clear()
        with open(self.filename, "rb") as f:
            for line in f:
                user, status = line.split(b":", 1)
                user = user.strip()
                status = status.strip()
                self.users[user] = status
        self.call = reactor.callLater(30, self._read)

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

    def getFingerFactory(self):
        f = protocol.ServerFactory()
        f.protocol = FingerProtocol
        f.getUser = self.getUser
        return f

    def getResource(self):
        r = FingerResource(self.users)
        return r

    def startService(self):
        self._read()
        service.Service.startService(self)

    def stopService(self):
        service.Service.stopService(self)
        self.call.cancel()


application = service.Application("finger", uid=1, gid=1)
f = FingerService("/etc/users")
serviceCollection = service.IServiceCollection(application)
f.setServiceParent(serviceCollection)
strports.service("tcp:79", f.getFingerFactory()).setServiceParent(serviceCollection)
strports.service("tcp:8000", server.Site(f.getResource())).setServiceParent(
    serviceCollection
)

在 IRC 上发布,太

这是第一次出现客户端代码。IRC 客户端通常的行为非常像服务器:响应来自网络的事件。客户端服务将确保断开的链接将被重新建立,并使用智能调整的指数退避算法。IRC 客户端本身很简单:唯一的真正技巧是在 connectionMade 中从工厂获取昵称。

finger16.tac

# Read from file, announce on the web, irc
from twisted.application import internet, service, strports
from twisted.internet import defer, endpoints, protocol, reactor
from twisted.protocols import basic
from twisted.web import resource, server, static
from twisted.words.protocols import irc


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 IRCReplyBot(irc.IRCClient):
    def connectionMade(self):
        self.nickname = self.factory.nickname
        irc.IRCClient.connectionMade(self)

    def privmsg(self, user, channel, msg):
        user = user.split("!")[0]
        if self.nickname.lower() == channel.lower():
            d = self.factory.getUser(msg.encode("ascii"))

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

            d.addErrback(onError)

            def writeResponse(message):
                message = message.decode("ascii")
                irc.IRCClient.msg(self, user, msg + ": " + message)

            d.addCallback(writeResponse)


class FingerService(service.Service):
    def __init__(self, filename):
        self.filename = filename
        self.users = {}

    def _read(self):
        self.users.clear()
        with open(self.filename, "rb") as f:
            for line in f:
                user, status = line.split(b":", 1)
                user = user.strip()
                status = status.strip()
                self.users[user] = status
        self.call = reactor.callLater(30, self._read)

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

    def getFingerFactory(self):
        f = protocol.ServerFactory()
        f.protocol = FingerProtocol
        f.getUser = self.getUser
        return f

    def getResource(self):
        def getData(path, request):
            user = self.users.get(path, b"No such users <p/> usage: site/user")
            path = path.decode("ascii")
            user = user.decode("ascii")
            text = f"<h1>{path}</h1><p>{user}</p>"
            text = text.encode("ascii")
            return static.Data(text, "text/html")

        r = resource.Resource()
        r.getChild = getData
        return r

    def getIRCBot(self, nickname):
        f = protocol.ClientFactory()
        f.protocol = IRCReplyBot
        f.nickname = nickname
        f.getUser = self.getUser
        return f

    def startService(self):
        self._read()
        service.Service.startService(self)

    def stopService(self):
        service.Service.stopService(self)
        self.call.cancel()


application = service.Application("finger", uid=1, gid=1)
f = FingerService("/etc/users")
serviceCollection = service.IServiceCollection(application)
f.setServiceParent(serviceCollection)
strports.service("tcp:79", f.getFingerFactory()).setServiceParent(serviceCollection)
strports.service("tcp:8000", server.Site(f.getResource())).setServiceParent(
    serviceCollection
)
internet.ClientService(
    endpoints.clientFromString(reactor, "tcp:irc.freenode.org:6667"),
    f.getIRCBot("fingerbot"),
).setServiceParent(serviceCollection)

FingerService 现在又有了另一个新函数 getIRCbot ,它返回一个 ClientFactory 。这个工厂反过来将实例化 IRCReplyBot 协议。IRCBot 在最后一行被配置为连接到 irc.libera.chat ,昵称是 fingerbot

通过覆盖 irc.IRCClient.connectionMadeIRCReplyBot 可以访问实例化它的工厂的 nickname 属性。

添加 XML-RPC 支持

在 Twisted 中,XML-RPC 支持就像另一个资源一样被处理。该资源仍然通过 render() 支持 GET 调用,但这通常没有实现。请注意,可以从 XML-RPC 方法返回延迟。当然,客户端在延迟被触发之前不会得到答案。

finger17.tac

# Read from file, announce on the web, irc, xml-rpc
from twisted.application import internet, service, strports
from twisted.internet import defer, endpoints, protocol, reactor
from twisted.protocols import basic
from twisted.web import resource, server, static, xmlrpc
from twisted.words.protocols import irc


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 IRCReplyBot(irc.IRCClient):
    def connectionMade(self):
        self.nickname = self.factory.nickname
        irc.IRCClient.connectionMade(self)

    def privmsg(self, user, channel, msg):
        user = user.split("!")[0]
        if self.nickname.lower() == channel.lower():
            d = self.factory.getUser(msg.encode("ascii"))

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

            d.addErrback(onError)

            def writeResponse(message):
                message = message.decode("ascii")
                irc.IRCClient.msg(self, user, msg + ": " + message)

            d.addCallback(writeResponse)


class FingerService(service.Service):
    def __init__(self, filename):
        self.filename = filename
        self.users = {}

    def _read(self):
        self.users.clear()
        with open(self.filename, "rb") as f:
            for line in f:
                user, status = line.split(b":", 1)
                user = user.strip()
                status = status.strip()
                self.users[user] = status
        self.call = reactor.callLater(30, self._read)

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

    def getFingerFactory(self):
        f = protocol.ServerFactory()
        f.protocol = FingerProtocol
        f.getUser = self.getUser
        return f

    def getResource(self):
        def getData(path, request):
            user = self.users.get(path, b"No such user <p/> usage: site/user")
            path = path.decode("ascii")
            user = user.decode("ascii")
            text = f"<h1>{path}</h1><p>{user}</p>"
            text = text.encode("ascii")
            return static.Data(text, "text/html")

        r = resource.Resource()
        r.getChild = getData
        x = xmlrpc.XMLRPC()
        x.xmlrpc_getUser = self.getUser
        r.putChild("RPC2", x)
        return r

    def getIRCBot(self, nickname):
        f = protocol.ClientFactory()
        f.protocol = IRCReplyBot
        f.nickname = nickname
        f.getUser = self.getUser
        return f

    def startService(self):
        self._read()
        service.Service.startService(self)

    def stopService(self):
        service.Service.stopService(self)
        self.call.cancel()


application = service.Application("finger", uid=1, gid=1)
f = FingerService("/etc/users")
serviceCollection = service.IServiceCollection(application)
f.setServiceParent(serviceCollection)
strports.service("tcp:79", f.getFingerFactory()).setServiceParent(serviceCollection)
strports.service("tcp:8000", server.Site(f.getResource())).setServiceParent(
    serviceCollection
)
internet.ClientService(
    endpoints.clientFromString(reactor, "tcp:irc.freenode.org:6667"),
    f.getIRCBot("fingerbot"),
).setServiceParent(serviceCollection)

我们可以使用基于 Python 内置的 xmlrpclib 的简单客户端来测试 XMLRPC finger,该客户端将访问我们在 localhost/RPC2 上提供的资源。

fingerXRclient.py

# testing xmlrpc finger

try:
    # Python 3
    from xmlrpc.client import Server
except ImportError:
    # Python 2
    from xmlrpclib import Server

server = Server("http://127.0.0.1:8000/RPC2")
print(server.getUser("moshez"))