编写客户端程序
twisted 框架是设计的十分灵活的,他可以让你编写强大的客户端程序。而是现在这种灵活性的代价仅仅是在编写客户端程序的时候多添加了几层。
实际上,你真正实现协议的解析和处理的是Protocol类。这个类一般是派生自 twisted.internet.protocol.Protocol.大多数的Protocol处理器都是继承这个类或者它的子类。Protocol类在连接服务器时进行实例化,断开连接时结束。这意味着持久配置并不是保存在Protocol中。
持久配置是保存在Factory类中,它通常继承自twisted.internet.protocol.Factory (or twisted.internet.protocol.ClientFactory : 参考下面),默认factory类实例化Protocol和设置Protocol的factory属性指向自己本身(Factory),这样可以访问、修改和持久化配置。
Protocol
一个Twisted Protocol通过异步方式处理数据,意味着Protocol不会等待事件,但是会响应从网络到达的事件。
这是简单例子:
from twisted.internet.protocol import Protocol from sys import stdout class Echo(Protocol): def dataReceived(self, data): stdout.write(data)
这是一个最简单的Protocol,它这是将所有读取数据写回到连接的标准输出,有很多它不响应的事件。这是一个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()
这个Protocol连接服务器,发送welcome信息,然后断开连接。
connectionMade事件是在Protocol实例出现时设置的,就像初始化消息。注销处理在Protocol对象的 connectionLost。
Simple, single-use clients
在大多数的情况下,protocol只需要连接到服务器一次,程序也只是想得到一个实例对象即可。在这种情况下,twisted.internet.endpoints为我们提供了适当的API。
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()
不管client endpoint的类型是什么,建立一个新的连接的方式就是简单地将它和一个Protocol的实例对象传给connectProtocol.这就意味着你不需要更改过多的代码就可以改变你创建连接的方式。
一个简单的例子就是:如果你想在SSL的协议下运行上述的Greeter,你只需要用SSL4ClientEndpoint替代TCP4ClientEndpoint即可
为了很好滴利用这个优点,用来开启一个连接的函数和方法通常是将一个endpoint作为一个参数,而这endpoint由他的调用者来构建,而不是简单地传入host和port,让函数和方法自己来构建endpoint对象实例。
ClientFactory
尽管如此,仍然有很多代码使用了较低级的api,还有一些特性(如自动重连接)还没有通过endpoint重新实现,所以在某些情况下使用它们可能更方便。
为了使用较低级的api,你需要直接去调用reactor.connect*方法。在这种情况下,你就需要ClientFactory来帮助你完成任务。ClientFactory同样可以创建Protocol实例对象和接收与网络连接状态相关的事件。这就使得其具有了在发生网络连接错误的时候,重新连接的能力。下面是一个简单的例子:
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)
启动之前设计的聊天服务,然后执行上述代码,即可得到连接成功:
---------服务启动后---------------------------
E:\desktop\TwisedLearn\simple_examples>python ClientConnectToServer.py
Started to connect.
Connected.
What your name?
---------服务终止后---------------------------
E:\desktop\TwisedLearn\simple_examples>python ClientConnectToServer.py
Started to connect.
Connection failed for reason : [Failure instance: Traceback (failure with no
frames): <class 'twisted.internet.error.ConnectionRefusedError'>: Connection
was refused by other side: 10061: 由于目标计算机积极拒绝,无法连接。.
] .
Reactor Client APIs
connectTCP()
IReactorTCP.connectTCP
提供了IPv4和IPv6两种客户端。
详细的API参考 IReactorTCP.connectTCP.
Reconnection
客户端的连接经常会因为网络连接的各种问题而发生中断。当连接断开的时候,一种在断开连接重新连接的方式是通过调用connector.connect()
.
from twisted.internet.protocol import ClientFactory class EchoClientFactory(ClientFactory): def clientConnectionLost(self, connector, reason): connector.connect()
connector作为这个接口的第一个参数传入,处在一个连接和一个protocol之间。当这个网络连接失败的时候,这个factory接收到一个connectionLost事件,随之调用connector.connect()从断开的地方重新开始连接。
然而,在大多数的情况下,我们希望使用一个实现了ReconnectingClientFactory接口的一个方法,它会在连接丢失或失败时尝试重新连接,并以指数级延迟重复的重新连接尝试。下面是一个例子:
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
# 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("{} {}\n".format(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("<{}> {}".format(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("<{}> {}".format(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("* {} {}".format(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("{} is now known as {}".format(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()
运行如下命令:(在连接成功之后,我断开了网络连接测试自动重新连接)
$ python ircLogBot.py test log.log
E:\desktop\TwisedLearn\simple_examples>python ircLogBot.py test log.log
2021-04-24 18:02:33+0800 [-] Log opened.
2021-04-24 18:02:33+0800 [-] Starting factory <__main__.LogBotFactory object at 0x000002653455B488>
2021-04-24 18:07:32+0800 [-] connection failed: [Failure instance: Traceback (failure with no frames): <class 'twisted.internet.error.TCPTimedOutError'>: TCP connection timed out: 10060: 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。.
2021-04-24 18:07:32+0800 [-] ]
2021-04-24 18:07:32+0800 [-] Stopping factory <__main__.LogBotFactory object at 0x000002653455B488>
2021-04-24 18:07:32+0800 [-] Main loop terminated.
对应的log.log
:
[18:02:35] [connected at Sat Apr 24 18:02:35 2021]
[18:07:11] [disconnected at Sat Apr 24 18:07:11 2021]
Persistent Data in the Factory
由于Protocol实例在每次建立连接时重新创建,client需要更多方法对持久数据进行跟踪。就像logging bot,它需要知道哪个channel正在logging和log在哪里。
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
当Protocol创建,他获取factory引用给self.factory。然后就可以在它的逻辑中访问属性factory。在LogBot中,它打开文件和连接到channel存储在factory中。
factories有个默认实现buildProtocol。它和上面使用Protocol的factory属性去创建Protocol实例一样。在上面例子中,factory可以重写成这样:
class LogBotFactory(protocol.ClientFactory): protocol = LogBot def __init__(self, channel, filename): self.channel = channel self.filename = filename
Further Reading
The Protocol class used throughout this document is a base implementation of IProtocol used in most Twisted applications for convenience. To learn about the complete IProtocol
interface, see the API documentation for IProtocol .
The transport
attribute used in some examples in this document provides the ITCPTransport interface. To learn about the complete interface, see the API documentation for ITCPTransport .
Interface classes are a way of specifying what methods and attributes an object has and how they behave. See the Components: Interfaces and Adapters document for more information on using interfaces in Twisted.