编写客户端(Writing Clients) -- JerryMarx [2004-08-03 03:33:44]

1. 概要(Overview)

Twisted is a framework designed to be very flexible, and let you write powerful clients. The cost of this flexibility is a few layers in the way to writing your client. This document covers creating clients that can be used for TCP, SSL and Unix sockets, UDP is covered in a different document.

Twisted被设计为一个非常灵活的框架,你可以用它写强大的客户端.这种灵活性的代价是编写客户端的时候需要分好几层来实现.本文档叙述如何写一个可用于TCP,SSL和Unix socket的客户端,关于UDP另有文档叙述.

At the base, the place where you actually implement the protocol parsing and handling, is the Protocol class. This class will usually be decended from twisted.internet.protocol.Protocol. Most protocol handlers inherit either from this class or from one of its convenience children. An instance of the protocol class will be instantiated when you connect to the server, and will go away when the connection is finished. This means that persistent configuration is not saved in the Protocol.

作为基础,协议类实现了协议解析和处理,通常继承自twisted.internet.protocol.Protocol.大部分协议处理类或者直接继承自这个类或者继承自这个类的某个合适的子类.协议类的的实例在连接到服务器的时候被实例化,在断开连接的时候被销毁.这就意味着不能使用协议类来做持续持有配置.

The persistent configuration is kept in a Factory class, which usually inherits from twisted.internet.protocol.ClientFactory. The default factory class just instantiate the Protocol, and then sets on it an attribute called factory which points to itself. This let the Protocol access, and possibly modify, the persistent configuration.

持续持有的地方应该在工厂类,它通常继承自twisted.internet.protocol.ClientFactory.缺省的工厂类的行为只是实例化Protocol类,并把Protocol类的factory属性设置为指向自己,这样协议类就可以访问甚至修改持续持有的配置.

2. 协议(Protocol)

As mentioned above, this, and auxiliary classes and functions, is where most of the code is. A Twisted protocol handles data in an asynchronous manner. What this means is that the protocol never waits for an event, but rather responds to events as they arrive from the network.

就像上面所说的,这就是基本框架.Twisted协议以异步方式处理数据.就是说它从来不等待一个事件,而是在事件从网络到来的时候响应事件.

Here is a simple example:

这里有一个例子:

   1 from twisted.internet.protocol import Protocol
   2 from sys import stdout
   3 class Echo(Protocol):
   4 
   5     def dataReceived(self, data):
   6         stdout.write(data)

This is one of the simplest protocols. It simply writes to standard output whatever it reads from the connection. There are many events it does not respond to. Here is an example of a Protocol responding to another event.

这是最简单的协议了,它只是把任何从网络收到的数据写到标准输出.有很多事件它并没有处理,下一个例子演示如何响应其它事件:

   1 from twisted.internet.protocol import Protocol
   2 class WelcomeMessage(Protocol):
   3 
   4     def connectionMade(self):
   5         self.transport.write("Hello server, I am the client!\r\n")
   6         self.transport.loseConnection()

This protocol connects to the server, sends it a welcome message, and then terminates the connection.

这个协议连接到服务器,发送一条欢迎信息,然后断开.

The connectionMade event is usually where set up of the Protocol object happens, as well as any initial greetings (as in the WelcomeMessage protocol above). Any tearing down of Protocol-specific objects is done in connectionLost.

事件connectionMade通常在协议对象被创建的时候触发,伴随着初始化问候(就像上面的WelcomMessage协议).在connectionLost事件中做销毁特定协议对象的处理.

3. 客户工厂(ClientFactory)

We use reactor.connect* and a ClientFactory. The ClientFactory is in charge of creating the Protocol, and also receives events relating to the connection state. This allows it to do things like reconnect on the event of a connection error. Here is an example of a simple ClientFactory that uses the Echo protocol (above) and also prints what state the connection is in.

我们使用reactor.connect*和一个ClientFactory.ClientFactory的职责是创建协议,它也会收到和连接状态相关的事件,这样就可以在连接出错的时候重新建立连接.这里有一个例子,它使用了上面的Echo协议,它也可以打印连接的状态.

   1 from twisted.internet.protocol import Protocol, ClientFactory
   2 from sys import stdout
   3 
   4 class Echo(Protocol):
   5 
   6     def dataReceived(self, data):
   7         stdout.write(data)
   8 
   9 class EchoClientFactory(ClientFactory):
  10 
  11     def startedConnecting(self, connector):
  12         print 'Started to connect.'
  13 
  14     def buildProtocol(self, addr):
  15         print 'Connected.'
  16         return Echo()
  17 
  18     def clientConnectionLost(self, connector, reason):
  19         print 'Lost connection.  Reason:', reason
  20 
  21     def clientConnectionFailed(self, connector, reason):
  22         print 'Connection failed. Reason:', reason

