Chapter 2. Building Basic Clients and Servers

The best way to learn about the components of a Twisted application is to dive right into some examples. This chapter will introduce you to the reactor event loop, transports, and protocols through implementations of a few basic TCP servers and clients.

A TCP Echo Server and Client

Skim the code for the TCP echo server and client pair in Examples 2-1 and 2-2. The server’s job is to listen for TCP connections on a particular port and echo back anything it receives. The client’s job is to connect to the server, send it a message, receive a response, and terminate the connection.

To test this pair of scripts, first run the server in one terminal with python echoserver.py. This will start a TCP server listening for connections on port 8000. Then run the client in a second terminal with python echoclient.py.

A transcript from the session looks like this:

    $ python echoserver.py # In Terminal 1
    $ python echoclient.py # In Terminal 2
    Server said: Hello, world!
    Connection lost.

Ta-da! You’ve just completed your first asynchronous, event-driven communication with Twisted. Let’s look at each of the components of these scripts in more detail.

The echo server and echo client are event-driven programs, and more generally Twisted is an event-driven networking engine. What does that mean?

In an event-driven program, program flow is determined by external events. It is characterized by an event loop and the use of callbacks to trigger actions when events happen. Contrast this structure with two other common models: single-threaded (synchronous) and multithreaded programming.

Figure 2-1 summarizes these three models visually by showing the work done by a program over time under each of them. The program has three tasks to complete, each of which blocks while waiting for I/O to finish. Time spent blocking on I/O is grayed out.

In the single-threaded synchronous version of the program, tasks are performed serially. If one task blocks on I/O, all of the other tasks must also wait. Single-threaded programs are thus easy to reason about but can be unnecessarily slow.

In the multithreaded version, the three blocking tasks are performed in separate threads of control, which may run interleaved on one or many processors. This allows progress to be made by some threads while others are blocking on resources and is often more time-efficient than the analogous synchronous program. However, one has to write code that protects shared resources that could be accessed concurrently from multiple threads, which when implemented improperly can lead to notoriously subtle and painful threading bugs.

The event-driven version of the program interleaves the execution of the three tasks, but in a single thread of control. When performing I/O or other expensive operations, a callback is registered with an event loop, and then execution continues while the I/O completes. The callback describes how to handle an event once it has completed. The event loop polls for events and dispatches them as they arrive to the callbacks that are waiting for them. This allows the program to make progress without the use of additional threads.

Event-driven programs enjoy both the parallelism of multithreaded programs and the ease of reasoning of single-threaded programs.

The core of Twisted is the reactor event loop. The reactor knows about network, filesystem, and timer events. It waits on and demultiplexes these events and dispatches them to waiting event handlers. Twisted takes care of abstracting away platform-specific behavior and using the underlying nonblocking APIs correctly. Twisted presents a common interface to the various event sources so that responding to events anywhere in the network stack is easy.

The reactor essentially accomplishes the following:

    while True:
        timeout = time_until_next_timed_event()
        events = wait_for_events(timeout)
        events += timed_events_until(now())
        for event in events:
            event.process()

In our echo server and client from Examples 2-1 and 2-2, the reactor’s listenTCP and connectTCP methods take care of registering callbacks with the reactor to get notified when data is available to read from a TCP socket on port 8000.

After those callbacks have been registered, we start the reactor’s event loop with reactor.run. Once running, the reactor will poll for and dispatch events forever or until reactor.stop is called.

A transport represents the connection between two endpoints communicating over a network. Transports describe connection details: for example, is this connection stream-oriented (like TCP) or datagram-oriented (like UDP)? TCP, UDP, Unix sockets, and serial ports are examples of transports. Transports implement the ITransport interface, which has the following methods:

In the echo server and client examples from earlier, the two endpoints send each other data using their transport’s write method. The client terminates the TCP connection after receiving a response from the server by calling loseConnection.

Protocols describe how to process network events asynchronously. Twisted maintains implementations for many popular application protocols, including HTTP, Telnet, DNS, and IMAP. Protocols implement the IProtocol interface, which has the following methods:

In our echo server, we create our own Echo protocol by subclassing protocol.Protocol. To echo data back to the client, we take the data received from the client and simply write it back out through the transport in dataReceived.

In the echo client, we create our own EchoClient protocol by subclassing protocol.Protocol. The call to connectTCP creates a TCP connection to the server on port 8000 and registers callbacks for the various stages of the connection. For example, a callback is registered to invoke dataReceived when new data is available on the transport. Once the connection is established, we write data out to the server through the transport in connectionMade. When we receive data back from the server in dataReceived, we print that data and close the TCP connection.

Let’s reiterate some of the core ideas discussed in the previous sections with a slightly more complicated quote exchange service.

