Python Socket Server
Jump to navigation
Jump to search
Socket Server
#!/usr/bin/python
#
# Copyright (c) 1998, 1999, Sean Reifschneider, tummy.com, ltd.
# All Rights Reserved
#
# High-level classes for implementing command/response clients and
# servers in Python. Though the server is much more highly developed
# than the client at this point.
#
# http://www.tummy.com/sockserv/
# ftp://ftp.tummy.com/pub/tummy/sockserv/
######################################################################
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.0 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is sockserv, released September 20, 1998.
# The Initial Developer of the Original Code is tummy.com, ltd.
# Portions created by tummy.com, ltd. are Copyright (C) 1998, 1999
# tummy.com, ltd. All Rights Reserved.
#
# Contributor(s): ______________________________________.
#
######################################################################
revision = "$Revision: 1.5 $"
rcsid = "$Id: sockserv.py,v 1.5 2002/10/08 16:30:41 jafo Exp $"
'''Base-class for server side of client/server programs.
This class manages the server side of a client/server program. It uses a
very simple command-oriented protocol. The server manages receiving
connections from clients and processing the commands they may send.
With the exception of a couple of stubs, all of the dealing with
these events is done by user-supplied routines (typically provided
via inheritance).
Commands are dispatched to methods named "do_COMMAND", where "COMMAND" is
the command name (and is always converted to upper-case). These methods
should normally return 0. Returning 1 indicates that the client socket
should be shut down (useful in a "do_QUIT" method).
CLIENTS:
Clients (as passed to the do_* and other routines) aren't pure socket
objects. In order to support some additional functionality, they have
been wrapped by a thin wrapper object called "connection". Therefore.
these objects have a few additional attributes to make setting
client-specific data easier.
The connection types support 'addGroup' and 'delGroup' methods for
adding and removing a client from a group. Clients may belong to
multiple groups. A list of clients in a given group can be found
by using the sockserv.getGroupSockList() method, and data may be
sent to clients in a group using sockserv.sendGroup().
User-data may be associated with a client by setting it's
"user_data" attribute (which defaults to "None". This is data
that's associated with an individual socket (for example, the
socket object of another connection which we are proxying for).
Command processing can be overridden by specifying a "processor"
function for a client. A processor function takes a single
argument (data ready to be processed), and returns the remaining
(unprocessed) data, or '' if all was processed
To set the processor, call "connection.processor()", passing
the function to be invoked. To clear this (and return to the
normal "do_*" processing, call "connection.processor()" with
no arguments.
This "processor" function is meant for implementing data phases:
helpinfo_DATA = ( '200- DATA', 'Enter data phase, terminated with ".".' )
def do_DATA(self, client, cmd, args):
client.processor(readUntilDot)
def readUntilDot(client, data)
# process data looking for '.' to terminate data.
if foundDot: client.processor()
return(bytesProcessed)
ATTRIBUTES:
- newLine -- String containing line termination suitable for socket
communication.
- portNum -- Port number server is running on, or "None" if not running.
Simple socket server example:
import sockserv
class server(sockserv.sockserv):
def clientClosed(self, client):
print 'Client %d closed' % client.fileno()
helpinfo_QUIT = ( '200- QUIT', 'Terminate the connection.' )
def do_QUIT(self, client, cmd, args):
client.send('Bye now!', nl = 1)
return(1)
helpinfo_SHUTDOWN = ( '200- SHUTDOWN', 'Terminate the server.' )
def do_SHUTDOWN(self, client, cmd, args):
client.send('Server terminating', nl = 1)
self.shutdownServer = 1
return(1)
helpinfo_HELP = ( '200- HELP', 'Display this message.' )
def do_HELP(self, client, cmd, args):
client.send('200-Help information', nl = 1)
client.send('200-', nl = 1)
sockserv.sockserv.do_HELP(self, client, cmd, args)
client.send('200-', nl = 1)
client.send('200 End of help information.', nl = 1)
s = server(( 5000, 5001, 5002, 5003))
print 'Socket opened on %d' % s.portNum
s.mainloop()
'''
revision = '$Revision: 1.5 $'
import socket
import string
import os
newLine = '\r\n' # socket line termination
######################
def extractLine(data):
'''Return next line from "data", and the rest of data.
Extract the next line from "data" and return a tuple containing it and
the remainder of the data.
RETURNS: Tuple containing: next line, remainder of data. Next line will
be None if there is no line terminator.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- data -- string of bytes to look for line in.
'''
pos = string.find(data, '\r')
pos2 = string.find(data, '\n')
if pos2 >= 0 and (pos2 < pos or pos < 0): pos = pos2
if pos < 0: return(( None, data ))
if data[pos] == '\r' and data[pos + 1] == '\n':
return(data[:pos], data[pos + 2:])
return(data[:pos], data[pos + 1:])
class sockserv:
helpinfo = {}
#######################################################################
def __init__(self, port, host = '', name = 'GENERIC', portFile = None):
'''Create an instance of a server.
This initializes the server, and calls an "__localinit__" method if
it exists.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- portList -- a single numeric port, or a list of ports to try to start
the server on.
- host -- Interface to bind server on. Defaults to '', meaning server
should bind on all interfaces.
- name -- String representing server name, used in initial connect
message.
- portFile -- File name that port of server is written to. Meant for
allowing clients to locate server when a list of ports are given.
Defaults to None which indicates no file. File is deleted
when server is closed.
'''
self.serverName = name
self.host = host
try:
self.portList = port[0]
self.portList = port
except:
self.portList = ( port, )
self.portFile = portFile
self.portNum = None
self.clientList = []
self.clientData = {}
if hasattr(self, '__localinit__'):
getattr(self, '__localinit__')()
self.open()
###################
def mainloop(self):
'''Loop forever processing data on sockets.
If the server has a "shutdownServer" attribute, the loop will stop
and this function will return.
RETURNS: None
EXCEPTIONS: None generated internally.
'''
import select
while not hasattr(self, 'shutdownServer'):
rfdl = []
self.rfdlAdd(rfdl)
rfdl, wfdl, efdl = select.select(rfdl, [], [], None)
self.process(rfdl)
###############
def open(self):
'''FOR INTERNAL USE ONLY
Opens the socket, creating portFile if necessary, etc....
RETURNS: Port that the server was started on
EXCEPTIONS: Raises an exception from socket.bind() on failure.
'''
self.close()
self.portNum = None
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
failWith = None
for port in self.portList:
try:
self.server.bind(( self.host, port ))
self.portNum = port
failWith = None
break
except Exception, e:
failWith = e
if self.portFile != None:
file = open(self.portFile, 'w')
file.write('%d\n' % self.portNum)
file.close()
if self.portNum != None:
self.server.listen(10)
if failWith != None:
raise failWith
return(self.portNum)
################
def close(self):
'''FOR INTERNAL USE ONLY
Closes the socket (only if open). Sets internal attributes to
sane values.
RETURNS: Port that the server was started on
EXCEPTIONS: None generated internally.
'''
if self.portFile != None:
os.unlink(self.portFile)
try: self.server.close()
except: pass
self.server = None
for client in self.clientList:
try: client.close()
except: pass
self.clientList = []
self.clientData = {}
self.portNum = None
########################
def rfdlAdd(self, list):
'''Add internal sockets to select() list.
Add to "list", the sockets that we want listened on during a select().
RETURNS: Reference to list passed in.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- list -- (MODIFIED) Mutable list type which sockets should be
appended to.
'''
try: list.append(self.server.fileno())
except: pass
for client in self.clientList:
fn = client.fileno()
if fn < 0: self.clientShutdown(client)
else: list.append(fn)
return(list)
########################
def process(self, rfdl):
'''Iterate over "rfdl", processing our sockets if listed.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- rfdl -- List of sockets as returned by select(). Data (or
connections in the case of the server socket) is processed,
commands are decoded and dispatched.
'''
try: foo = rfdl[0]
except: rfdl = ( rfdl )
for sock in rfdl:
# new connections to server
if self.server != None and sock == self.server.fileno():
conn, addr = self.server.accept()
conn = connection(conn)
self.clientList.append(conn)
self.clientReady(conn, addr)
# data from a client
for client in self.clientList:
fileno = client.fileno()
if sock == fileno:
try: data = client.recv(1024)
except: data = ''
if len(data) < 1:
self.clientShutdown(client)
continue
if self.clientData.has_key(fileno):
data = self.clientData.get(fileno, '') + data
while 1:
if client.dataProcessor:
# If processor returns -1, it consumed all data.
# Otherwise, it returns number of bytes processed.
data = client.dataProcessor(client, data)
break
line, data = extractLine(data)
if line == None: break
line = string.strip(line)
if len(line) > 0:
cmd = line
args = ''
try: cmd, args = string.split(line, None, 1)
except: pass
cmd = string.upper(string.strip(cmd))
args = string.strip(args)
mname = 'do_' + cmd
if hasattr(self, mname):
method = getattr(self, mname)
elif hasattr(self, 'do_unknownCommand'):
method = getattr(self, 'do_unknownCommand')
else:
client.send('500 Unknown command "%s"%s'
% ( cmd, newLine ))
continue
if method(client, cmd, args):
self.clientShutdown(client)
break
if data: self.clientData[fileno] = data
else:
try: del(self.clientData[fileno])
except KeyError: pass
#################################
def clientShutdown(self, client):
'''Close specified client connection.
Used to remove a socket from the list of clients and do other misc
clean-up.
RETURNS: Nothing
EXCEPTIONS: None generated internally.
ARGUMENTS:
- client -- "socket" to be removed.
'''
self.clientClosed(client)
try: client.close()
except IOError: pass
try:
self.clientList.remove(client)
del(self.clientData[client.fileno()])
client.close()
except:
pass
###############################
def clientClosed(self, client):
'''STUB: Called when socket is being closed.
All attempts are made to call this routine before the socket is
actually closed. However, data sent on the "client" socket may
cause an exception. "client.fileno()" can be used as a connection
identifier, but in certain cases this may be -1.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- client -- "socket" which is being closed.
'''
pass
##################################
def clientReady(self, sock, addr):
'''STUB: Called when new connection is opened.
This routine is called right after a connection is opened, displaying
a default connection message.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- sock -- "socket" that client is on.
- addr -- Address as returned by "socket.accept()". Typically, a
2-element list containing host and address of client side.
'''
sock.send('200 %s server ready, connection from %s:%d...%s' %
( self.serverName, addr[0], addr[1], newLine ))
####################################
def getGroupSockList(self, groupId):
'''Returns a list of all sockets in specified group.
Returns a list of all sockets in specified group.
RETURNS: List of socket objects in group "groupId".
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- ID of group to send message to.
'''
list = []
for client in self.clientList:
try:
if client.isinGroup(groupId): list.append(client)
except: pass
return(list)
########################################################
def sendGroup(self, groupId, string, appendNewLine = 0):
'''Sends a string to all clients in a group.
Sends a string to all clients in the specified group, with optional
terminating newLine.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- ID of group to send message to.
- string -- String to send to clients.
- appendNewLine -- write a newLine after string?
'''
for client in self.getGroupSockList(groupId):
try:
client.send(string)
if appendNewLine: client.send(newLine)
except socket.error: pass
#####################################
def do_HELP(self, client, cmd, args):
'''Prints help message based on "helpinfo_*" data elements.
Sends a list of command information based on "helpinfo_*" member
elements to client. Headers and footers are to be provided by
subclasses.
RETURNS: None
Exceptions: None generated internally.
ARGUMENTS:
- client -- Socket which client is on.
- cmd -- Command entered by user.
- args -- Arguments.
'''
list = {}
maxLen = 0
for key in self.__class__.__dict__.keys():
if key[:9] == 'helpinfo_':
element = self.__class__.__dict__[key]
list[element[0]] = element[1]
thisLen = len(element[0])
if thisLen > maxLen: maxLen = thisLen
keys = list.keys()
keys.sort()
if len(keys) > 0:
for key in keys:
client.send(('%-*s %s' % ( maxLen, key, list[key] )) + newLine)
else:
client.send('No help available...' + newLine)
class connection:
#########################
def __init__(self, sock):
self.sock = sock
self.user_data = None
self.groupIdList = {}
self.dataProcessor = None
############################
def __getattr__(self, name):
'''FOR INTERNAL USE ONLY
Unknown attributes are pulled from socket "object".
RETURNS: Named attribute pulled from "self.socket".
EXCEPTIONS: None generated internally.
ARGUMENTS:
- name -- Attribute to look up in "self.socket".
'''
return(getattr(self.sock, name))
########################################
def send(self, data, flags = 0, nl = 0):
'''Send data over the socket.
This is the same as the socket.send function, except that it adds the
"nl" argument. If "nl" is set, a newline is appended to the string
being sent.
RETURNS: Number of bytes sent (same as socket object send() method).
EXCEPTIONS: None generated internally.
ARGUMENTS:
- data -- Data to be sent over socket.
- flags -- (optional, defaults to 0) Same as argument to socket
object send method. "See Unix manual page recv(2) for the
meaning of the optional argument flags."
- nl -- (optional, defaults to 0) If set, sockserv.newLine is
appended to "data".
'''
if nl: return(self.sock.send(data + newLine, flags))
return(self.sock.send(data, flags))
#################################
def processor(self, func = None):
'''Set up function to process all incoming data.
Specify function to be called which handles all incoming data.
Default handler processes data into lines and calls do_* methods
of sockserv class. This overrides that processing (used typicly
for data-phase processing).
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- func -- Function to be called, passing data to process as argument.
If not present, processor function is cleared, returning to
default do_* command processing.
'''
self.dataProcessor = func
############################
def addGroup(self, groupId):
'''Add the client to a given group.
Allows grouping of sockets for different tasks.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- Group ID to place client in.
'''
self.groupIdList[groupId] = 1
###################################
def delGroup(self, groupId = None):
'''Add the client to a given group.
Allows grouping of sockets for different tasks.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- Group ID to place client in. If not specified, removes
client from ALL groups.
'''
if groupId: del(self.groupIdList[groupId])
else: self.groupIdList = {}
#############################
def isinGroup(self, groupId):
'''Is this client in the specified group?
RETURNS: 1 if client is in specified group, 0 otherwise.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- Group ID to check client for membership.
'''
return(self.groupIdList.has_key(groupId))
##########################################
# client which connects to socket servers
class sockcli:
############################################################
def __init__(self, port, host = 'localhost', reconnect = 0):
'''Creates an instance of a client.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- port -- Port server is running on.
- host -- Host server is running on.
- reconnect -- If 1, client will try re-connecting if the connection
is broken.
'''
self.host = host
self.port = port
self.reconnect = reconnect
self.sock = None
self.open()
self.data = ''
self.setCallback()
###############
def open(self):
'''Open socket if it's not already open.
RETURNS: None
EXCEPTIONS: None generated internally.
'''
if self.sock == None:
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(self.host, self.port)
except Exception, e:
self.sock = None
####################
def getSocket(self):
'''Returns a list of sockects that should be selected on.
RETURNS: A list of sockects that should be selected on.
EXCEPTIONS: None generated internally.
'''
if self.sock == None and self.reconnect: self.open()
if self.sock == None: return([ ])
return([ self.sock ])
#######################################
def setCallback(self, callback = None):
'''Sets function to be called when data arrives.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- callback -- Specifies routine to call when data arrives on the
socket. If ommited, the callback is removed.
'''
self.callback = callback
##################
def process(self):
'''Process data on the socket.
Processes any data on socket, dispatching lines to the callback when
appropriate. Returns 1 while socket is still available.
RETURNS: 1 if socket is still open, or 0 if connection terminated.
EXCEPTIONS: None generated internally.
'''
if self.sock == None and self.reconnect: self.open()
if self.sock == None:
if self.reconnect: return(1)
return(0)
data = self.sock.recv(1024);
if len(data) < 1:
self.sock = None
return self.reconnect
self.data = self.data + data
# locate lines in input
while 1:
line, data = extractLine(data)
if line == None: break
line = string.strip(line)
if len(line) > 0:
if self.callback:
self.callback(self.sock, line)
return(1)
#####################################
# test code
#####################################
if __name__ == '__main__':
class server(sockserv):
def clientClosed(self, client):
print 'Client %d closed' % client.fileno()
helpinfo_QUIT = ( '200- QUIT', 'Terminate the connection.' )
def do_QUIT(self, client, cmd, args):
client.send('Bye now!', nl = 1)
return(1)
helpinfo_SHUTDOWN = ( '200- SHUTDOWN', 'Terminate the server.' )
def do_SHUTDOWN(self, client, cmd, args):
client.send('Server terminating', nl = 1)
self.shutdownServer = 1
return(1)
helpinfo_HELP = ( '200- HELP', 'Display this message.' )
def do_HELP(self, client, cmd, args):
client.send('200-Help information', nl = 1)
client.send('200-', nl = 1)
sockserv.do_HELP(self, client, cmd, args)
client.send('200-', nl = 1)
client.send('200 End of help information.', nl = 1)
s = server(( 5000, 5001, 5002, 5003))
print 'Socket opened on %d' % s.portNum
s.mainloop()
Variant
#!/usr/bin/python
#
# Copyright (c) 1998, 1999, Sean Reifschneider, tummy.com, ltd.
# All Rights Reserved
#
# High-level classes for implementing command/response clients and
# servers in Python. Though the server is much more highly developed
# than the client at this point.
#
# http://www.tummy.com/sockserv/
# ftp://ftp.tummy.com/pub/tummy/sockserv/
######################################################################
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.0 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is sockserv, released September 20, 1998.
# The Initial Developer of the Original Code is tummy.com, ltd.
# Portions created by tummy.com, ltd. are Copyright (C) 1998, 1999
# tummy.com, ltd. All Rights Reserved.
#
# Contributor(s): ______________________________________.
#
######################################################################
revision = "$Revision: 1.5 $"
rcsid = "$Id: sockserv.py,v 1.5 2002/10/08 16:30:41 jafo Exp $"
'''Base-class for server side of client/server programs.
This class manages the server side of a client/server program. It uses a
very simple command-oriented protocol. The server manages receiving
connections from clients and processing the commands they may send.
With the exception of a couple of stubs, all of the dealing with
these events is done by user-supplied routines (typically provided
via inheritance).
Commands are dispatched to methods named "do_COMMAND", where "COMMAND" is
the command name (and is always converted to upper-case). These methods
should normally return 0. Returning 1 indicates that the client socket
should be shut down (useful in a "do_QUIT" method).
CLIENTS:
Clients (as passed to the do_* and other routines) aren't pure socket
objects. In order to support some additional functionality, they have
been wrapped by a thin wrapper object called "connection". Therefore.
these objects have a few additional attributes to make setting
client-specific data easier.
The connection types support 'addGroup' and 'delGroup' methods for
adding and removing a client from a group. Clients may belong to
multiple groups. A list of clients in a given group can be found
by using the sockserv.getGroupSockList() method, and data may be
sent to clients in a group using sockserv.sendGroup().
User-data may be associated with a client by setting it's
"user_data" attribute (which defaults to "None". This is data
that's associated with an individual socket (for example, the
socket object of another connection which we are proxying for).
Command processing can be overridden by specifying a "processor"
function for a client. A processor function takes a single
argument (data ready to be processed), and returns the remaining
(unprocessed) data, or '' if all was processed
To set the processor, call "connection.processor()", passing
the function to be invoked. To clear this (and return to the
normal "do_*" processing, call "connection.processor()" with
no arguments.
This "processor" function is meant for implementing data phases:
helpinfo_DATA = ( '200- DATA', 'Enter data phase, terminated with ".".' )
def do_DATA(self, client, cmd, args):
client.processor(readUntilDot)
def readUntilDot(client, data)
# process data looking for '.' to terminate data.
if foundDot: client.processor()
return(bytesProcessed)
ATTRIBUTES:
- newLine -- String containing line termination suitable for socket
communication.
- portNum -- Port number server is running on, or "None" if not running.
Simple socket server example:
import sockserv
class server(sockserv.sockserv):
def clientClosed(self, client):
print 'Client %d closed' % client.fileno()
helpinfo_QUIT = ( '200- QUIT', 'Terminate the connection.' )
def do_QUIT(self, client, cmd, args):
client.send('Bye now!', nl = 1)
return(1)
helpinfo_SHUTDOWN = ( '200- SHUTDOWN', 'Terminate the server.' )
def do_SHUTDOWN(self, client, cmd, args):
client.send('Server terminating', nl = 1)
self.shutdownServer = 1
return(1)
helpinfo_HELP = ( '200- HELP', 'Display this message.' )
def do_HELP(self, client, cmd, args):
client.send('200-Help information', nl = 1)
client.send('200-', nl = 1)
sockserv.sockserv.do_HELP(self, client, cmd, args)
client.send('200-', nl = 1)
client.send('200 End of help information.', nl = 1)
s = server(( 5000, 5001, 5002, 5003))
print 'Socket opened on %d' % s.portNum
s.mainloop()
'''
revision = '$Revision: 1.5 $'
import socket
import string
import os
newLine = '\r\n' # socket line termination
######################
def extractLine(data):
'''Return next line from "data", and the rest of data.
Extract the next line from "data" and return a tuple containing it and
the remainder of the data.
RETURNS: Tuple containing: next line, remainder of data. Next line will
be None if there is no line terminator.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- data -- string of bytes to look for line in.
'''
pos = string.find(data, '\r')
pos2 = string.find(data, '\n')
if pos2 >= 0 and (pos2 < pos or pos < 0): pos = pos2
if pos < 0: return(( None, data ))
if data[pos] == '\r' and data[pos + 1] == '\n':
return(data[:pos], data[pos + 2:])
return(data[:pos], data[pos + 1:])
class sockserv:
helpinfo = {}
#######################################################################
def __init__(self, port, host = '', name = 'GENERIC', portFile = None):
'''Create an instance of a server.
This initializes the server, and calls an "__localinit__" method if
it exists.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- portList -- a single numeric port, or a list of ports to try to start
the server on.
- host -- Interface to bind server on. Defaults to '', meaning server
should bind on all interfaces.
- name -- String representing server name, used in initial connect
message.
- portFile -- File name that port of server is written to. Meant for
allowing clients to locate server when a list of ports are given.
Defaults to None which indicates no file. File is deleted
when server is closed.
'''
self.serverName = name
self.host = host
try:
self.portList = port[0]
self.portList = port
except:
self.portList = ( port, )
self.portFile = portFile
self.portNum = None
self.clientList = []
self.clientData = {}
if hasattr(self, '__localinit__'):
getattr(self, '__localinit__')()
self.open()
###################
def mainloop(self):
'''Loop forever processing data on sockets.
If the server has a "shutdownServer" attribute, the loop will stop
and this function will return.
RETURNS: None
EXCEPTIONS: None generated internally.
'''
import select
while not hasattr(self, 'shutdownServer'):
rfdl = []
self.rfdlAdd(rfdl)
rfdl, wfdl, efdl = select.select(rfdl, [], [], None)
self.process(rfdl)
###############
def open(self):
'''FOR INTERNAL USE ONLY
Opens the socket, creating portFile if necessary, etc....
RETURNS: Port that the server was started on
EXCEPTIONS: Raises an exception from socket.bind() on failure.
'''
self.close()
self.portNum = None
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
failWith = None
for port in self.portList:
try:
self.server.bind(( self.host, port ))
self.portNum = port
failWith = None
break
except Exception, e:
failWith = e
if self.portFile != None:
file = open(self.portFile, 'w')
file.write('%d\n' % self.portNum)
file.close()
if self.portNum != None:
self.server.listen(10)
if failWith != None:
raise failWith
return(self.portNum)
################
def close(self):
'''FOR INTERNAL USE ONLY
Closes the socket (only if open). Sets internal attributes to
sane values.
RETURNS: Port that the server was started on
EXCEPTIONS: None generated internally.
'''
if self.portFile != None:
os.unlink(self.portFile)
try: self.server.close()
except: pass
self.server = None
for client in self.clientList:
try: client.close()
except: pass
self.clientList = []
self.clientData = {}
self.portNum = None
########################
def rfdlAdd(self, list):
'''Add internal sockets to select() list.
Add to "list", the sockets that we want listened on during a select().
RETURNS: Reference to list passed in.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- list -- (MODIFIED) Mutable list type which sockets should be
appended to.
'''
try: list.append(self.server.fileno())
except: pass
for client in self.clientList:
fn = client.fileno()
if fn < 0: self.clientShutdown(client)
else: list.append(fn)
return(list)
########################
def process(self, rfdl):
'''Iterate over "rfdl", processing our sockets if listed.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- rfdl -- List of sockets as returned by select(). Data (or
connections in the case of the server socket) is processed,
commands are decoded and dispatched.
'''
try: foo = rfdl[0]
except: rfdl = ( rfdl )
for sock in rfdl:
# new connections to server
if self.server != None and sock == self.server.fileno():
conn, addr = self.server.accept()
conn = connection(conn)
self.clientList.append(conn)
self.clientReady(conn, addr)
# data from a client
for client in self.clientList:
fileno = client.fileno()
if sock == fileno:
try: data = client.recv(1024)
except: data = ''
if len(data) < 1:
self.clientShutdown(client)
continue
if self.clientData.has_key(fileno):
data = self.clientData.get(fileno, '') + data
while 1:
if client.dataProcessor:
# If processor returns -1, it consumed all data.
# Otherwise, it returns number of bytes processed.
data = client.dataProcessor(client, data)
break
line, data = extractLine(data)
if line == None: break
line = string.strip(line)
if len(line) > 0:
cmd = line
args = ''
try: cmd, args = string.split(line, None, 1)
except: pass
cmd = string.upper(string.strip(cmd))
args = string.strip(args)
mname = 'do_' + cmd
if hasattr(self, mname):
method = getattr(self, mname)
elif hasattr(self, 'do_unknownCommand'):
method = getattr(self, 'do_unknownCommand')
else:
client.send('500 Unknown command "%s"%s'
% ( cmd, newLine ))
continue
if method(client, cmd, args):
self.clientShutdown(client)
break
if data: self.clientData[fileno] = data
else:
try: del(self.clientData[fileno])
except KeyError: pass
#################################
def clientShutdown(self, client):
'''Close specified client connection.
Used to remove a socket from the list of clients and do other misc
clean-up.
RETURNS: Nothing
EXCEPTIONS: None generated internally.
ARGUMENTS:
- client -- "socket" to be removed.
'''
self.clientClosed(client)
try: client.close()
except IOError: pass
try:
self.clientList.remove(client)
del(self.clientData[client.fileno()])
client.close()
except:
pass
###############################
def clientClosed(self, client):
'''STUB: Called when socket is being closed.
All attempts are made to call this routine before the socket is
actually closed. However, data sent on the "client" socket may
cause an exception. "client.fileno()" can be used as a connection
identifier, but in certain cases this may be -1.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- client -- "socket" which is being closed.
'''
pass
##################################
def clientReady(self, sock, addr):
'''STUB: Called when new connection is opened.
This routine is called right after a connection is opened, displaying
a default connection message.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- sock -- "socket" that client is on.
- addr -- Address as returned by "socket.accept()". Typically, a
2-element list containing host and address of client side.
'''
sock.send('200 %s server ready, connection from %s:%d...%s' %
( self.serverName, addr[0], addr[1], newLine ))
####################################
def getGroupSockList(self, groupId):
'''Returns a list of all sockets in specified group.
Returns a list of all sockets in specified group.
RETURNS: List of socket objects in group "groupId".
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- ID of group to send message to.
'''
list = []
for client in self.clientList:
try:
if client.isinGroup(groupId): list.append(client)
except: pass
return(list)
########################################################
def sendGroup(self, groupId, string, appendNewLine = 0):
'''Sends a string to all clients in a group.
Sends a string to all clients in the specified group, with optional
terminating newLine.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- ID of group to send message to.
- string -- String to send to clients.
- appendNewLine -- write a newLine after string?
'''
for client in self.getGroupSockList(groupId):
try:
client.send(string)
if appendNewLine: client.send(newLine)
except socket.error: pass
#####################################
def do_HELP(self, client, cmd, args):
'''Prints help message based on "helpinfo_*" data elements.
Sends a list of command information based on "helpinfo_*" member
elements to client. Headers and footers are to be provided by
subclasses.
RETURNS: None
Exceptions: None generated internally.
ARGUMENTS:
- client -- Socket which client is on.
- cmd -- Command entered by user.
- args -- Arguments.
'''
list = {}
maxLen = 0
for key in self.__class__.__dict__.keys():
if key[:9] == 'helpinfo_':
element = self.__class__.__dict__[key]
list[element[0]] = element[1]
thisLen = len(element[0])
if thisLen > maxLen: maxLen = thisLen
keys = list.keys()
keys.sort()
if len(keys) > 0:
for key in keys:
client.send(('%-*s %s' % ( maxLen, key, list[key] )) + newLine)
else:
client.send('No help available...' + newLine)
class connection:
#########################
def __init__(self, sock):
self.sock = sock
self.user_data = None
self.groupIdList = {}
self.dataProcessor = None
############################
def __getattr__(self, name):
'''FOR INTERNAL USE ONLY
Unknown attributes are pulled from socket "object".
RETURNS: Named attribute pulled from "self.socket".
EXCEPTIONS: None generated internally.
ARGUMENTS:
- name -- Attribute to look up in "self.socket".
'''
return(getattr(self.sock, name))
########################################
def send(self, data, flags = 0, nl = 0):
'''Send data over the socket.
This is the same as the socket.send function, except that it adds the
"nl" argument. If "nl" is set, a newline is appended to the string
being sent.
RETURNS: Number of bytes sent (same as socket object send() method).
EXCEPTIONS: None generated internally.
ARGUMENTS:
- data -- Data to be sent over socket.
- flags -- (optional, defaults to 0) Same as argument to socket
object send method. "See Unix manual page recv(2) for the
meaning of the optional argument flags."
- nl -- (optional, defaults to 0) If set, sockserv.newLine is
appended to "data".
'''
if nl: return(self.sock.send(data + newLine, flags))
return(self.sock.send(data, flags))
#################################
def processor(self, func = None):
'''Set up function to process all incoming data.
Specify function to be called which handles all incoming data.
Default handler processes data into lines and calls do_* methods
of sockserv class. This overrides that processing (used typicly
for data-phase processing).
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- func -- Function to be called, passing data to process as argument.
If not present, processor function is cleared, returning to
default do_* command processing.
'''
self.dataProcessor = func
############################
def addGroup(self, groupId):
'''Add the client to a given group.
Allows grouping of sockets for different tasks.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- Group ID to place client in.
'''
self.groupIdList[groupId] = 1
###################################
def delGroup(self, groupId = None):
'''Add the client to a given group.
Allows grouping of sockets for different tasks.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- Group ID to place client in. If not specified, removes
client from ALL groups.
'''
if groupId: del(self.groupIdList[groupId])
else: self.groupIdList = {}
#############################
def isinGroup(self, groupId):
'''Is this client in the specified group?
RETURNS: 1 if client is in specified group, 0 otherwise.
EXCEPTIONS: None generated internally.
ARGUMENTS:
- groupId -- Group ID to check client for membership.
'''
return(self.groupIdList.has_key(groupId))
##########################################
# client which connects to socket servers
class sockcli:
############################################################
def __init__(self, port, host = 'localhost', reconnect = 0):
'''Creates an instance of a client.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- port -- Port server is running on.
- host -- Host server is running on.
- reconnect -- If 1, client will try re-connecting if the connection
is broken.
'''
self.host = host
self.port = port
self.reconnect = reconnect
self.sock = None
self.open()
self.data = ''
self.setCallback()
###############
def open(self):
'''Open socket if it's not already open.
RETURNS: None
EXCEPTIONS: None generated internally.
'''
if self.sock == None:
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(self.host, self.port)
except Exception, e:
self.sock = None
####################
def getSocket(self):
'''Returns a list of sockects that should be selected on.
RETURNS: A list of sockects that should be selected on.
EXCEPTIONS: None generated internally.
'''
if self.sock == None and self.reconnect: self.open()
if self.sock == None: return([ ])
return([ self.sock ])
#######################################
def setCallback(self, callback = None):
'''Sets function to be called when data arrives.
RETURNS: None
EXCEPTIONS: None generated internally.
ARGUMENTS:
- callback -- Specifies routine to call when data arrives on the
socket. If ommited, the callback is removed.
'''
self.callback = callback
##################
def process(self):
'''Process data on the socket.
Processes any data on socket, dispatching lines to the callback when
appropriate. Returns 1 while socket is still available.
RETURNS: 1 if socket is still open, or 0 if connection terminated.
EXCEPTIONS: None generated internally.
'''
if self.sock == None and self.reconnect: self.open()
if self.sock == None:
if self.reconnect: return(1)
return(0)
data = self.sock.recv(1024);
if len(data) < 1:
self.sock = None
return self.reconnect
self.data = self.data + data
# locate lines in input
while 1:
line, data = extractLine(data)
if line == None: break
line = string.strip(line)
if len(line) > 0:
if self.callback:
self.callback(self.sock, line)
return(1)
#####################################
# test code
#####################################
if __name__ == '__main__':
class server(sockserv):
def clientClosed(self, client):
print 'Client %d closed' % client.fileno()
helpinfo_QUIT = ( '200- QUIT', 'Terminate the connection.' )
def do_QUIT(self, client, cmd, args):
client.send('Bye now!', nl = 1)
return(1)
helpinfo_SHUTDOWN = ( '200- SHUTDOWN', 'Terminate the server.' )
def do_SHUTDOWN(self, client, cmd, args):
client.send('Server terminating', nl = 1)
self.shutdownServer = 1
return(1)
helpinfo_HELP = ( '200- HELP', 'Display this message.' )
def do_HELP(self, client, cmd, args):
client.send('200-Help information', nl = 1)
client.send('200-', nl = 1)
sockserv.do_HELP(self, client, cmd, args)
client.send('200-', nl = 1)
client.send('200 End of help information.', nl = 1)
s = server(( 5000, 5001, 5002, 5003))
print 'Socket opened on %d' % s.portNum
s.mainloop()
uptimed
#!/usr/bin/env python
#
# sockserv (http://www.tummy.com/sockserv/) example.
#
# Implemnts remote access to "uptime" data
import sockserv, os, string
class uptimed(sockserv.sockserv):
helpinfo_QUIT = ( '200- QUIT', 'Terminate the connection.' )
def do_QUIT(self, client, cmd, args):
client.send('Bye now!', nl = 1)
return(1)
helpinfo_UPTIME = ( '200- UPTIME', 'Display hosts uptime information.' )
def do_UPTIME(self, client, cmd, args):
uptime = string.strip(os.popen('uptime', 'r').readlines()[0])
client.send(str(uptime), nl = 1)
s = uptimed(5000)
s.mainloop()