注意py2 和 py3 的区别:

官方代码环境为 python2.x 使用 python3.x 需要把 Protocol.transport.write() 使用 encode()编码为 b'xxx'收到信息之后使用 decode() 转换为 str

概览

Twisted 是一个非常灵活的框架,可以用它写出非常强大的客户端。灵活的代价就是需要写多层客户端代码,本文档涵盖了使用 Twisted 创建TCP、SSL、Unix socket 客户端的方法, UDP 客户端将在另外一篇文档中讲解。

在底层,实际执行协议解析和处理的地方是Protocol类。这个类通常来自twisted.internet.protocol.Protocol, 大多数 protocol 处理程序都从此类或此类的子孙类继承,当连接到服务器时,Protocol 类将被实例化,在断开连接之后此实例将消失。这意味着协议中不会持久保存配置。

持久性保存配置应该在 Factory 类中,这个类通常来自twisted.internet.protocol.Factory 或者 twisted.internet.protocol.ClientFactory。 默认工厂类仅实例化 protocol ,然后将 protocol 的工厂属性设置为指向其自身(factory),这样就允许 Protocol 访问并可以修改持久性配置。

Protocol

如上所述,通常大多数代码会被编写在此类及其辅助的类和函数中,Twisted 协议以异步方式处理数据。这意味着它从不等待事件,而只是响应(网络)事件。

例程如下:

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

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

这是一个最简单的 protocols 实现之一,它从连接中收到什么就回应什么数据,但是有很多事件它并会不响应(比如:客户端连接事件、断开连接事件等),下面代码用来响应其他部分事件:

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()

上面代码连接到服务器,发送一个欢迎消息,然后终止连接。

简单的一次性客户端

在很多情况下,客户端只需要连接一次服务器,而代码只是想获得一个连接的 protocol 实例, 在这种情况下 twisted.internet.endpoints 提供了一个适当的 API, 特别是 connectProtocol ,它只需要 protocol 实例而不是一个 factory。

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")
    # 连接到 localhost:1234 服务端之后会在服务端接收到信息
    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()  

无论客户端的类型如何,只需将新连接的方式和 Protocol 实例传递给 connectProtocol,这意味着用户可以轻松地更改正在使用的连接机制,而无需更改程序的其余部分。 例如:要通过SSL运行 greeter 实例,唯一需要更改的就是实例化 SSL4ClientEndpoint 而不是 TCP4ClientEndpoint。 为了利用这一点,启动新连接的函数和方法通常应该接受端点作为参数,并让调用者构造它,而不是采用诸如 “host” 和 “port” 这样的参数并构造自己的参数。有关不同方法的更多信息,可以将传出连接发送到不同类型的端点,以及将字符串解析为端点,请参阅端点API的文档。您可能会遇到使用 ClientCreator 的代码,该 APICreator 不像端点API那么灵活。而不是在端点上调用连接,这样的代码将如下所示:

from twisted.internet.protocol import ClientCreator

...

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

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

ClientFactory

尽管如此,还有很多代码需要使用更底层的API,使用端点重新实现一些功能(如自动重新连接),所以在某些情况下,底层 SPI 会更方便使用。

要想使用更底层的 API,需要直接调用 reactor.connect* 方法,在这种情况下,需要一个 ClientFactory 类,ClientFactory负责创建协议,并且还接收与连接状态有关的事件。 这样可以在连接错误的情况下执行重新连接。以下是使用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)

使用如下代码连接服务器:

from twisted.internet import reactor  
reactor.connectTCP(host, port, EchoClientFactory())  
reactor.run()  

注意: 在无法建立连接的时候会调用 clientConnectionFailed, 连接创建成功之后被断开会调用 clientConnectionLost 方法。

Reactor Client APIs

connectTCP

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

Reconnection

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

from twisted.internet.protocol import ClientFactory

class EchoClientFactory(ClientFactory):  
    def clientConnectionLost(self, connector, reason):
        connector.connect()

connector 作为一个连接和协议之间接口的第一个参数,当连接失败并且 factory 收到 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 目录寻找复杂的客户端例程。

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


from __future__ import print_function

# 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服务器,加入一个通道,并将其上的所有流量记录到文件中。它演示了在丢失的连接上重新连接的一些连接级逻辑,以及在Factory中存储持久数据。

在 Factory 中持久化数据

由于每次建立连接时都会重新创建协议实例,所以客户端需要一些方法来跟踪应该保留的数据。例如日志机器人,它需要知道使用哪个通道记录日志、记录在哪里。

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 ,然后它可以在其逻辑中访问工厂的属性,在日志机器人例程中,它打开文件并连接到工厂中存储的通道。工厂有一个默认的 buildProtocol 实现。上面的例子使用工厂的协议属性来创建协议实例也是一样的,在上面的示例中,工厂可以重写为:

class LogBotFactory(protocol.ClientFactory):  
    protocol = LogBot

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

更多

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

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

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