To connect this EchoClientFactory to a server, you could use this code:

可以使用一下代码连接到服务器

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

Note that clientConnectionFailed is called when a connection could not be established, and that clientConnectionLost is called when a connection was made and then disconnected.

注意当连接不能建立的时候clientConnectionFailed会被调用,而当连接建立之后断开则clientConnectionLost会被调用.

3.1. 再次连接(Reconnection)

Many times, the connection of a client will be lost unintentionally due to network errors. One way to reconnect after a disconnection would be to call connector.connect() when the connection is lost:

很多时候,客户端会因为网络错误而意外失去连接.再连接的一种方式是在失去连接后调用connector.connect().

   1 from twisted.internet.protocol import ClientFactory
   2 class EchoClientFactory(ClientFactory):
   3 
   4     def clientConnectionLost(self, connector, reason):
   5         connector.connect()

The connector passed as the first argument is the interface between a connection and a protocol. When the connection fails and the factory receives the clientConnectionLost event, the factory can call connector.connect() to start the connection over again from scratch.

connector是connection和protocol之间接口的第一个参数.当连接失败,工厂收到clientConnectionLost事件,就调用connector.connect()重新开始连接.

However, most programs that want this functionality should implement ReconnectingClientFactory instead, which tries to reconnect if a connection is lost or fails, and which exponentially delays repeated reconnect attempts.

而然,大部分的程序员向实现一种ReconnectingClientFactory机制,当连接断开或者失败的时候,它以指数函数关系延迟重复的连接意图.

Here is the Echo protocol implemented with a ReconnectingClientFactory:

下面这个例子使用ReconnectingClientFactory实现Echo协议:

   1 from twisted.internet.protocol import Protocol, ReconnectingClientFactory
   2 from sys import stdout
   3 
   4 class Echo(Protocol):
   5 
   6     def dataReceived(self, data):
   7         stdout.write(data)
   8 
   9 class EchoClientFactory(ReconnectingClientFactory):
  10 
  11     def startedConnecting(self, connector):
  12         print 'Started to connect.'
  13 
  14     def buildProtocol(self, addr):
  15         print 'Connected.'
  16         print 'Resetting reconnection delay'
  17         self.resetDelay()
  18         return Echo()
  19 
  20     def clientConnectionLost(self, connector, reason):
  21         print 'Lost connection.  Reason:', reason
  22         ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
  23 
  24     def clientConnectionFailed(self, connector, reason):
  25         print 'Connection failed. Reason:', reason
  26         ReconnectingClientFactory.clientConnectionFailed(self,
  27                 connector, reason)

4. 一个稍微复杂的例子:irc日志机器人(A Higher-Level Example: ircLogBot)

4.1. irc日志机器人概述(Overview of ircLogBot)

The clients so far have been fairly simple. A more complicated example comes with Twisted in the doc/examples directory.

