socketserver - Creare Server di Rete

Scopo: Creare Server di Rete

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 blocca il flusso del programma fino al completamento della richiesta stessa) su TCP, UDP, canali Unix e datagrammi Unix. Fornisce anche classi mix-in per convertire facilmente i server all'uso di un thread o processo separato per ogni richiesta.

La responsabilità per l'elaborazione di una richiesta viene divisa da una classe per il server e da un classe per la gestione della richiesta. Il server si occupa degli aspetti della comunicazione tipo stare in ascolto su di un socket, accettare connessioni, mentre il gestore di richiesta si occupa degli aspetti del "protocollo" come l'interpretazione dei dati in ricezione, elaborazione, e reinvio dei dati al client. Questa divisione di responsabilità comporta che molte applicazioni possono usare una delle classi server esistenti senza modifiche, e fornire una classe per la gestione della richiesta che possa lavorare con il protocollo personalizzato.

Tipi di Server

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

Oggetti Server

Per costruire un server, si passa un indirizzo sul quale si ascoltano le richieste e 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.

Una volta che l'oggetto server è stato istanziato, si usa handle_request() oppure serve_forever() per elaborare le richieste. Il metodo serve_forever() chiama semplicemente handle_request() in un ciclo infinito, ma se una applicazione deve integrare il server con un altro ciclo di eventi oppure usare select() per monitorare socket diversi per server diversi può chiamare handle_request() direttamente.

Implementare un server

Per creare un server, in genere è possibile riutilizzare una delle classi esistenti e fornire una classe personalizzata per la gestione della richiesta. Per altri casi BaseServer mette a disposizione parecchi metodi che possono essere sovrascritti in una sottoclasse:

  • verify_request(request, client_address) - Restituisce True per elaborare la richiesta o False per ignorarla. Ad esempio, un server potrebbe rifiutare richieste provenienti da un raggio di indirizzi IP oppure se è sovraccarico.
  • process_request(request, client_address) - Chiama finish_request() per eseguire effettivamente il lavoro di gestione della richiesta. Può anche creare un thread o processo separato, come fanno le classi mix-in.
  • finish_request(request, client_address) - Crea una istanza di un gestore di richiesta usando la classe fornita al costruttore del server. Chiama handle() sul gestore di richiesta per elaborarla.

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, XML-RPC, AMQP). Il gestore di richiesta legge la richiesta dal canale dati in arrivo, la elabora e invia la risposta. Ci sono 3 metodi disponibili che possono essere sovrascritti.

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

Molti gestori possono essere implementati con il solo metodo handle().

Esempio di Eco

Questo esempio implementa una semplice coppia server/gestore di richiesta che accetta connessioni TCP e ritorna gli stessi dati inviati dal client. Si inizia con il gestore di richiesta.

# socketserver_echo.py

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')

        # Ritona gli stessi dati 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)

Il solo metodo implementato è EchoRequestHandler.handle(), ma sono incluse versioni di tutti i metodi sopra descritti per illustrare la sequenza delle chiamate effettuate. La classe EchoServer non fa nulla di diverso da TCPServer, eccetto il registrare quando viene chiamato ciascun metodo.

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, poll_interval=0.5):
        self.logger.debug('in attesa della richiesta')
        self.logger.info(
            'Gestione della richiesta, premere <Ctrl-C> per abbandonare'
        )
        socketserver.TCPServer.serve_forever(self, poll_interval)
        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,
        )

    def shutdown(self):
        self.logger.debug('shutdown()')
        return socketserver.TCPServer.shutdown(self)

L'ultimo passo è quello di aggiungere il programma principale che imposta il server per essere eseguito in un thread, e gli invia dati per illustrare quali metodi sono chiamati mentre i dati sono restituiti.

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

    address = ('localhost', 0)  # lasciamo assegnare la porta al kernel
    server = EchoServer(address, EchoRequestHandler)
    ip, port = server.server_address  # che porta è stata assegnata?

    # Partenza del server in un thread
    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # non rimane piantato in uscita
    t.start()

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

    # Connessione al server
    logger.debug('creazione del socket')
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    logger.debug('connessione al server')
    s.connect((ip, port))

    # Invio dati
    message = 'Ciao, mondo'.encode()
    logger.debug('invio dati: %r', message)
    len_sent = s.send(message)

    # Ricezione di una risposta
    logger.debug('in attesa di risposta')
    response = s.recv(len_sent)
    logger.debug('risposta dal server: %r', response)

    # Pulizia
    server.shutdown()
    logger.debug('chiusura del socket')
    s.close()
    logger.debug('fatto')
    server.socket.close()

Eseguendo il programma si produce il seguente risultato:

