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)
- RestituisceTrue
per elaborare la richiesta oFalse
per ignorarla. Ad esempio, un server potrebbe rifiutare richieste provenienti da un raggio di indirizzi IP oppure se è sovraccarico.process_request(request, client_address)
- Chiamafinish_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. Chiamahandle()
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. InStreamRequestHandler
, il metodosetup()
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 durantesetup()
.
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
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