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.
At the base, the place where you actually implement the protocol parsing
and handling, is the Protocol
class. This class will usually be
descended
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
.
The persistent configuration is kept in a Factory
class, which usually inherits from twisted.internet.protocol.Factory
(or twisted.internet.protocol.ClientFactory
: see
below). The default factory class just instantiates the Protocol
and then sets the protocol's factory
attribute to point to
itself (the factory). This lets the Protocol
access, and
possibly modify, the persistent configuration.
Protocol
As mentioned above, this and auxiliary classes and functions are where most of the code is. A Twisted protocol handles data in an asynchronous manner. This means that the protocol never waits for an event, but rather responds to events as they arrive from the network.
Here is a simple example:
1 2 3 4 5 6
from twisted.internet.protocol import Protocol from sys import stdout class Echo(Protocol): def dataReceived(self, data): stdout.write(data)
This is one of the simplest protocols. It just writes whatever it reads
from the connection to standard output. There are many events it does not
respond to. Here is an example of a Protocol
responding to
another event:
1 2 3 4 5 6
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()
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
.
Simple, single-use clients
In many cases, the protocol only needs to connect to the server once,
and the code just wants to get a connected instance of the protocol. In
those cases twisted.internet.endpoints
provides
the appropriate API, and in particular connectProtocol
which takes a
protocol instance rather than a factory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
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()
Regardless of the type of client endpoint, the way to set up a new
connection is simply pass it to connectProtocol
along with a
protocol instance. This means it's easy to change the mechanism you're
using to connect, without changing the rest of your program. For example,
to run the greeter example over SSL, the only change required is to
instantiate an
SSL4ClientEndpoint
instead of a
TCP4ClientEndpoint
. To take advantage of this, functions and
methods which initiates a new connection should generally accept an
endpoint as an argument and let the caller construct it, rather than taking
arguments like 'host' and 'port' and constructing its own.
For more information on different ways you can make outgoing connections to different types of endpoints, as well as parsing strings into endpoints, see the documentation for the endpoints API.
You may come across code using ClientCreator
, an older API which is not as flexible as
the endpoint API. Rather than calling connect
on an endpoint,
such code will look like this:
1 2 3 4 5 6 7 8
from twisted.internet.protocol import ClientCreator ... creator = ClientCreator(reactor, Greeter) d = creator.connectTCP("localhost", 1234) d.addCallback(gotProtocol) reactor.run()
In general, the endpoint API should be preferred in new code, as it lets the caller select the method of connecting.
ClientFactory
Still, there's plenty of code out there that uses lower-level APIs, and a few features (such as automatic reconnection) have not been re-implemented with endpoints yet, so in some cases they may be more convenient to use.
To use the lower-level connection APIs, you will need to call one of the
reactor.connect* methods directly. For these cases, you need 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 in 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
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
To connect this EchoClientFactory
to a server, you could use
this code:
1 2 3
from twisted.internet import reactor reactor.connectTCP(host, port, EchoClientFactory()) 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.
Reconnection
Often, the connection of a client will be lost unintentionally due to
network problems. One way to reconnect after a disconnection would be to
call connector.connect()
when the connection is lost:
1 2 3 4 5
from twisted.internet.protocol import ClientFactory class EchoClientFactory(ClientFactory): def clientConnectionLost(self, connector, reason): 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.
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.
Here is the Echo
protocol implemented with
a ReconnectingClientFactory
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
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
The clients so far have been fairly simple. A more complicated example
comes with Twisted Words in the doc/words/examples
directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
# 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('%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)) # 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
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
.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
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
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.
Factories have a default implementation of buildProtocol
that does the same thing the example above does, using
the protocol
attribute of the factory to create the protocol
instance. In the example above, the factory could be rewritten to look
like this:
1 2 3 4 5 6
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.