本帖最后由 阳 于 2018-11-06 08:21:17 编辑。

Writing Clients

Overview

Twisted 是一种非常灵活的框架, 它可以编写强大的客户端。 这种灵活性的代价是在编写客户端时要花费几层。 本文档介绍如何创建可用于TCP、SSL和Unix套接字的客户端。UDP在另一个文档中介绍。

在底层,实现协议解析和处理的地方是 Protocol 类。这个类通常是 twisted.internet.protocol.Protocol 的子类。大多数协议处理程序要么从这个类继承,要么从它的子类继承。当您连接到服务器时,协议类的一个实例将被实例化,当连接完成时,它将消失。这意味着在协议中不保存持久配置。

持久性配置保存在 Factory 类中,工厂类通常继承自 twisted.internet.protocol.Factory (或 twisted.internet.protocol.ClientFactory: 参见下面)。默认的工厂类只是实例化 Protocol,然后将协议对象的 factory 属性设置为指向自己(the factory)。这允许 Protocol 访问并可能修改持久配置。

Protocol

如上所述,大部分代码都位于Protocol类、辅助类和函数中。Twisted 协议处理程序以异步方式处理数据。这意味着协议从不等待事件,而是在事件从网络到达时响应它们。

这里有一个简单的例子:

from twisted.internet.protocol import Protocol
from sys import stdout

class Echo(Protocol):
    def dataReceived(self, data):
        stdout.write(data)

这是最简单的协议之一。它只是从连接中读取数据并写人到标准输出。有许多事件它没有回应。

这里有一个协议响应事件的例子:

from twisted.internet.protocol import Protocol

class WelcomeMessage(Protocol):
    def connectionMade(self):
        self.transport.write(b"Hello server, I am the client!\r\n")
        self.transport.loseConnection()

该协议连接到服务器,发送一条欢迎消息,然后终止连接。

connectionMade 事件通常是设置协议对象以及任何初始问候语的地方(如上面的 WelcomeMessage 协议)。任何对协议对象的销毁都是在 connectionLost 中完成的。

简单的一次性客户

在许多情况下,协议只需要连接服务器一次,而代码只想获得协议的连接实例。在这些情况下 twisted.internet.endpoints 提供了适当的API,特别是在 connectProtocol 中需要提供协议实例而不是工厂。

from twisted.internet import reactor
from twisted.internet.protocol import Protocol
from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol

class Greeter(Protocol):

    def sendMessage(self, msg):
        self.transport.write(b"MESSAGE %s\n" % msg)

def gotProtocol(p):
    p.sendMessage(b"Hello")
    reactor.callLater(1, p.sendMessage, b"This is sent in a second")
    reactor.callLater(2, p.transport.loseConnection)

if __name__ == "__main__":
    point = TCP4ClientEndpoint(reactor, "localhost", 1234)
    d = connectProtocol(point, Greeter())
    d.addCallback(gotProtocol)
    reactor.run()

无论客户端端点的类型如何,建立新连接的方法都是将其连同协议实例一起传递给 connectProtocol。 这意味着您可以很容易地更改您所使用的连接机制,而不会改变程序的其他部分。例如,要在SSL上运行上面示例,惟一需要的更改是实例化 SSL4ClientEndpoint 而不是 TCP4ClientEndpoint。 为了利用这一点,启动新连接的函数和方法通常应该接受端点作为参数,并让调用者构造它,而不是接受“host”和“port”这样的参数并构建自己的端点。

要了解关于不同方式的更多信息,您可以建立到不同类型端点的传出连接,以及将字符串解析为端点,请参阅端点API的文档

您可能会遇到使用 ClientCreator 的代码,它是一种较老的API,不如端点API灵活。这样的代码不会调用端点上的connect,而是如下所示:

from twisted.internet.protocol import ClientCreator

...

creator = ClientCreator(reactor, Greeter)
d = creator.connectTCP("localhost", 1234)
d.addCallback(gotProtocol)
reactor.run()

通常,在新代码中应该首选端点API,因为它允许调用者选择连接的方法。

ClientFactory

尽管如此,仍然有大量使用低级api的代码,还有一些特性(比如自动重新连接)还没有重新实现到端点,因此在某些情况下,它们可能更方便使用。

要使用较低级别的连接api,您将需要调用其中一个 reactor.connect* 方法。对于这些情况,您需要一个 ClientFactoryClientFactory 负责创建 Protocol,并接收与连接状态相关的事件。这允许它在发生连接错误时重新连接。下面是一个简单的 ClientFactory 示例,它使用 Echo 协议(上面的)并打印连接的状态。

from twisted.internet.protocol import Protocol, ClientFactory
from sys import stdout

