Python Socket Server
Revision as of 18:21, 24 May 2008 by PeterHarding (talk | contribs) (New page: =Socket Server= <pre> #!/usr/bin/python # # Copyright (c) 1998, 1999, Sean Reifschneider, tummy.com, ltd. # All Rights Reserved # # High-level classes for implementing command/response...)
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()