438 lines
14 KiB
Python
Executable File
438 lines
14 KiB
Python
Executable File
"""Generic socket server classes.
|
|
|
|
This module tries to capture the various aspects of defining a server:
|
|
|
|
- address family:
|
|
- AF_INET: IP (Internet Protocol) sockets (default)
|
|
- AF_UNIX: Unix domain sockets
|
|
- others, e.g. AF_DECNET are conceivable (see <socket.h>
|
|
- socket type:
|
|
- SOCK_STREAM (reliable stream, e.g. TCP)
|
|
- SOCK_DGRAM (datagrams, e.g. UDP)
|
|
- client address verification before further looking at the request
|
|
(This is actually a hook for any processing that needs to look
|
|
at the request before anything else, e.g. logging)
|
|
- how to handle multiple requests:
|
|
- synchronous (one request is handled at a time)
|
|
- forking (each request is handled by a new process)
|
|
- threading (each request is handled by a new thread)
|
|
|
|
The classes in this module favor the server type that is simplest to
|
|
write: a synchronous TCP/IP server. This is bad class design, but
|
|
save some typing. (There's also the issue that a deep class hierarchy
|
|
slows down method lookups.)
|
|
|
|
There are four classes in an inheritance diagram that represent
|
|
synchronous servers of four types:
|
|
|
|
+-----------+ +------------------+
|
|
| TCPServer |------->| UnixStreamServer |
|
|
+-----------+ +------------------+
|
|
|
|
|
v
|
|
+-----------+ +--------------------+
|
|
| UDPServer |------->| UnixDatagramServer |
|
|
+-----------+ +--------------------+
|
|
|
|
Note that UnixDatagramServer derives from UDPServer, not from
|
|
UnixStreamServer -- the only difference between an IP and a Unix
|
|
stream server is the address family, which is simply repeated in both
|
|
unix server classes.
|
|
|
|
Forking and threading versions of each type of server can be created
|
|
using the ForkingServer and ThreadingServer mix-in classes. For
|
|
instance, a threading UDP server class is created as follows:
|
|
|
|
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
|
|
|
|
The Mix-in class must come first, since it overrides a method defined
|
|
in UDPServer!
|
|
|
|
To implement a service, you must derive a class from
|
|
BaseRequestHandler and redefine its handle() method. You can then run
|
|
various versions of the service by combining one of the server classes
|
|
with your request handler class.
|
|
|
|
The request handler class must be different for datagram or stream
|
|
services. This can be hidden by using the mix-in request handler
|
|
classes StreamRequestHandler or DatagramRequestHandler.
|
|
|
|
Of course, you still have to use your head!
|
|
|
|
For instance, it makes no sense to use a forking server if the service
|
|
contains state in memory that can be modified by requests (since the
|
|
modifications in the child process would never reach the initial state
|
|
kept in the parent process and passed to each child). In this case,
|
|
you can use a threading server, but you will probably have to use
|
|
locks to avoid two requests that come in nearly simultaneous to apply
|
|
conflicting changes to the server state.
|
|
|
|
On the other hand, if you are building e.g. an HTTP server, where all
|
|
data is stored externally (e.g. in the file system), a synchronous
|
|
class will essentially render the service "deaf" while one request is
|
|
being handled -- which may be for a very long time if a client is slow
|
|
to reqd all the data it has requested. Here a threading or forking
|
|
server is appropriate.
|
|
|
|
In some cases, it may be appropriate to process part of a request
|
|
synchronously, but to finish processing in a forked child depending on
|
|
the request data. This can be implemented by using a synchronous
|
|
server and doing an explicit fork in the request handler class's
|
|
handle() method.
|
|
|
|
Another approach to handling multiple simultaneous requests in an
|
|
environment that supports neither threads nor fork (or where these are
|
|
too expensive or inappropriate for the service) is to maintain an
|
|
explicit table of partially finished requests and to use select() to
|
|
decide which request to work on next (or whether to handle a new
|
|
incoming request). This is particularly important for stream services
|
|
where each client can potentially be connected for a long time (if
|
|
threads or subprocesses can't be used).
|
|
|
|
Future work:
|
|
- Standard classes for Sun RPC (which uses either UDP or TCP)
|
|
- Standard mix-in classes to implement various authentication
|
|
and encryption schemes
|
|
- Standard framework for select-based multiplexing
|
|
|
|
XXX Open problems:
|
|
- What to do with out-of-band data?
|
|
|
|
"""
|
|
|
|
|
|
__version__ = "0.2"
|
|
|
|
|
|
import socket
|
|
import sys
|
|
import os
|
|
|
|
|
|
class TCPServer:
|
|
|
|
"""Base class for various socket-based server classes.
|
|
|
|
Defaults to synchronous IP stream (i.e., TCP).
|
|
|
|
Methods for the caller:
|
|
|
|
- __init__(server_address, RequestHandlerClass)
|
|
- serve_forever()
|
|
- handle_request() # if you don't use serve_forever()
|
|
- fileno() -> int # for select()
|
|
|
|
Methods that may be overridden:
|
|
|
|
- server_bind()
|
|
- server_activate()
|
|
- get_request() -> request, client_address
|
|
- verify_request(request, client_address)
|
|
- process_request(request, client_address)
|
|
- handle_error()
|
|
|
|
Methods for derived classes:
|
|
|
|
- finish_request(request, client_address)
|
|
|
|
Class variables that may be overridden by derived classes or
|
|
instances:
|
|
|
|
- address_family
|
|
- socket_type
|
|
- request_queue_size (only for stream sockets)
|
|
- reuse_address
|
|
|
|
Instance variables:
|
|
|
|
- server_address
|
|
- RequestHandlerClass
|
|
- socket
|
|
|
|
"""
|
|
|
|
address_family = socket.AF_INET
|
|
|
|
socket_type = socket.SOCK_STREAM
|
|
|
|
request_queue_size = 5
|
|
|
|
allow_reuse_address = 0
|
|
|
|
def __init__(self, server_address, RequestHandlerClass):
|
|
"""Constructor. May be extended, do not override."""
|
|
self.server_address = server_address
|
|
self.RequestHandlerClass = RequestHandlerClass
|
|
self.socket = socket.socket(self.address_family,
|
|
self.socket_type)
|
|
self.server_bind()
|
|
self.server_activate()
|
|
|
|
def server_bind(self):
|
|
"""Called by constructor to bind the socket.
|
|
|
|
May be overridden.
|
|
|
|
"""
|
|
if self.allow_reuse_address:
|
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
self.socket.bind(self.server_address)
|
|
|
|
def server_activate(self):
|
|
"""Called by constructor to activate the server.
|
|
|
|
May be overridden.
|
|
|
|
"""
|
|
self.socket.listen(self.request_queue_size)
|
|
|
|
def fileno(self):
|
|
"""Return socket file number.
|
|
|
|
Interface required by select().
|
|
|
|
"""
|
|
return self.socket.fileno()
|
|
|
|
def serve_forever(self):
|
|
"""Handle one request at a time until doomsday."""
|
|
while 1:
|
|
self.handle_request()
|
|
|
|
# The distinction between handling, getting, processing and
|
|
# finishing a request is fairly arbitrary. Remember:
|
|
#
|
|
# - handle_request() is the top-level call. It calls
|
|
# get_request(), verify_request() and process_request()
|
|
# - get_request() is different for stream or datagram sockets
|
|
# - process_request() is the place that may fork a new process
|
|
# or create a new thread to finish the request
|
|
# - finish_request() instantiates the request handler class;
|
|
# this constructor will handle the request all by itself
|
|
|
|
def handle_request(self):
|
|
"""Handle one request, possibly blocking."""
|
|
try:
|
|
request, client_address = self.get_request()
|
|
except socket.error:
|
|
return
|
|
if self.verify_request(request, client_address):
|
|
try:
|
|
self.process_request(request, client_address)
|
|
except:
|
|
self.handle_error(request, client_address)
|
|
|
|
def get_request(self):
|
|
"""Get the request and client address from the socket.
|
|
|
|
May be overridden.
|
|
|
|
"""
|
|
return self.socket.accept()
|
|
|
|
def verify_request(self, request, client_address):
|
|
"""Verify the request. May be overridden.
|
|
|
|
Return true if we should proceed with this request.
|
|
|
|
"""
|
|
return 1
|
|
|
|
def process_request(self, request, client_address):
|
|
"""Call finish_request.
|
|
|
|
Overridden by ForkingMixIn and ThreadingMixIn.
|
|
|
|
"""
|
|
self.finish_request(request, client_address)
|
|
|
|
def finish_request(self, request, client_address):
|
|
"""Finish one request by instantiating RequestHandlerClass."""
|
|
self.RequestHandlerClass(request, client_address, self)
|
|
|
|
def handle_error(self, request, client_address):
|
|
"""Handle an error gracefully. May be overridden.
|
|
|
|
The default is to print a traceback and continue.
|
|
|
|
"""
|
|
print '-'*40
|
|
print 'Exception happened during processing of request from',
|
|
print client_address
|
|
import traceback
|
|
traceback.print_exc()
|
|
print '-'*40
|
|
|
|
|
|
class UDPServer(TCPServer):
|
|
|
|
"""UDP server class."""
|
|
|
|
socket_type = socket.SOCK_DGRAM
|
|
|
|
max_packet_size = 8192
|
|
|
|
def get_request(self):
|
|
data, client_addr = self.socket.recvfrom(self.max_packet_size)
|
|
return (data, self.socket), client_addr
|
|
|
|
def server_activate(self):
|
|
# No need to call listen() for UDP.
|
|
pass
|
|
|
|
|
|
class ForkingMixIn:
|
|
|
|
"""Mix-in class to handle each request in a new process."""
|
|
|
|
active_children = None
|
|
max_children = 40
|
|
|
|
def collect_children(self):
|
|
"""Internal routine to wait for died children."""
|
|
while self.active_children:
|
|
if len(self.active_children) < self.max_children:
|
|
options = os.WNOHANG
|
|
else:
|
|
# If the maximum number of children are already
|
|
# running, block while waiting for a child to exit
|
|
options = 0
|
|
try:
|
|
pid, status = os.waitpid(0, options)
|
|
except os.error:
|
|
pid = None
|
|
if not pid: break
|
|
self.active_children.remove(pid)
|
|
|
|
def process_request(self, request, client_address):
|
|
"""Fork a new subprocess to process the request."""
|
|
self.collect_children()
|
|
pid = os.fork()
|
|
if pid:
|
|
# Parent process
|
|
if self.active_children is None:
|
|
self.active_children = []
|
|
self.active_children.append(pid)
|
|
return
|
|
else:
|
|
# Child process.
|
|
# This must never return, hence os._exit()!
|
|
try:
|
|
self.socket.close()
|
|
self.finish_request(request, client_address)
|
|
os._exit(0)
|
|
except:
|
|
try:
|
|
self.handle_error(request,
|
|
client_address)
|
|
finally:
|
|
os._exit(1)
|
|
|
|
|
|
class ThreadingMixIn:
|
|
"""Mix-in class to handle each request in a new thread."""
|
|
|
|
def process_request(self, request, client_address):
|
|
"""Start a new thread to process the request."""
|
|
import threading
|
|
t = threading.Thread(target = self.finish_request,
|
|
args = (request, client_address))
|
|
t.start()
|
|
|
|
|
|
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
|
|
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
|
|
|
|
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
|
|
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
|
|
|
|
if hasattr(socket, 'AF_UNIX'):
|
|
|
|
class UnixStreamServer(TCPServer):
|
|
address_family = socket.AF_UNIX
|
|
|
|
class UnixDatagramServer(UDPServer):
|
|
address_family = socket.AF_UNIX
|
|
|
|
class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass
|
|
|
|
class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass
|
|
|
|
class BaseRequestHandler:
|
|
|
|
"""Base class for request handler classes.
|
|
|
|
This class is instantiated for each request to be handled. The
|
|
constructor sets the instance variables request, client_address
|
|
and server, and then calls the handle() method. To implement a
|
|
specific service, all you need to do is to derive a class which
|
|
defines a handle() method.
|
|
|
|
The handle() method can find the request as self.request, the
|
|
client address as self.client_address, and the server (in case it
|
|
needs access to per-server information) as self.server. Since a
|
|
separate instance is created for each request, the handle() method
|
|
can define arbitrary other instance variariables.
|
|
|
|
"""
|
|
|
|
def __init__(self, request, client_address, server):
|
|
self.request = request
|
|
self.client_address = client_address
|
|
self.server = server
|
|
try:
|
|
self.setup()
|
|
self.handle()
|
|
self.finish()
|
|
finally:
|
|
sys.exc_traceback = None # Help garbage collection
|
|
|
|
def setup(self):
|
|
pass
|
|
|
|
def __del__(self):
|
|
pass
|
|
|
|
def handle(self):
|
|
pass
|
|
|
|
def finish(self):
|
|
pass
|
|
|
|
|
|
# The following two classes make it possible to use the same service
|
|
# class for stream or datagram servers.
|
|
# Each class sets up these instance variables:
|
|
# - rfile: a file object from which receives the request is read
|
|
# - wfile: a file object to which the reply is written
|
|
# When the handle() method returns, wfile is flushed properly
|
|
|
|
|
|
class StreamRequestHandler(BaseRequestHandler):
|
|
|
|
"""Define self.rfile and self.wfile for stream sockets."""
|
|
|
|
def setup(self):
|
|
self.connection = self.request
|
|
self.rfile = self.connection.makefile('rb', 0)
|
|
self.wfile = self.connection.makefile('wb', 0)
|
|
|
|
def finish(self):
|
|
self.wfile.flush()
|
|
self.wfile.close()
|
|
self.rfile.close()
|
|
|
|
|
|
class DatagramRequestHandler(BaseRequestHandler):
|
|
|
|
"""Define self.rfile and self.wfile for datagram sockets."""
|
|
|
|
def setup(self):
|
|
import StringIO
|
|
self.packet, self.socket = self.request
|
|
self.rfile = StringIO.StringIO(self.packet)
|
|
self.wfile = StringIO.StringIO(self.packet)
|
|
|
|
def finish(self):
|
|
self.socket.sendto(self.wfile.getvalue(), self.client_address)
|