使用 Twisted 创建 XML-RPC 服务器和客户端

简介

XML-RPC 是一种简单的请求/回复协议,运行在 HTTP 之上。它简单易行,易于实现,并得到大多数编程语言的支持。Twisted 的 XML-RPC 支持是使用 `xmlrpclib <https://docs.pythonlang.cn/library/xmlrpclib.html>`_ 库实现的,该库包含在 Python 2.2 及更高版本中。

创建 XML-RPC 服务器

创建服务器非常简单 - 您只需从 twisted.web.xmlrpc.XMLRPC 继承即可。然后,您创建以 xmlrpc_ 开头的函数。函数的参数决定了它将从 XML-RPC 客户端接受哪些参数。结果将返回给客户端。

通过 XML-RPC 发布的函数可以返回所有基本的 XML-RPC 类型,例如字符串、列表等(只需返回一个普通的 Python 整数等)。它们也可以抛出异常或返回 Failure 实例以指示发生了错误,或者返回 BinaryBooleanDateTime 实例(所有这些都与 xmlrpclib 中的相应类相同。此外,XML-RPC 发布的函数可以返回 Deferred 实例,其结果是上述之一。这允许您返回无法立即计算的结果,例如数据库查询。有关更多详细信息,请参阅 Deferred 文档

XMLRPC 实例是 Resource 对象,因此可以使用 Site 发布它们。以下示例通过 XML-RPC 发布了两个函数,add(a, b)echo(x)

from twisted.web import xmlrpc, server

class Example(xmlrpc.XMLRPC):
    """
    An example object to be published.
    """

    def xmlrpc_echo(self, x):
        """
        Return all passed args.
        """
        return x

    def xmlrpc_add(self, a, b):
        """
        Return sum of arguments.
        """
        return a + b

    def xmlrpc_fault(self):
        """
        Raise a Fault indicating that the procedure should not be used.
        """
        raise xmlrpc.Fault(123, "The fault procedure is faulty.")

if __name__ == '__main__':
    from twisted.internet import reactor, endpoints
    r = Example()
    endpoint = endpoints.TCP4ServerEndpoint(reactor, 7080)
    endpoint.listen(server.Site(r))
    reactor.run()

运行此命令后,我们可以使用客户端连接并向服务器发送命令

>>> import xmlrpclib
>>> s = xmlrpclib.Server('http://localhost:7080/')
>>> s.echo("lala")
'lala'
>>> s.add(1, 2)
3
>>> s.fault()
Traceback (most recent call last):
...
xmlrpclib.Fault: <Fault 123: 'The fault procedure is faulty.'>
>>>

如果 Request 对象需要 xmlrpc_* 函数,可以使用 twisted.web.xmlrpc.withRequest() 装饰器提供它。使用此装饰器时,函数将以第一个参数的形式传递请求对象,在任何 XML-RPC 参数之前。例如

from twisted.web.xmlrpc import XMLRPC, withRequest
from twisted.web.server import Site

class Example(XMLRPC):
    @withRequest
    def xmlrpc_headerValue(self, request, headerName):
        return request.requestHeaders.getRawHeaders(headerName)

if __name__ == '__main__':
    from twisted.internet import reactor, endpoints
    endpoint = endpoints.TCP4ServerEndpoint(reactor, 7080)
    endpoint.listen(Site(Example()))
    reactor.run()

XML-RPC 资源也可以是正常 Twisted Web 服务器的一部分,使用资源脚本。以下是一个此类资源脚本的示例

xmlquote.rpy

from twisted.web import xmlrpc
import os

def getQuote():
    return "What are you talking about, William?"

class Quoter(xmlrpc.XMLRPC):
    
    def xmlrpc_quote(self):
        return getQuote()
    
resource = Quoter()

使用 XML-RPC 子处理程序

XML-RPC 资源可以嵌套,以便一个处理程序在调用具有给定前缀的函数时调用另一个处理程序。例如,要为 Example 类添加对 XML-RPC 函数 date.time() 的支持,您可以执行以下操作

import time
from twisted.web import xmlrpc, server

class Example(xmlrpc.XMLRPC):
    """
    An example object to be published.
    """

    def xmlrpc_echo(self, x):
        """
        Return all passed args.
        """
        return x

    def xmlrpc_add(self, a, b):
        """
        Return sum of arguments.
        """
        return a + b

class Date(xmlrpc.XMLRPC):
    """
    Serve the XML-RPC 'time' method.
    """

    def xmlrpc_time(self):
        """
        Return UNIX time.
        """
        return time.time()

if __name__ == '__main__':
    from twisted.internet import reactor, endpoints
    r = Example()
    date = Date()
    r.putSubHandler('date', date)
    endpoint = endpoints.TCP4ServerEndpoint(reactor, 7080)
    endpoint.listen(server.Site(r))
    reactor.run()

默认情况下,句点(‘.’)将前缀与函数名分隔开,但您可以通过覆盖基 XML-RPC 服务器中的 XMLRPC.separator 数据成员来使用不同的字符。使用此方法,可以将 XML-RPC 服务器嵌套到任意深度。

使用您自己的过程获取器

有时,您希望实现自己的获取最终实现的策略。例如,就像子处理程序一样,您希望将实现划分为单独的类,但可能不想在过程名称中引入 XMLRPC.separator。在这种情况下,只需覆盖 lookupProcedure(self, procedurePath) 函数并返回正确的可调用对象即可。否则,请抛出 twisted.web.xmlrpc.NoSuchFunction

xmlrpc-customized.py

from twisted.internet import endpoints
from twisted.web import server, xmlrpc


class EchoHandler:
    def echo(self, x):
        """
        Return all passed args
        """
        return x


class AddHandler:
    def add(self, a, b):
        """
        Return sum of arguments.
        """
        return a + b


class Example(xmlrpc.XMLRPC):
    """
    An example of using you own policy to fetch the handler
    """

    def __init__(self):
        xmlrpc.XMLRPC.__init__(self)
        self._addHandler = AddHandler()
        self._echoHandler = EchoHandler()

        # We keep a dict of all relevant
        # procedure names and callable.
        self._procedureToCallable = {
            "add": self._addHandler.add,
            "echo": self._echoHandler.echo,
        }

    def lookupProcedure(self, procedurePath):
        try:
            return self._procedureToCallable[procedurePath]
        except KeyError as e:
            raise xmlrpc.NoSuchFunction(
                self.NOT_FOUND, "procedure %s not found" % procedurePath
            )

    def listProcedures(self):
        """
        Since we override lookupProcedure, its suggested to override
        listProcedures too.
        """
        return ["add", "echo"]


if __name__ == "__main__":
    from twisted.internet import reactor

    r = Example()
    endpoint = endpoints.TCP4ServerEndpoint(reactor, 7080)
    endpoint.listen(server.Site(r))
    reactor.run()

添加 XML-RPC 自省支持

XML-RPC 具有非正式的 自省 API,它在 system 子处理程序中指定了三个函数,允许客户端查询服务器有关服务器 API 的信息。使用 XMLRPCIntrospection 类可以轻松地为 Example 类添加自省支持

from twisted.web import xmlrpc, server

class Example(xmlrpc.XMLRPC):
    """An example object to be published."""

    def xmlrpc_echo(self, x):
        """Return all passed args."""
        return x

    xmlrpc_echo.signature = [['string', 'string'],
                             ['int', 'int'],
                             ['double', 'double'],
                             ['array', 'array'],
                             ['struct', 'struct']]

    def xmlrpc_add(self, a, b):
        """Return sum of arguments."""
        return a + b

    xmlrpc_add.signature = [['int', 'int', 'int'],
                            ['double', 'double', 'double']]
    xmlrpc_add.help = "Add the arguments and return the sum."

if __name__ == '__main__':
    from twisted.internet import reactor, endpoints
    r = Example()
    xmlrpc.addIntrospection(r)
    endpoint = endpoints.TCP4ServerEndpoint(reactor, 7080)
    endpoint.listen(server.Site(r))
    reactor.run()

请注意函数属性 helpsignature,它们分别用于自省 API 函数 system.methodHelpsystem.methodSignature。如果没有指定 help 属性,则使用函数的文档字符串。

SOAP 支持

从 Twisted 开发者的角度来看,XML-RPC 支持和 SOAP 支持之间几乎没有区别。以下是一个 SOAP 使用示例

soap.rpy

from twisted.web import soap
import os

def getQuote():
    return "That beverage, sir, is off the hizzy."

class Quoter(soap.SOAPPublisher):
    """Publish one method, 'quote'."""

    def soap_quote(self):
        return getQuote()

resource = Quoter()                 

创建 XML-RPC 客户端

Twisted 中的 XML-RPC 客户端旨在看起来像熟悉 xmlrpclib 或 Perspective Broker 用户的东西,根据需要从两者中获取功能。有两个主要偏差与 xmlrpclib 的方式不同,应该注意

  1. 没有隐式 /RPC2。如果服务使用此路径进行 XML-RPC 调用,则必须显式提供它。

  2. 没有神奇的 __getattr__:必须通过显式 callRemote 进行调用。

Twisted 向 XML-RPC 客户端提供的接口是代理对象的接口:twisted.web.xmlrpc.Proxy。对象的构造函数接收一个 URL:它必须是 HTTP 或 HTTPS URL。当描述 XML-RPC 服务时,将提供该服务的 URL。

拥有一个代理对象后,您只需调用 callRemote 函数,它接受一个函数名和一个可变参数列表(但没有命名参数,因为 XML-RPC 不支持这些参数)。它返回一个延迟对象,该对象将在回调时返回结果。如果在任何级别出现任何错误,则将调用 errback。在底层连接出现问题的情况下(例如,超时),异常将是相关的 Twisted 错误;在非 200 状态的情况下,异常将是包含状态和消息的 IOError;在 XML-RPC 级别出现问题的情况下,异常将是 xmlrpclib.Fault

from twisted.web.xmlrpc import Proxy
from twisted.internet import reactor

def printValue(value):
    print(repr(value))
    reactor.stop()

def printError(error):
    print('error', error)
    reactor.stop()

proxy = Proxy('http://advogato.org/XMLRPC')
proxy.callRemote('test.sumprod', 3, 5).addCallbacks(printValue, printError)
reactor.run()

打印

[8, 15]

使用 XML-RPC 客户端进行调试

有时,XML-RPC 服务器可能会向您的客户端发送非标准的 XML-RPC 响应。在这些情况下,您可以使用 twisted.web.xmlrpcQueryFactory 访问来自服务器的原始 XML-RPC 响应。

您可以简单地记录响应内容字符串以进行调试,或者实现您自己的自定义 XML-RPC 编组器来处理非标准的 XML-RPC 响应。

可以在 docs/web/examples/xmlrpc-debug.py 中找到执行此操作的 Twisted 应用程序示例。

同时提供 SOAP 和 XML-RPC 服务

twisted.web.xmlrpc.XMLRPCtwisted.web.soap.SOAPPublisher 都是 Resource 。因此,要在同一个 Web 服务器中同时提供 XML-RPC 和 SOAP 服务,可以使用 putChild 方法。

以下示例使用一个空的 resource.Resource 作为 Site 的根资源,然后添加 /RPC2/SOAP 路径。

xmlAndSoapQuote.py

import os

from twisted.internet import endpoints
from twisted.web import resource, server, soap, xmlrpc


def getQuote():
    return "Victory to the burgeois, you capitalist swine!"


class XMLRPCQuoter(xmlrpc.XMLRPC):
    def xmlrpc_quote(self):
        return getQuote()


class SOAPQuoter(soap.SOAPPublisher):
    def soap_quote(self):
        return getQuote()


def main():
    from twisted.internet import reactor

    root = resource.Resource()
    root.putChild("RPC2", XMLRPCQuoter())
    root.putChild("SOAP", SOAPQuoter())
    endpoint = endpoints.TCP4ServerEndpoint(reactor, 7080)
    endpoint.listen(server.Site(root))
    reactor.run()


if __name__ == "__main__":
    main()

有关资源的更多详细信息,请参阅 Twisted Web 开发