class Echo(Protocol):
    def dataReceived(self, data):
        stdout.write(data)

class EchoClientFactory(ClientFactory):
    def startedConnecting(self, connector):
        print('Started to connect.')

    def buildProtocol(self, addr):
        print('Connected.')
        return Echo()

    def clientConnectionLost(self, connector, reason):
        print('Lost connection.  Reason:', reason)

    def clientConnectionFailed(self, connector, reason):
        print('Connection failed. Reason:', reason)

要将这个 EchoClientFactory 连接到服务器,可以使用以下代码:

from twisted.internet import reactor

if __name__ == "__main__":
    reactor.connectTCP(host, port, EchoClientFactory())
    reactor.run()

请注意,当连接无法建立时,调用 clientConnectionFailed,在创建连接并断开连接时调用 clientConnectionLost

Reactor Client APIs

connectTCP

IReactorTCP.connectTCP 为IPv4和IPv6 TCP客户端提供支持。它接受的 host 参数可以是主机名,也可以是IP地址。在主机名的情况下,反应器将在尝试连接之前自动将名称解析为IP地址。这意味着,对于有多个地址记录的主机名,重新连接尝试可能并不总是指向同一个服务器(参见下面)。它还意味着每个连接尝试都有名称解析开销。如果您正在创建许多短期连接(通常每秒数百或数千次),那么您可能希望首先将主机名解析为一个地址,然后将地址传递给connectTCP

Reconnection

通常,客户端连接会由于网络问题而无意中丢失。断开连接后重新连接的一种方法是在连接丢失时调用 connector.connect():

from twisted.internet.protocol import ClientFactory

class EchoClientFactory(ClientFactory):

    def clientConnectionLost(self, connector, reason):
        connector.connect()

作为第一个参数传递的连接器是连接和协议之间的接口。当连接失败并且工厂接收到 clientConnectionLost 事件时,工厂可以调用 connector.connect() 重新从头开始连接。

然而,大多数希望这个功能的程序应该实现 ReconnectingClientFactory,它试图在连接丢失或失败的情况下重新连接,并且以指数级延迟重复的重新连接尝试。

以下是通过 ReconnectingClientFactory 实现的Echo协议:

from twisted.internet.protocol import Protocol, ReconnectingClientFactory
from sys import stdout

class Echo(Protocol):
    def dataReceived(self, data):
        stdout.write(data)

class EchoClientFactory(ReconnectingClientFactory):
    def startedConnecting(self, connector):
        print('Started to connect.')

    def buildProtocol(self, addr):
        print('Connected.')
        print('Resetting reconnection delay')
        self.resetDelay()
        return Echo()

    def clientConnectionLost(self, connector, reason):
        print('Lost connection.  Reason:', reason)
        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)

    def clientConnectionFailed(self, connector, reason):
        print('Connection failed. Reason:', reason)
        ReconnectingClientFactory.clientConnectionFailed(self, connector,
                                                         reason)

A Higher-Level Example: ircLogBot

Overview of ircLogBot

到目前为止,客户端相当简单。一个更复杂的例子是 doc/words/examples 目录中的Twisted Words。

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
An example IRC log bot - logs a channel's events to a file.

If someone says the bot's name in the channel followed by a ':',
e.g.

    <foo> logbot: hello!

the bot will reply:

    <logbot> foo: I am a log bot

Run this script with two arguments, the channel name the bot should
connect to, and file to log to, e.g.:

    $ python ircLogBot.py test test.log

will log channel #test to the file 'test.log'.

To run the script:

    $ python ircLogBot.py <channel> <file>
