编写客户端

概述

Twisted 是一个旨在非常灵活的框架,可以让你编写强大的客户端。这种灵活性的代价是在编写客户端时需要多层。本文档涵盖了创建可用于 TCP、SSL 和 Unix 套接字的客户端。UDP 在另一份文档中进行了介绍。

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

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

协议

如上所述,这个类以及辅助类和函数是大多数代码所在的地方。Twisted 协议以异步方式处理数据。这意味着协议从不等待事件,而是根据从网络接收到的事件做出响应。

以下是一个简单的示例

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

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

这是最简单的协议之一。它只是将从连接中读取的任何内容写入标准输出。它没有响应许多事件。以下是一个Protocol 响应另一个事件的示例

from twisted.internet.protocol import Protocol

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

此协议连接到服务器,向其发送欢迎消息,然后终止连接。

connectionMade 事件通常是Protocol 对象设置的地方,以及任何初始问候(如上面的WelcomeMessage 协议)。任何Protocol 特定对象的拆卸都在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("MESSAGE %s\n" % msg)

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

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

无论客户端端点类型如何,设置新连接的方法都是简单地将它传递给connectProtocol 以及协议实例。这意味着可以轻松地更改用于连接的机制,而无需更改程序的其余部分。例如,要通过 SSL 运行 greeter 示例,唯一需要的更改是实例化一个SSL4ClientEndpoint 而不是TCP4ClientEndpoint。为了利用这一点,启动新连接的函数和方法通常应该接受端点作为参数,并让调用者构造它,而不是接受诸如“主机”和“端口”之类的参数并构造自己的端点。

有关不同方法(以及解析字符串为端点)的信息,请参阅端点 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,并且还接收与连接状态相关的事件。这使它能够执行诸如在连接错误时重新连接之类的操作。以下是一个使用Echo 协议(如上)的简单ClientFactory 示例,它还打印连接状态。

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
reactor.connectTCP(host, port, EchoClientFactory())
reactor.run()

请注意,clientConnectionFailed 在无法建立连接时被调用,而clientConnectionLost 在建立连接后断开连接时被调用。

Reactor 客户端 API

connectTCP

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

重新连接

通常,客户端的连接会由于网络问题而意外断开。重新连接的一种方法是在连接断开后调用 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)

更高级别的示例:ircLogBot

ircLogBot 概述

到目前为止,客户端都相当简单。Twisted Words 在 doc/words/examples 目录中提供了一个更复杂的示例。

ircLogBot.py

# 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>
"""


import sys

# system imports
import time

from twisted.internet import protocol, reactor
from twisted.python import log

# twisted imports
from twisted.words.protocols import irc


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(f"{timestamp} {message}\n")
        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(f"<{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(f"<{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(f"* {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(f"{old_nick} is now known as {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 服务器,加入频道,并将所有流量记录到文件中。它演示了在连接丢失时重新连接的连接级逻辑,以及在 Factory 中存储持久数据。

工厂中的持久数据

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

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 类是 IProtocol 的基本实现,在大多数 Twisted 应用程序中为了方便起见而使用。要了解完整的 IProtocol 接口,请参阅 IProtocol 的 API 文档。

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

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