$ python3 socketserver_echo.py

EchoServer: __init__
EchoServer: server_activate
EchoServer: in attesa della richiesta
EchoServer: Gestione della richiesta, premere <Ctrl-C> per abbandonare
client: Server su 127.0.0.1:56579
client: creazione del socket
client: connessione al server
client: invio dati: b'Ciao, mondo'
client: in attesa di risposta
EchoServer: verify_request(<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 56579), raddr=('127.0.0.1', 42546)>, ('127.0.0.1', 42546))
EchoServer: process_request(<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 56579), raddr=('127.0.0.1', 42546)>, ('127.0.0.1', 42546))
EchoServer: finish_request(<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 56579), raddr=('127.0.0.1', 42546)>, ('127.0.0.1', 42546))
EchoRequestHandler: __init__
EchoRequestHandler: setup
EchoRequestHandler: handle
EchoRequestHandler: recv()->"b'Ciao, mondo'"
EchoRequestHandler: finish
client: risposta dal server: b'Ciao, mondo'
EchoServer: close_request(<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 56579), raddr=('127.0.0.1', 42546)>)
EchoServer: shutdown()
client: chiusura del socket
client: fatto
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, fornire quel numero di porta nella tupla dell'indirizzo invece che 0.

Ecco una versione condensata dello stesso programma, senza la registrazione delle chiamate. E' necessario fornire solo il metodo handle() nella classe del gestore di richiesta.

# socketserver_echo_simple.py

import socketserver


class EchoRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # Echo the back to the client
        data = self.request.recv(1024)
        self.request.send(data)
        return


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

    address = ('localhost', 0)  # lasciamo assegnare la porta al kernel
    server = socketserver.TCPServer(address, EchoRequestHandler)
    ip, port = server.server_address  # che porta è stata assegnata?

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

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

    # Invio dati
    message = 'Ciao, mondo'.encode()
    print('In invio : {!r}'.format(message))
    len_sent = s.send(message)

    # Ricezione di una risposta
    response = s.recv(len_sent)
    print('Ricevuto : {!r}'.format(response))

    # Pulizia
    server.shutdown()
    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.

$ python3 socketserver_echo_simple.py

In invio : b'Ciao, mondo'
Ricevuto : b'Ciao, mondo'

Threading e Forking

Per aggiungere il supporto di threading e forking a un server si include 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, e il lavoro viene effettuato nel nuovo figlio.

Per i thread si usa ThreadingMixIn:

# socketserver_threaded.py


socketserver_threaded.py
import threading
import socketserver


class ThreadedEchoRequestHandler(
        socketserver.BaseRequestHandler,
):

    def handle(self):
        # Ripete al client
        data = self.request.recv(1024)
        cur_thread = threading.currentThread()
        response = b'%s: %s' % (cur_thread.getName().encode(),
                                data)
        self.request.send(response)
        return


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


if __name__ == '__main__':
    import socket

    address = ('localhost', 0)  # lasciamo assegnare la porta al kernel
    server = ThreadedEchoServer(address,
                                ThreadedEchoRequestHandler)
    ip, port = server.server_address  # che porta è stata assegnata?

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

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

    # Invio dati
    message = 'Ciao, mondo'.encode()
    print('In invio : {!r}'.format(message))
    len_sent = s.send(message)

    # Ricezione di una risposta
    response = s.recv(len_sent)
    print('Ricevuto : {!r}'.format(response))

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

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

$ python3 socketserver_threaded.py

In invio : b'Ciao, mondo'
Ricevuto : b'Thread-2: Ciao, mondo'

Per usare processi separati si usa ForkingMixIn:

# socketserver_forking.py

import os
import socketserver


class ForkingEchoRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # Ripete al client
        data = self.request.recv(1024)
        cur_pid = os.getpid()
        response = b'%d: %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)  # lasciamo assegnare la porta al kernel
    server = ForkingEchoServer(address,
                               ForkingEchoRequestHandler)
    ip, port = server.server_address  # che porta è stata assegnata?

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # non rimane piantato in uscita
    t.start()
    print('Ciclo del server in esecuzione nel processo:', os.getpid())

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

    # Invio dati
    message = 'Ciao, mondo'.encode()
    print('In invio : {!r}'.format(message))
    len_sent = s.send(message)

    # Ricezione di una risposta
    response = s.recv(1024)
    print('Ricevuto : {!r}'.format(response))

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

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

$ python3 socketserver_forking.py

Ciclo del server in esecuzione nel processo: 12733
In invio : b'Ciao, mondo'
Ricevuto : b'12735: Ciao, mondo'

Vedere anche:

socketserver
La documentazione della libreria standard per questo modulo