SocketServer - Creazione di server di rete

Scopo Creazione di server di rete
Versione Python 1.4

Il modulo SocketServer è una infrastruttura per la creazione di server di rete. Definisce le classi per la gestione di richieste di rete sincrone (il gestore della richiesta del server si blocca fino al completamento di una richiesta) su TCP, UDP, flussi Unix e datagrammi Unix. Può anche fornire classi mix-in per convertire facilmente i server all'uso di un thread o processo separato per ogni richiesta, a seconda di quello che è più appropriato per le proprie esigenze.

La responsabilità per l'elaborazione di una richiesta viene condivisa da una classe per il server e da un classe per la gestione della richiesta. Il server si occupa degli aspetti della comunicazione (stare in ascolto su di un socket, accettare connessioni, ecc.) ed il gestore di richiesta si occupa degli aspetti del "protocollo" (interpretazione dei dati in ricezione, elaborazione, re-invio dei dati al client). Questa divisione di responsabilità comporta che in molti casi si può semplicemente usare una delle classi server esistenti senza ulteriori modifiche, e fornire una classe per la gestione della richiesta che possa lavorare con il proprio protocollo.

Tipi di Server

Ci sono cinque diverse classi server definite in SocketServer. BaseServer definisce l'API, e non è concepita per essere istanziata ed usata direttamente. TCPServer usa i socket TCP/IP per comunicare. UDPServer usa i socket datagramma. UnixStreamServer e UnixDatagramServer usano i socket domain di Unix sono sono disponibili solo su piattaforme Unix.

Oggetti server

Per costruire un server, si passa un indirizzo al quale si ascoltano le richieste ed una classe (non una istanza) di un gestore di richiesta. Il formato dell'indirizzo dipende dal tipo di server e dalla famiglia di socket usati. Si faccia riferimento alla documentazione del modulo socket per i dettagli.

Implementare un server

Se si sta creando un server, in genere è possibile riutilizzare una delle classi esistenti e fornire semplicemnte un classe per la gestione della richiesta personalizzata. Se questo non fa al caso proprio, ci sono diversi metodi di BaseServer a disposizione per eseguire la riscrittura in una sotto classe:

  • verify_request(request, client_address) - Restituisce True per elaborare la richiesta o False per ignorarla. Si potrebbe, ad esempio, rifiutare richieste provenienti da un ventaglio di indirizzi IP se si vuole bloccare l'accesso al server a client particolari.
  • process_request(request, client_address) - In genere la semplice chiamata di finish_request() svolge il lavoro necessario. Si può anche creare un thread o processo separato, come fanno le classi mix-in (vedere sotto).
  • finish_request(request, client_address) - Crea una istanza di un gestore di richiesta che usa la classe fornita al costruttore del server. Si chiama handle() sul gestore di richiesta per elaborare la richiesta.

Gestori di richiesta

Essi svolgono la maggior parte del lavoro di ricezione delle richieste in arrivo e decidono quale azione intraprendere. Il gestore è responsabile dell'implementazione del "protocollo" alla sommità del socket layer (ad esempio HTTP oppure XML-RPC). Il gestore di richiesta legge la richiesta dal canale dati in arrivo, lo elabora e invia la risposta. Ci sono 3 metodi disponibili per la riscrittura.

  • setup() - Prepara il gestore di richiesta la richiesta. Ad esempio in StreamRequestHandler, il metodo setup() crea oggetti di tipo file per la lettura dal socket e per la scrittura verso il medesimo.
  • handle() - Svolge l'effettivo lavoro per la richiesta. Analizza la richiesta in arrivo, elabora i dati ed invia la risposta.
  • finish() - Pulisce tutto quanto creato durante setup().

Nella maggior parte dei casi si può semplicemente fornire un metodo handle().

Esempio di Eco

Si esamina una semplice coppia server/gestore di richiesta che accetta connessioni TCP e ritorna gli stessi dati inviati dal client. Il solo metodo che occorre fornire nel codice di esempio è EchoRequestHandler.handle() , ma tutti i metodi sopra descritti sono riscritti per inserire delle chiamate a logging in modo che l'output del programma di esempio possa illustrare la sequenza delle chiamate fatte.