"""

# twisted imports
from twisted.words.protocols import irc
from twisted.internet import reactor, protocol
from twisted.python import log

# system imports
import time, sys

class MessageLogger:
    """
    An independent logger class (because separation of application
    and protocol logic is a good thing).
    """

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

    def log(self, message):
        """Write a message to the file."""
        timestamp = time.strftime("[%H:%M:%S]", time.localtime(time.time()))
        self.file.write('%s %s\n' % (timestamp, message))
        self.file.flush()

    def close(self):
        self.file.close()

class LogBot(irc.IRCClient):
    """A logging IRC bot."""

    nickname = "twistedbot"

    def connectionMade(self):
        irc.IRCClient.connectionMade(self)
        self.logger = MessageLogger(open(self.factory.filename, "a"))
        self.logger.log("[connected at %s]" %
                        time.asctime(time.localtime(time.time())))

    def connectionLost(self, reason):
        irc.IRCClient.connectionLost(self, reason)
        self.logger.log("[disconnected at %s]" %
                        time.asctime(time.localtime(time.time())))
        self.logger.close()

    # callbacks for events

    def signedOn(self):
        """Called when bot has successfully signed on to server."""
        self.join(self.factory.channel)

    def joined(self, channel):
        """This will get called when the bot joins the channel."""
        self.logger.log("[I have joined %s]" % channel)

    def privmsg(self, user, channel, msg):
        """This will get called when the bot receives a message."""
        user = user.split('!', 1)[0]
        self.logger.log("<%s> %s" % (user, msg))

        # Check to see if they're sending me a private message
        if channel == self.nickname:
            msg = "It isn't nice to whisper!  Play nice with the group."
            self.msg(user, msg)
            return

        # Otherwise check to see if it is a message directed at me
        if msg.startswith(self.nickname + ":"):
            msg = "%s: I am a log bot" % user
            self.msg(channel, msg)
            self.logger.log("<%s> %s" % (self.nickname, msg))

    def action(self, user, channel, msg):
        """This will get called when the bot sees someone do an action."""
        user = user.split('!', 1)[0]
        self.logger.log("* %s %s" % (user, msg))

    # irc callbacks

    def irc_NICK(self, prefix, params):
        """Called when an IRC user changes their nickname."""
        old_nick = prefix.split('!')[0]
        new_nick = params[0]
        self.logger.log("%s is now known as %s" % (old_nick, new_nick))

    # For fun, override the method that determines how a nickname is changed on
    # collisions. The default method appends an underscore.
    def alterCollidedNick(self, nickname):
        """
        Generate an altered version of a nickname that caused a collision in an
        effort to create an unused related name for subsequent registration.
        """
        return nickname + '^'

class LogBotFactory(protocol.ClientFactory):
    """A factory for LogBots.

    A new protocol instance will be created each time we connect to the server.
    """

    def __init__(self, channel, filename):
        self.channel = channel
        self.filename = filename

    def buildProtocol(self, addr):
        p = LogBot()
        p.factory = self
        return p

    def clientConnectionLost(self, connector, reason):
        """If we get disconnected, reconnect to server."""
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        print("connection failed:", reason)
        reactor.stop()

if __name__ == '__main__':
    # initialize logging
    log.startLogging(sys.stdout)

    # create factory protocol and application
    f = LogBotFactory(sys.argv[1], sys.argv[2])

    # connect factory to this host and port
    reactor.connectTCP("irc.freenode.net", 6667, f)

    # run bot
    reactor.run()

ircLogBot.py 连接到IRC服务器,连接一个通道,并将其上的所有通信记录到一个文件中。它演示了在丢失的连接上重新连接的连接级别逻辑,以及在工厂中存储持久数据。

工厂中的持久数据

由于每次建立连接时都会重新创建协议实例,因此客户机需要某种方式来跟踪应该持久化的数据。对于日志机器人,它需要知道它正在记录哪个通道,以及记录它的位置。

from twisted.words.protocols import irc
from twisted.internet import protocol

class LogBot(irc.IRCClient):

    def connectionMade(self):
        irc.IRCClient.connectionMade(self)
        self.logger = MessageLogger(open(self.factory.filename, "a"))
        self.logger.log("[connected at %s]" %
                        time.asctime(time.localtime(time.time())))

    def signedOn(self):
        self.join(self.factory.channel)

class LogBotFactory(protocol.ClientFactory):

    def __init__(self, channel, filename):
        self.channel = channel
        self.filename = filename

    def buildProtocol(self, addr):
        p = LogBot()
        p.factory = self
        return p

当创建协议时,它将获得对工厂的引用 self.factory。然后,它可以在逻辑中访问工厂的属性。对于LogBot,它打开文件并连接到存储在工厂中的通道。

工厂有一个默认的 buildProtocol 实现。它执行与上面示例相同的操作,使用工厂的 protocol 属性来创建协议实例。在上面的例子中,工厂可以被改写成这样:

class LogBotFactory(protocol.ClientFactory):
    protocol = LogBot

    def __init__(self, channel, filename):
        self.channel = channel
        self.filename = filename

进一步的阅读

本文档中使用的 Protocol 类是大多数 Twisted 应用程序中使用的 IProtocol 的基本实现,这是为了方便起见。要了解完整的 IProtocol 接口,请参阅 IProtocol 的API文档

本文档中某些示例中使用的 transport 属性提供了 ITCPTransport 接口。要了解完整的接口,请参阅 ITCPTransport 的API文档

接口类是一种指定对象具有哪些方法和属性以及它们的行为的方式。有关在Twisted中使用接口的更多信息,请参阅组件:接口和适配器文档

共收到 0 条回复