Chapter 7 ■ Server arChiteCture
121
As usual with the server programs you wrote in Chapter 2 and Chapter 3, this server demands a single
command-line argument : the interface on which the server should listen for incoming connections. To protect the
server from other people on your LAN or network, specify the standard local host IP address.
$ python srv_single.py 127.0.0.1
Listening at ('127.0.0.1', 1060)
Or be more daring and offer the service on all your machine’s interfaces by specifying the empty string, which
Python interprets as meaning every interface on the current machine.
$ python srv_single.py ''
Listening at ('', 1060)
Either way, the server prints a line to announce that it opened its server port successfully and then waits for
incoming connections. The server also supports an -h help option and a -p option to choose a port other than 1060,
if you want to play with those. Once it is up and running, try executing the client script documented in the previous
section to see the server operate. As your clients connect and disconnect, you will see the server reporting client
activity in the terminal window where it is running.
Accepted connection from ('127.0.0.1', 40765)
Client socket to ('127.0.0.1', 1060) has closed
Accepted connection from ('127.0.0.1', 40768)
Client socket to ('127.0.0.1', 1060) has closed
If your network service has only a single client making a single connection at a time, then this design is all you
need. As soon as the previous connection closes, this server is ready for the next. For as long as a connection exists,
either this server sits blocked in a recv() call, waiting for the operating system to wake it back up when more data
arrives, or it is putting together an answer as quickly as it can and transmitting it without further delay. The only
circumstance in which send() or sendall() can block is when the client is not ready to receive data yet, in which case
the data will be sent—and the server unblocked to return to its recv()—as soon as the client is ready. In all situations,
therefore, responses are provided to the client as quickly as they can be computed and received.
The weakness of this single-threaded design is apparent the moment that a second client tries to connect while
the server is still in conversation with the first. If the integer argument to listen() was greater than zero, then the
operating system will at least be willing to acknowledge the second incoming client with a three-way TCP handshake
to set up a connection, which saves a bit of time when the server is finally ready to talk. But that connection will then
sit in the operating system’s listen queue until the server’s conversation with the first client is complete. Only once the
first client conversation is complete and the server code has looped back to its next call to accept() will the second
client’s connection be available to the server and its first request over that socket be able to be answered.
Performing a denial-of-service attack against this single-threaded server is trivial: connect and never close the
connection. The server will remain permanently blocked in recv() waiting for your data. If the server author gets
clever and tries setting a timeout with sock.settimeout() to avoid waiting forever, then adjust your denial-of-service
tool so that it sends a request often enough that the timeout is never reached. No other clients will ever be able to use
the server.