La sola cosa rimasta da fare è quella di avere un semplice programma che crea il server, lo esegue in un thread, e si connette ad esso per illustrare quali metodi sono stati chiamati mentre i dati sono restituiti

import logging
import sys
import SocketServer

logging.basicConfig(level=logging.DEBUG,
                    format='%(name)s: %(message)s',
                    )

class EchoRequestHandler(SocketServer.BaseRequestHandler):

    def __init__(self, request, client_address, server):
        self.logger = logging.getLogger('EchoRequestHandler')
        self.logger.debug('__init__')
        SocketServer.BaseRequestHandler.__init__(self, request, client_address, server)
        return

    def setup(self):
        self.logger.debug('setup')
        return SocketServer.BaseRequestHandler.setup(self)

    def handle(self):
        self.logger.debug('handle')

        # Restituisce i dati ricevuti al client
        data = self.request.recv(1024)
        self.logger.debug('recv()->"%s"', data)
        self.request.send(data)
        return

    def finish(self):
        self.logger.debug('finish')
        return SocketServer.BaseRequestHandler.finish(self)

class EchoServer(SocketServer.TCPServer):

    def __init__(self, server_address, handler_class=EchoRequestHandler):
        self.logger = logging.getLogger('EchoServer')
        self.logger.debug('__init__')
        SocketServer.TCPServer.__init__(self, server_address, handler_class)
        return

    def server_activate(self):
        self.logger.debug('server_activate')
        SocketServer.TCPServer.server_activate(self)
        return

    def serve_forever(self):
        self.logger.debug('In attesa della richiesta')
        self.logger.info('Elaborazione delle richieste, prmere <Ctrl-C> per abbandonare')
        while True:
            self.handle_request()
        return

    def handle_request(self):
        self.logger.debug('handle_request')
        return SocketServer.TCPServer.handle_request(self)

    def verify_request(self, request, client_address):
        self.logger.debug('verify_request(%s, %s)', request, client_address)
        return SocketServer.TCPServer.verify_request(self, request, client_address)

    def process_request(self, request, client_address):
        self.logger.debug('process_request(%s, %s)', request, client_address)
        return SocketServer.TCPServer.process_request(self, request, client_address)

    def server_close(self):
        self.logger.debug('server_close')
        return SocketServer.TCPServer.server_close(self)

    def finish_request(self, request, client_address):
        self.logger.debug('finish_request(%s, %s)', request, client_address)
        return SocketServer.TCPServer.finish_request(self, request, client_address)

    def close_request(self, request_address):
        self.logger.debug('close_request(%s)', request_address)
        return SocketServer.TCPServer.close_request(self, request_address)

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # si ottiene una porta dal kernel
    server = EchoServer(address, EchoRequestHandler)
    ip, port = server.server_address # si trova quale porta si è ottenuto

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # non rimane appeso in uscita
    t.start()

    logger = logging.getLogger('client')
    logger.info('Server on %s:%s', ip, port)

    # Connessione al server
    logger.debug('creating socket')
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    logger.debug('connecting to server')
    s.connect((ip, port))

    # Invio dei dati
    message = 'Hello, world'
    logger.debug('sending data: "%s"', message)
    len_sent = s.send(message)

    # Ricezione della risposta
    logger.debug('waiting for response')
    response = s.recv(len_sent)
    logger.debug('response from server: "%s"', response)

    # Pulizia
    logger.debug('closing socket')
    s.close()
    logger.debug('done')
    server.socket.close()

Il risultato dell'esecuzione del programma dovrebbe essere circa questo:

$ python SocketServer_echo.py
EchoServer: __init__
EchoServer: server_activate
EchoServer: In attesa della richiesta
client: Server on 127.0.0.1:48661
client: creating socket
client: connecting to server
client: sending data: "Hello, world"
EchoServer: Elaborazione delle richieste, prmere  per abbandonare
client: waiting for response
EchoServer: handle_request
EchoServer: verify_request(, ('127.0.0.1', 51950))
EchoServer: process_request(, ('127.0.0.1', 51950))
EchoServer: finish_request(, ('127.0.0.1', 51950))
EchoRequestHandler: __init__
EchoRequestHandler: setup
EchoRequestHandler: handle
EchoRequestHandler: recv()->"Hello, world"
EchoRequestHandler: finish
client: response from server: "Hello, world"
EchoServer: close_request()
client: closing socket
EchoServer: handle_request
client: done

