Sockets, threads, and killing servers
Robin Munn
rmunn at pobox.com
Wed Jan 9 15:37:00 EST 2002
I've been playing around with the SocketServer module recently. It
provides some useful classes for building various kinds of servers. But
there's one feature I wanted that didn't seem available. I wanted to be
able to kill the server externally at any time, without using kill -9 or
anything that relied on Unix signals, since I also needed to be able to
run this under Windows. After a little bit of pondering, I came up with
the following solution:
1) Subclass the ThreadingTCPServer class so that instead of calling
socket.accept() and blocking until a connection comes in, it will
call select.select() with a timeout; at the end of the timeout, it
will check a "Quit now?" flag and if the flag is set, will close the
connection and exit. If the quit flag is not set, go back into the
select.select() for another N seconds.
2) A "master control thread" launches an instance of the
ThreadingTCPServer subclass, then goes and blocks on some event
(which could be anything from just pressing Return on the console the
server was run from, as in the simplified example below, to receiving
a signal from the Windows NT service manager, to something else).
Once that event takes place, it sets the quit flag on the server
thread and then join()s the thread, which will exit as soon as the
next select.select() call returns.
3) If the Master Control Thread ever gets out of hand, a "Tron" thread
will activate and kill it. (Just kidding). :)
A fully-functional example is below. Any comments? I'd be especially
interested if you know a better way of doing what I was trying to do
(stay responsive to "quit now" commands from an external source without
completely re-inventing the wheel).
----- begin code -----
# Witty socket server
# Note: In subclassing ThreadingTCPServer, try overriding get_request()
# to select() on the single socket for a certain timeout period,
# then poll the quit flag at the end of that timeout before
# returning
# to the select(). That should provide a good balance between
# blocking
# to keep CPU usage down and speed of response to a shutdown
# request.
import os, sys
import socket
import SocketServer
import time
import threading
import select
class TimeToQuit(Exception):
pass
class ResponsiveThreadingTCPServer(SocketServer.ThreadingTCPServer):
def __init__(self, server_address, RequestHandlerClass, lock, timeout = 5.0):
SocketServer.ThreadingTCPServer.__init__(self, server_address, RequestHandlerClass)
self.timeout = timeout # Default timeout: 5.0 seconds
self.lock = lock # Should be a preexisting threading.RLock() object
self.lock.acquire()
self.QuitFlag = 0
self.lock.release()
def get_request(self):
socklist = [self.socket]
while 1:
# Select with a timeout, then poll a quit flag. An alternate
# approach would be to let the master thread "wake us up"
# with
# a socket connection.
ready = select.select(socklist, [], [], self.timeout)
self.lock.acquire()
time_to_quit = self.QuitFlag
self.lock.release()
if time_to_quit:
raise TimeToQuit # Get out now
if ready[0]: # A socket was ready to read
return SocketServer.ThreadingTCPServer.get_request(self)
else: # We timed out, no connection yet
pass # Just go back to the select()
def serve_forever(self):
try:
SocketServer.ThreadingTCPServer.serve_forever(self)
except TimeToQuit:
self.server_close() # Clean up before we leave
class CommandHandler(SocketServer.StreamRequestHandler):
def handle(self):
self.requestline = self.rfile.readline()
while self.requestline[-1] in ['\r', '\n']:
self.requestline = self.requestline[:-1] # Strip trailing CR/LF if any
command = 'do_' + self.requestline.upper()
if not hasattr(self, command):
self.send("Unknown command:")
self.send(self.requestline)
return
method = getattr(self, command)
method()
def send(self, text):
self.wfile.write(text)
self.wfile.flush()
def do_SONG(self):
self.send('Old McDonald had a farm, E-I-E-I-O!\r\n')
def do_QUOTE(self):
self.send('And now for something completely different...\r\n')
def do_POEM(self):
self.do_HAIKU()
def do_HAIKU(self):
self.send('A haiku on "Error 404"\r\n')
self.send('\r\n')
self.send(' You step in the stream,\r\n')
self.send(' But the water has moved on.\r\n')
self.send(' This file is not here.\r\n')
class MasterControlThread(threading.Thread):
def __init__(self, port=31310):
threading.Thread.__init__(self)
self.port = port
self.lock = threading.RLock()
def run(self):
print "Serving on port", self.port
self.server = ResponsiveThreadingTCPServer(('', self.port), CommandHandler, self.lock)
# Note: Five seconds timeout instead of a minute, for testing.
self.thread = threading.Thread(target = self.server.serve_forever)
self.thread.start()
print "Press Enter to quit."
raw_input()
# Tell the server it's time to shut down
self.server.lock.acquire()
self.server.QuitFlag = 1
self.server.lock.release()
print "Waiting for server to shut down (could take several seconds)..."
self.thread.join()
print "Exiting now."
if __name__ == '__main__':
mct = MasterControlThread()
mct.start()
mct.join()
--
Robin Munn
rmunn at pobox.com
More information about the Python-list
mailing list