The quote server in Example 2-3 is seeded with an initial quote. Upon receiving a quote from a client, it will send the client its current quote and store the client’s quote to share with the next client. It also keeps track of the number of concurrent client connections.

The client in Example 2-4 creates several TCP connections, each of which exchanges a quote with the server.

Start the server in one terminal with python quoteserver.py and then run the client in another terminal with python quoteclient.py. Transcripts from these sessions will look something like the following—note that because this communication is asynchronous, the order in which connections are made and terminated may vary between runs:

$ python quoteserver.py 
Number of active connections: 2
> Received: ``You snooze you lose''
>  Sending: ``An apple a day keeps the doctor away.''
Number of active connections: 2
> Received: ``The early bird gets the worm''
>  Sending: ``You snooze you lose''
Number of active connections: 3
> Received: ``Carpe diem''
>  Sending: ``The early bird gets the worm''
$ python quoteclient.py
Received quote: The early bird gets the worm
Received quote: You snooze you lose
connection lost: Connection was closed cleanly.
connection lost: Connection was closed cleanly.
Received quote: Carpe diem
connection lost: Connection was closed cleanly.

This quote server and client pair highlight some key points about client/server communication in Twisted:

  1. Persistent protocol state is kept in the factory.

    Because a new instance of a protocol class is created for each connection, protocols can’t contain persistent state; that information must instead be stored in a protocol factory. In the echo server, the number of current connections is stored in numConnections in QuoteFactory.

    It is common for a factory’s buildProtocol method to do nothing beyond return an instance of a Protocol. For that simple case, Twisted provides a shortcut: instead of implementing buildProtocol, just define a protocol class variable for the factory; the default implementation of buildProtocol will take care of creating an instance of your Protocol and setting a factory attribute on the protocol pointing back to the factory (making it easy for protocol instances to access the shared state stored in the factory).

    For example, you could get rid of QuoteProtocol’s __init__ method and QuoteFactory could be rewritten as:

    class QuoteFactory(Factory):
        numConnections = 0
        protocol = QuoteProtocol
    
        def __init__(self, quote=None):
            self.quote = quote or "An apple a day keeps the doctor away."

    This is a common idiom in Twisted programs, so keep an eye out for it!

  2. Protocols can retrieve the reason why a connection was terminated.

    The reason is passed as an argument to clientConnectionLost and clientConnectionFailed. If you run quoteclient.py without a server waiting for its connections, you’ll get:

    $ python quoteclient.py
    connection failed: Connection was refused by other side...
    connection failed: Connection was refused by other side...
    connection failed: Connection was refused by other side...
  3. Clients can make make many simultaneous connections to a server.

    To do this, simply call connectTCP repeatedly, as was done in the quote client before starting the reactor.

Lastly, our use of maybeStopReactor is hinting at a general client design issue of how to determine when all of the connections you wanted to make have terminated (often so that you can shut down the reactor). maybeStopReactor gets the job done here, but we’ll explore a more idiomatic way of accomplishing this using objects called Deferreds later in the next book.

Protocols typically have different states and can be expressed in client and server code as a state machine. Example 2-5 is a chat server that implements a small state machine. It also subclasses the LineReceiver class, which is a convenience class that makes it easy to write line-based protocols. When using LineReceiver, a client should send messages with sendLine and a server should process received messages in lineReceived.

Run the chat server with python chatserver.py. You can then connect to the chat server with the telnet utility. Example 2-6 shows a sample transcript of two users chatting.

ChatProtocol has two states, REGISTER and CHAT. lineReceived calls the correct handler based on the current state of the protocol.

Note that the persistent protocol state—the dictionary of connected users—is stored in ChatFactory.

As you can see, the servers and clients for the echo, quote, and chat services are all structurally very similar. The shared recipe is:

This chapter introduced the core components of Twisted servers and clients: the reactor, transports, protocols, and protocol factories. Because a new instance of a protocol class is created for each connection, persistent state is kept in a protocol factory. Protocols and transports are decoupled, which makes transport reuse and protocol testing easy.

The Twisted Core examples directory has many additional examples of basic servers and clients, including implementations for UDP and SSL.

The Twisted Core HOWTO index has an extended “Twisted from Scratch” tutorial that builds a finger service from scratch.

One real-world example of building a protocol in Twisted is AutobahnPython, a WebSockets implementation.

Twisted has been developing a new higher-level endpoints API for creating a connection between a client and server. The endpoints API wraps lower-level APIs like listenTCP and connectTCP, and provides greater flexibility because it decouples constructing a connection from initiating use of the connection, allowing parameterization of the endpoint. You’ll start seeing the endpoints API in more documentation and examples through the next couple of Twisted releases, so keep an eye out for it. You can read more about that at the Twisted endpoints API page.