原文:http://www.klipdas.com/blog/?p=twisted-clients
客户端
2.2.1 概述.
Twisted框架设计的很灵活,可以编写功能强大的客户端。灵活的代价在于编写客户端方法的多一些层次,文档涵盖了使用TCP,SSL和Unix sockets,UDP来创建客户端,它们分别都有介绍(92页)。
基本上,实际实现协议的解析和处理的是在Protocol类中。该类通常继承至twisted.internet.protocol.Protocol,大多数协议处理程序要么继承至该类,要么是其子类。当你连接到服务器时协议类就会实例化一个实体,在你离开的时候,被释放。就是说持久的配置不会驻留在Protocol中。
持久配置被保存在Factory类中,它通常继承至twisted.internet.protocol.ClientFactory。默认的工厂类只实例化Protocol,并赋值给它的factory属性,这就允许Protocol访问,有可能修改持久化配置。
2.2.2 协议
如上所述,协议、辅助类和函数是大部分代码。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事件中被清除。
2.2.3 简易单用户客户端
很多情况下,协议只需要连接服务器一次,程序只想获取一个协议连接的实例。这些情况下,twisted.internet.protocol.ClientCreator提供了必要的API。
from twisted.internet import reactor
from twisted.internet.protocol import Protocol, ClientCreator
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)
c = ClientCreator(reactor, Greeter)
c.connectTCP("localhost", 1234).addCallback(gotProtocol)
2.2.4 ClientFactory
我们使用reactor.connect*和ClientFactory,该ClientFactory负责创建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
reactor.connectTCP(host, port, EchoClientFactory())
reactor.run()
注意:clientConnectionFailed在不能建立连接时被调用,clientConnectionLost在创建连接后被释放时调用。
重新连接
很多时候,由于网络错误,客户端连接就会丢失意义。有一种方法,在连接中断时,disconnection会调用connector.connect()来重新进行连接:
from twisted.internet.protocol import ClientFactory
class EchoClientFactory(ClientFactory):
def clientConnectionLost(self, connector, reason):
connector.connect()
作为第一个参数传递的connector是connection和protocol的接口,当连接失败,factory收到clientConnectionLost事件时,factory可以调用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)
2.2.5 高级示例: ircLogBot
ircLogBot简介
到现在的客户端还相当简单,更复杂的示例在Twisted文档的doc/examples目录下面。
from twisted.words.protocols import irc
from twisted.internet import reactor, protocol
from twisted.python import log
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 succesfully 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))
class LogBotFactory(protocol.ClientFactory):
"""A factory for LogBots
A new protocol instance will be created each time we connect to the server.
"""
#the class of the protocol to build when new connection is mede
protocol = LogBot
def __init__(self, channel, filename):
self.channel = channel
self.filename = filename
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中的持久数据
由于Protocol实例在每次连接时都会重新创建,客户端需要一些方法来跟踪应该被持久化的数据。在这个logging bot案例中,它需要知道哪个频道被记录及记录在哪里。
from twisted.internet import protocol
from twisted.protocols import irc
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):
protocol = LogBot
def __init__(self, channel, filename):
self.channel = channel
self.filename = filename
当protocol被创建,它就会得到factory的引用赋值给self.factory,它可以通过自身的逻辑对factory的属性进行访问。在LogBot案例中,它打开文件,并连接factory中的通道存储。
2.2.6 延伸
本书中所使用的Protocol类是实现了IProtocol接口的基本类,该接口在大多少Twisted应用都广泛应用。要了解完整的IProtocol接口,请参阅API文档。
本书中在一些示例中使用的transport属性提供了ITCPTransport接口,要了解有关该接口的详细情况,参阅ITCPTransport的API文档。
接口是用于指定一个类所拥有的或如何使用的方法和属性的。参阅Components:Interfaces和Adapters(文档148页)。