客户端一直以来都使用简单清楚的例子,这次我们来看Twisted中doc/examples附带的一个稍微复杂的例子:

   1 """An example IRC log bot - logs a channel's events to a file.
   2 
   3 If someone says the bot's name in the channel followed by a ':',
   4 e.g.
   5 
   6   <foo> logbot: hello!
   7 
   8 the bot will reply:
   9 
  10   <logbot> foo: I am a log bot
  11 
  12 Run this script with two arguments, the channel name the bot should
  13 connect to, and file to log to, e.g.:
  14 
  15   $ python ircLogBot.py test test.log
  16 
  17 will log channel #test to the file 'test.log'.
  18 """
  19 
  20 
  21 # twisted imports
  22 from twisted.protocols import irc
  23 from twisted.internet import reactor, protocol
  24 from twisted.python import log
  25 
  26 # system imports
  27 import time, sys
  28 
  29 
  30 class MessageLogger:
  31     """
  32     An independant logger class (because separation of application
  33     and protocol logic is a good thing).
  34     """
  35     def __init__(self, file):
  36         self.file = file
  37 
  38     def log(self, message):
  39         """Write a message to the file."""
  40         timestamp = time.strftime("[%H:%M:%S]", time.localtime(time.time()))
  41         self.file.write('%s %s\n' % (timestamp, message))
  42         self.file.flush()
  43 
  44     def close(self):
  45         self.file.close()
  46 
  47 
  48 class LogBot(irc.IRCClient):
  49     """A logging IRC bot."""
  50 
  51     def __init__(self):
  52         self.nickname = "twistedbot"
  53 
  54     def connectionMade(self):
  55         irc.IRCClient.connectionMade(self)
  56         self.logger = MessageLogger(open(self.factory.filename, "a"))
  57         self.logger.log("[connected at %s]" %
  58                         time.asctime(time.localtime(time.time())))
  59 
  60     def connectionLost(self, reason):
  61         irc.IRCClient.connectionLost(self, reason)
  62         self.logger.log("[disconnected at %s]" %
  63                         time.asctime(time.localtime(time.time())))
  64         self.logger.close()
  65 
  66 
  67     # callbacks for events
  68 
  69     def signedOn(self):
  70         """Called when bot has succesfully signed on to server."""
  71         self.join(self.factory.channel)
  72 
  73     def joined(self, channel):
  74         """This will get called when the bot joins the channel."""
  75         self.logger.log("[I have joined %s]" % channel)
  76 
  77     def privmsg(self, user, channel, msg):
  78         """This will get called when the bot receives a message."""
  79         user = user.split('!', 1)[0]
  80         self.logger.log("<%s> %s" % (user, msg))
  81         if msg.startswith("%s:" % self.nickname):
  82             # someone is talking to me, lets respond:
  83             msg = "%s: I am a log bot" % user
  84             self.say(channel, msg)
  85             self.logger.log("<%s> %s" % (self.nickname, msg))
  86 
  87     def action(self, user, channel, msg):
  88         """This will get called when the bot sees someone do an action."""
  89         user = user.split('!', 1)[0]
  90         self.logger.log("* %s %s" % (user, msg))
  91 
  92     # irc callbacks
  93 
  94     def irc_NICK(self, prefix, params):
  95         """Called when an IRC user changes their nickname."""
  96         old_nick = prefix.split('!')[0]
  97         new_nick = params[0]
  98         self.logger.log("%s is now known as %s" % (old_nick, new_nick))
  99 
 100 
 101 class LogBotFactory(protocol.ClientFactory):
 102     """A factory for LogBots.
 103 
 104     A new protocol instance will be created each time we connect to the server.
 105     """
 106 
 107     # the class of the protocol to build when new connection is made
 108     protocol = LogBot
 109 
 110     def __init__(self, channel, filename):
 111         self.channel = channel
 112         self.filename = filename
 113 
 114     def clientConnectionLost(self, connector, reason):
 115         """If we get disconnected, reconnect to server."""
 116         connector.connect()
 117 
 118     def clientConnectionFailed(self, connector, reason):
 119         print "connection failed:", reason
 120         reactor.stop()
 121 
 122 
 123 if __name__ == '__main__':
 124     # initialize logging
 125     log.startLogging(sys.stdout)
 126 
 127     # create factory protocol and application
 128     f = LogBotFactory(sys.argv[1], sys.argv[2])
 129 
 130     # connect factory to this host and port
 131     reactor.connectTCP("irc.freenode.net", 6667, f)
 132 
 133     # run bot
 134     reactor.run()

Source listing - ../examples/ircLogBot.py ircLogBot.py connects to an IRC server, joins a channel, and logs all traffic on it to a file. It demonstrates some of the connection-level logic of reconnecting on a lost connection, as well as storing persistent data in the Factory.

ircLogBot.py 连接到IRC服务器,加入一个频道,将各种信息纪录到文件.它演示了连接-再次连接的逻辑,演示了工厂中的持续数据.

4.2. 在工厂中保存持续数据(Persistent Data in the Factory)

Since the Protocol instance is recreated each time the connection is made, the client needs some way to keep track of data that should be persisted. In the case of the logging bot, it needs to know which channel it is logging, and where to log it to.

由于Protocol实例会随着每个连接的建立而实例化,而客户端又需要保存一些持续数据.在上面这个例子里面,需要保存的数据就是机器人要加入哪个频道和要把日子写在什么地方.

   1 from twisted.internet import protocol
   2 from twisted.protocols import irc
   3 
   4 class LogBot(irc.IRCClient):
   5 
   6     def connectionMade(self):
   7         irc.IRCClient.connectionMade(self)
   8         self.logger = MessageLogger(open(self.factory.filename, "a"))
   9         self.logger.log("[connected at %s]" %
  10                         time.asctime(time.localtime(time.time())))
  11 
  12     def signedOn(self):
  13         self.join(self.factory.channel)
  14 
  15 
  16 class LogBotFactory(protocol.ClientFactory):
  17 
  18     protocol = LogBot
  19 
  20     def __init__(self, channel, filename):
  21         self.channel = channel
  22         self.filename = filename

When the protocol is created, it gets a reference to the factory as self.factory. It can then access attributes of the factory in its logic. In the case of LogBot, it opens the file and connects to the channel stored in the factory.

在protocol被创建的时候,它得到了对工厂的引用.这样它就可以访问工厂的属性.在上面这个例子里面,它打开的文件和要加入的频道就存储在工厂.

  • 翻译 -- Jerry Marx.

(目录)Index

Version: 1.3.0