A tremendously stimulating investigation of failure in biological and economic systems.
Part 11: Your Poetry is Served
A Twisted Poetry Server
Now that we’ve learned so much about writing clients with Twisted, let’s turn around and re-implement our poetry server with Twisted too. And thanks to the generality of Twisted’s abstractions, it turns out we’ve already learned almost everything we need to know. Take a look at our Twisted poetry server located in twisted-server-1/fastpoetry.py. It’s called fastpoetry because this server sends the poetry as fast as possible, without any delays at all. Note there’s significantly less code than in the client!
Let’s take the pieces of the server one at a time. First, the
class PoetryProtocol(Protocol): def connectionMade(self): self.transport.write(self.factory.poem) self.transport.loseConnection()
Like the client, the server uses a separate
Protocol instance to manage each different connection (in this case, connections that clients make to the server). Here the
Protocol is implementing the server-side portion of our poetry protocol. Since our wire protocol is strictly one-way, the server’s
Protocol instance only needs to be concerned with sending data. If you recall, our wire protocol requires the server to start sending the poem immediately after the connection is made, so we implement the
connectionMade method, a callback that is invoked after a
Protocol instance is connected to a
Our method tells the
Transport to do two things: send the entire text of the poem (
self.transport.write) and close the connection (
self.transport.loseConnection). Of course, both of those operations are asynchronous. So the call to
write() really means “eventually send all this data to the client” and the call to
loseConnection() really means “close this connection once all the data I’ve asked you to write has been written”.
As you can see, the
Protocol retrieves the text of the poem from the
Factory, so let’s look at that next:
class PoetryFactory(ServerFactory): protocol = PoetryProtocol def __init__(self, poem): self.poem = poem
Now that’s pretty darn simple. Our factory’s only real job, besides making
PoetryProtocol instances on demand, is storing the poem that each
PoetryProtocol sends to a client.
Notice that we are sub-classing
ServerFactory instead of
ClientFactory. Since our server is passively listening for connections instead of actively making them, we don’t need the extra methods
ClientFactory provides. How can we be sure of that? Because we are using the
listenTCP reactor method and the documentation for that method explains that the
factory argument should be an instance of
main function where we call
def main(): options, poetry_file = parse_args() poem = open(poetry_file).read() factory = PoetryFactory(poem) from twisted.internet import reactor port = reactor.listenTCP(options.port or 0, factory, interface=options.iface) print 'Serving %s on %s.' % (poetry_file, port.getHost()) reactor.run()
It basically does three things:
- Read the text of the poem we are going to serve.
- Create a
PoetryFactorywith that poem.
listenTCPto tell Twisted to listen for connections on a port, and use our factory to make the protocol instances for each new connection.
After that, the only thing left to do is tell the
reactor to start running the loop. You can use any of our previous poetry clients (or just netcat) to test out the server.
Recall Figure 8 and Figure 9 from Part 5. Those figures illustrated how a new
Protocol instance is created and initialized after Twisted makes a new connection on our behalf. It turns out the same mechanism is used when Twisted accepts a new incoming connection on a port we are listening on. That’s why both
One thing we didn’t show in Figure 9 is that the
connectionMade callback is also called as part of
Protocol initialization. This happens no matter what, but we didn’t need to use it in the client code. And the
Protocol methods that we did use in the client aren’t used in the server’s implementation. So if we wanted to, we could make a shared library with a single
PoetryProtocol that works for both clients and servers. That’s actually the way things are typically done in Twisted itself. For example, the
Protocol can both read and write netstrings from and to a
We skipped writing a low-level version of our server, but let’s think about what sort of things are going on under the hood. First, calling
listenTCP tells Twisted to create a listening socket and add it to the event loop. An “event” on a listening socket doesn’t mean there is data to read; instead it means there is a client waiting to connect to us.
Twisted will automatically accept incoming connection requests, thus creating a new client socket that links the server directly to an individual client. That client socket is also added to the event loop, and Twisted creates a new
Transport and (via the
PoetryFactory) a new
PoetryProtocol instance to service that specific client. So the
Protocol instances are always connected to client sockets, never to the listening socket.
We can visualize all of this in Figure 26:
In the figure there are three clients currently connected to the poetry server. Each
Transport represents a single client socket, and the listening socket makes a total of four file descriptors for the select loop to monitor. When a client is disconnected the associated
PoetryProtocol will be dereferenced and garbage-collected (assuming we haven’t stashed a reference to one of them somewhere, a practice we should avoid to prevent memory leaks). The
PoetryFactory, meanwhile, will stick around as long as we keep listening for new connections which, in our poetry server, is forever. Like the beauty of poetry. Or something. At any rate, Figure 26 certainly cuts a fine figure of a Figure, doesn’t it?
The client sockets and their associated Python objects won’t live very long if the poem we are serving is relatively short. But with a large poem and a really busy poetry server we could end up with hundreds or thousands of simultaneous clients. And that’s OK — Twisted has no built-in limits on the number of connections it can handle. Of course, as you increase the load on any server, at some point you will find it cannot keep up or some internal OS limit is reached. For highly-loaded servers, careful measurement and testing is the order of the day.
Twisted also imposes no limit on the number of ports we can listen on. In fact, a single Twisted process could listen on dozens of ports and provide a different service on each one (by using a different factory class for each
listenTCP call). And with careful design, whether you provide multiple services with a single Twisted process or several is a decision you could potentially even postpone to the deployment phase.
There’s a couple things our server is missing. First of all, it doesn’t generate any logs that might help us debug problems or analyze our network traffic. Furthermore, the server doesn’t run as a daemon, making it vulnerable to death by accidental Ctrl-C (or just logging out). We’ll fix both those problems in a future Part but first, in Part 12, we’ll write another server to perform poetry transformation.
- Write an asynchronous poetry server without using Twisted, like we did for the client in Part 2. Note that listening sockets need to be monitored for reading and a “readable” listening socket means we can
accepta new client socket.
- Write a low-level asynchronous poetry server using Twisted, but without using
listenTCPor protocols, transports, and factories, like we did for the client in Part 4. So you’ll still be making your own sockets, but you can use the Twisted reactor instead of your own
- Make the high-level version of the Twisted poetry server a “slow server” by using
LoopingCallto make multiple calls to
transport.write(). Add the --num-bytes and --delay command line options supported by the blocking server. Don’t forget to handle the case where the client disconnects before receiving the whole poem.
- Extend the high-level Twisted server so it can serve multiple poems (on different ports).
- What are some reasons to serve multiple services from the same Twisted process? What are some reasons not to?
A thorough, and thoroughly excellent, introduction to the programming language Haskell.