Il numero della porta usata cambierà ogni volta che il programma viene eseguito, visto che il kernel alloca automaticamente una porta disponibile. Se si vuole che il server sia in ascolto su di una specifica porta ogni volta che si esegue, allora si fornisce il numero di porta desiderato nella tupla dell'indirizzo invece che 0.

Ecco una versione semplificata dello stesso programma, senza logging:

import SocketServer

class EchoRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        # Restituisce i dati ricevuti al client
        data = self.request.recv(1024)
        self.request.send(data)
        return

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # si ottiene una porta dal kernel
    server = SocketServer.TCPServer(address, EchoRequestHandler)
    ip, port = server.server_address # si trova quale porta si è ottenuto

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # non rimane appeso in uscita
    t.start()

    # Connessione al server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Invio dei dati
    message = 'Hello, world'
    print 'In invio: "%s"' % message
    len_sent = s.send(message)

    # Ricezione della risposta
    response = s.recv(len_sent)
    print 'Ricevuti: "%s"' % response

    # Pulizia
    s.close()
    server.socket.close()

In questo caso non serve alcuna classe speciale per il server visto che TCPServer gestisce tutte le esigenze del server.

$ python SocketServer_echo_simple.py
In invio: "Hello, world"
Ricevuti: "Hello, world"

Threading e Forking

Aggiungere il supporto al threading ed al forking è tanto semplice quanto l'includere l'appropriato mix-in nella gerarchia di classe per il server. Le classi mix-in riscrivono process_request() per iniziare un nuovo thread o processo quando una richiesta è pronta per essere gestita, ed il lavoro viene effettuato nel nuovo figlio.

Per i thread si usa ThreadingMixIn:

import threading
import SocketServer

class ThreadedEchoRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        # Restituisce i dati ricevuti al client
        data = self.request.recv(1024)
        cur_thread = threading.currentThread()
        response = '%s: %s' % (cur_thread.getName(), data)
        self.request.send(response)
        return

class ThreadedEchoServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # si ottiene una porta dal kernel
    server = ThreadedEchoServer(address, ThreadedEchoRequestHandler)
    ip, port = server.server_address # si trova quale porta si è ottenuto

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # non rimane appeso in uscita
    t.start()
    print 'Server loop running in thread:', t.getName()

    # Connessione al server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Invio dei dati
    message = 'Hello, world'
    print 'In invio: "%s"' % message
    len_sent = s.send(message)

    # Ricezione della risposta
    response = s.recv(1024)
    print 'Ricevuti: "%s"' % response

    # Pulizia
    s.close()
    server.socket.close()

La risposta dal server comprende l'identificativo del thread dove la richiesta viene gestita.

s$ python SocketServer_threaded.py
Server loop running in thread: Thread-1
In invio: "Hello, world"
Ricevuti: "Thread-2: Hello, world"

Per usare processi separati si usa ForkingMixIn:

import os
import SocketServer

class ForkingEchoRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        # Echo the back to the client
        data = self.request.recv(1024)
        cur_pid = os.getpid()
        response = '%s: %s' % (cur_pid, data)
        self.request.send(response)
        return

class ForkingEchoServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
    pass


if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # si ottiene una porta dal kernel
    server = ForkingEchoServer(address, ForkingEchoRequestHandler)
    ip, port = server.server_address # si trova quale porta si è ottenuto

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # non rimane appeso in uscita
    t.start()
    print 'Server loop running in process:', os.getpid()


    # Connessione al server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Invio dei dati
    message = 'Hello, world'
    print 'In invio: "%s"' % message
    len_sent = s.send(message)

    # Ricezione della risposta
    response = s.recv(1024)
    print 'Ricevuti: "%s"' % response

    # Pulizia
    s.close()
    server.socket.close()

In questo caso l'identificativo del processo viene incluso nella risposta del server:

$ python SocketServer_forking.py
Server loop running in process: 7443
In invio: "Hello, world"
Ricevuti: "7445: Hello, world"

Vedere anche:

SocketServer
La documentazione della libreria standard per questo modulo
asyncore
Si usa asyncore per creare server asincroni che non si bloccano durante l'elaborazione di una richiesta.
SimpleXMLRPCServer
Server XML-RPC costruito usando SocketServer.