Twisted Documentation: Writing Clients

# Writing Clients

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from twisted.internet import reactor
    from twisted.internet.protocol import Factory, Protocol
    from twisted.internet.endpoints import TCP4ClientEndpoint

    class Greeter(Protocol):
        def sendMessage(self, msg):
            self.transport.write("MESSAGE %s\n" % msg)

    class GreeterFactory(Factory):
        def buildProtocol(self, addr):
            return Greeter()

    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 = point.connect(GreeterFactory())
    d.addCallback(gotProtocol)
    reactor.run()

Regardless of the type of client endpoint, the way to set up a new connection is simply to call the `connect` method on it and pass in a factory. 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 before calling `connect`.

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.

Note: If you've used `ClientFactory` before, make sure you remember that the `connect` method takes a `Factory`, not a `ClientFactory`. Even if you pass a `ClientFactory` to `endpoint.connect`, its `clientConnectionFailed` and `clientConnectionLost` methods.

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

Source listing - ../../words/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`.

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

Index

Version: 11.1.0