SOCKET
Un socket è una connessione tra il proprio programma e la
rete IP esterna. Tramite i socket due o più programmmi (scritti
in un qualsiasi linguaggio) possono scambiarsi dei dati senza
necessità di sapere cosa ci sia al di là dei socket stessi.
I programmi infatti possono essere in esecuzione sullo stesso
computer, su computer diversi di una rete LAN locale, oppure su
computer geograficamente distanti collegati tramite internet. I
computer possono anche avere sistemi operativi differenti, o
utilizzare tipologie di LAN differenti (ad esempio uno ethernet e
l'altro token ring), ma tutti questi dettagli sono "nascosti" alle
applicazioni, che non devono fare altro che trasmettere o ricevere
dati attraverso il socket.
Più precisamente i socket sono l'interfaccia di livello più
basso tra il proprio programma e i protocolli (TCP e UDP) del
livello di trasporto dati della rete IP. In sostanza tramite i
socket consegnamo un messaggio ai protocolli di trasporto che,
veicolati e instradati dall'IP sottostante, lo recapitano a
destinazione.
Il sistema di trasporto più semplice è l'UDP, è sufficiente
consegnargli i dati e indicargli l'indirizzo IP e la porta di
ricezione del destinatario. A questo punto i dati vengono
trasportati fino a destinazione, dove, in ricezione a
quell'indirizzo e su quella porta, dovrà esserci il socket del
programma destinatario.
L'UDP è un protocollo semplice, è come imbucare una lettera e
lasciare che le poste, se tutto va bene, la recapitino al
destinatario. Se invece qualcosa va storto, la lettera viene
semplicemente persa (in nessun caso viene rispedita al mittente).
I messaggi possono anche essere spediti a destinatari inesistenti,
in questo caso vengono persi, il mittente non ha alcun modo di
conoscere l'esito dell'operazione se non ricevendo un messaggio di
conferma da parte del destinatario.
Questo può sembrare uno svantaggio, ma nella sua semplicità
l'UDP è molto veloce, e all'interno di una piccola LAN locale
probabilmente è rara la perdita di messaggi. I programmi che
comunicano attraverso questo sistema devono però essere
strutturati in modo da far fronte a questa eventualità.
Se invece serve un trasporto "sicuro", con controllo degli errori,
ritrasmissione automatica in caso di dati non corretti, e con
garanzia che alla fine tutto quello che si è spedito è giunto
integro a destinazione, allora si deve usare il TCP.
CLIENT E SERVER
Generalmente un programma che tiene un socket aperto in ascolto
(listening) su un indirizzo e porta ben precisi si chiama server.
Viceversa un programma che apre un socket e contatta un server si
chiama client.
Un server deve sempre associare (binding) al proprio socket un
indirizzo preciso, che deve essere ovviamente conosciuto da
ciascun client che lo voglia contattare.
Invece un client è obbligato a conoscere solo l'indirizzo da
chiamare, l'indirizzo del proprio socket, se non sottoposto a
binding, viene fornito dal sistema operativo.
Un server riceve comunque l'indirizzo di chi lo sta contattando,
per cui è in grado di rispondergli.
Più in generale quando da un socket arrivano dei dati, assieme ad
essi arriva anche l'indirizzo IP di chi li ha mandati.
I DATI TRASMESSI E RICEVUTI
Tipo di dati trasmessi e ricevuti
I dati trasferiti sono fondamentalmente delle sequenze di byte lunghe al massimo quanto impostato
come grandezza del buffer di ricezione.
Se ad un socket UDP arriva una sequenza più lunga del suo buffer,
il socket stesso va in errore e i dati sono persi.
Inoltre se ad un socket arrivano più messaggi (datagrammi) senza
che i precedenti siano stati acquisiti dal programma destinatario,
i messaggi si accodano e vanno recuperati ciascuno con una
successiva lettura.
Il contenuto delle sequenze binarie ha senso solo per i programmi mittente
e destinatario, ai socket non interessa cosa viene trasmesso, allo
stesso modo ai programmi non interessa sapere come fanno i socket
a vedersi tra di loro. L'unica cosa che conta è che entrambi i programmi si appoggino
allo stesso tipo di "trasportatore", TCP o UDP.
IN PRATICA
Vogliamo trasmettere delle stringhe di caratteri ASCII da un programma client ad un
programma server tramite protocollo UDP/IP, il server deve stare
in ascolto (listening) all'indirizzo 192.168.1.22 porta 5000.
Il server deve rimandare indietro la stringa al mittente (eco) e
stampare cosa ha ricevuto. Inoltre se la stringa è uguale a "stop
server" deve terminare la sua esecuzione.
Il client deve trasmettere le stringhe, ricevere l'eco e
stamparlo, e alla fine inviare la stringa "stop server" per
arrestare il server stesso.
Creiamo il socket UDP del client dandogli il nome cli:
import socket
cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Con un ciclo for trasmettiamo in formato stringa i numeri da 1 a
10, e riceviamo e stampiamo l'eco di ritorno:
for h in range(10):
stringa = str(h+1)
cli.sendto(stringa.encode("ascii"), ("192.168.1.22", 5000))
dati = cli.recv(8192)
print("Eco: " + dati.decode("ascii"))
Ed infine diamo lo stop al server e chiudiamo il nostro socket:
cli.sendto("stop server".encode("ascii"), ("192.168.1.22", 5000))
cli.close()
Sul lato server facciamo più o meno la stessa cosa, creiamo un
socket UDP di nome ser e lo associamo (binding) ad un indirizzo
ben preciso:
import socket
ser = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ser.bind("192.168.1.22", 5000)
A questo punto con un ciclo ci mettiamo in ascolto, stampiamo i
messaggi ricevuti e li rispediamo al mittente. Se il messaggio
corrisponde a "stop server" allora terminiamo l'esecuzione del
ciclo con break e chiudiamo il socket:
while True:
dati, addr = ser.recvfrom(8192)
print("Ricevuto: " + dati.decode("ascii"))
cli.sendto(dati, addr)
if dati.decode("ascii") == "stop server": break
ser.close()
Come si può vedere nel server usiamo recvfrom
invece di recv per ottenere in addr anche l'indirizzo del
mittente, quindi possiamo rispondergli.
Nel caso del client le uniche cose che possono arrivare sono le
risposte del server contattato (nessun altro conosce l'indirizzo
del client), mentre il server, avendo effettuato il bind con un
indirizzo preciso, può essere chiamato da chiunque conosca
quell'indirizzo, anche da più di un client (caso non gestito in
questi esempi).
Il codice degli esempi appena visti è dimostrativo, in pratica
vi sono alcuni probemi, il primo è che le letture sono
"bloccanti", cioè il programma ogni volta che incontra
una recvfrom() si ferma e attende indefinitmente l'arrivo di un datagramma.
Questo può portare a situazioni di blocco dell'elaborazione
(basta un pacchetto perso), e anche a non poter più usare un
indirizzo IP nel caso il programma venga interrotto forzatamente.
Inoltre le istruzioni recvfrom() possono generare una serie di
errori difficilmente prevedibili, che interromperebbero
immediatamente l'esecuzione del programma, lasciando l'altro capo
"appeso", in attesa di una risposta che non arriverà mai.
I programmi client e server visti prima potrebbero funzionare
bene, se non si verifica nessun errore di comunicazione, ma
potrebbero anche bloccarsi senza possibilità di fermarli.
Un altro errore che sembra verificarsi spesso è "connection reset
by peer", ad esempio provando a leggere un socket subito dopo
averlo usato per inviare dei dati. Questo si risolve aggiungendo
un ritardo di qualche millisecondo dopo l'invio usando la funzione
sleep del modulo time.
NON BLOCCHIAMO...
Un sistema per togliersi da ogni impiccio in modo semplice è
quello di:
- rendere non bloccanti le recv/recvfrom aggiungendo un
timeout
- racchiudere tutte le ricezioni in blocchi try/except
- generare piccoli ritardi dopo le trasmissioni
Il primo punto è il più semplice, prima dell'utilizzo del socket
in lettura si imposta un timeout:
nomesocket.settimeout(10)
In questo caso, quando si chiama una recv/recvfrom, se ci sono
dati vengono letti, altrimenti il socket attende per 10 secondi al massimo.
Se il timeout scade viene sollevata un'eccezione di tipo
socket.timeout. Passare 0 a settimeout fa si che la lettura non
attenda, se ci sono dati vengono ritornati, altrimenti il socket
va subito in errore. Passando invece Null il socket torna ad
essere bloccante come di default.
Per la gestione degli errori può essere interessante creare una
piccola funzione di nome sock_rd che fa una lettura, se va a buon
fine ritorna dati e indirizzo del mittente, se va male per
qualsiasi motivo (reset di connessione, datagrammi troppo grandi,
dati non presenti) restituisce stringa nulla e indirizzo nullo.
def sock_rd(s):
try:
return s.recvfrom(LEN_UDP_BUFFER)
except:
return "".encode("ascii"), ("", 0)
Per una lettura con timeout variabili può invece essere comoda una
funzione, chiamiamola sock_rdt, e dotiamola anche di un parametro
facoltativo "timeout" in modo da poter eventualmente generare un
timeout lungo a piacere:
def sock_rdt(s, timeout=1.0):
s.settimeout(timeout)
try:
return s.recvfrom(LEN_UDP_BUFFER)
except:
return "".encode("ascii"), ("", 0)
E se a questo punto ci serve invece di nuovo una lettura
bloccante? Può essere il caso del ciclo di attesa principale di
un server che nel frattempo non deve fare altro. Allora basta
semplicemente chiamare sock_rdt specificando timeout=Null
Ecco allora come conviene modificare i programmi client e server
precedenti, per renderli "immuni" da errori di comunicazione di
basso livello e blocchi in cicli infiniti:
#-----------------------------------------------------
# Esempio client UDP
#-----------------------------------------------------
import socket, time
REMOTE_ADDR = "192.168.1.22", 5000
LEN_UDP_BUFFER = 8192
#-----------------------------------------------------
def sock_rdt(s, timeout=1.0):
s.settimeout(timeout)
try:
return s.recvfrom(LEN_UDP_BUFFER)
except:
return "".encode("ascii"), ("", 0)
#-----------------------------------------------------
cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for h in range(10):
cli.sendto(str(h+1).encode("ascii"), REMOTE_ADDR)
time.sleep(0.005)
dati, addr = sock_rdt(cli)
if dati:
print("Eco: " + dati.decode("ascii"))
else:
print("Timeout ricezione!")
cli.sendto("stop server".encode("ascii"), REMOTE_ADDR)
cli.close()
try: raw_input("...")
except NameError: input("...")
#-----------------------------------------------------
# Esempio server UDP
#-----------------------------------------------------
import socket
LOCAL_ADDR = "192.168.1.22", 5000
LEN_UDP_BUFFER = 8192
#-----------------------------------------------------
def sock_rd(s):
try:
return s.recvfrom(LEN_UDP_BUFFER)
except:
return "".encode("ascii"), ("", 0)
#-----------------------------------------------------
ser = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ser.bind(LOCAL_ADDR)
while True:
dati, addr = sock_rd(ser)
if dati:
s = "Da (%s:%s) Ricevuto: %s"
print(s % (addr[0], addr[1], dati.decode("ascii")))
ser.sendto(dati, addr)
else:
print("Errore ricezione!")
if dati.decode("ascii") == "stop server": break
ser.close()
try: raw_input("...")
except NameError: input("...")
La ricezione del server è stata lasciata volutamente bloccante,
cioè la funzione sock_rd termina solo quando arrivano
effettivamente dei dati... o quando si verifica un errore, che in
questo caso non crea alcun tipo di problema se non la stampa
dell'avviso "Errore ricezione!". Quando arriva un messaggio viene
stampato anche l'indirizzo del mittente. Nota: il programma server
si può arrestare solo inviandogli la stringa "stop server".
L'indirizzo IP è stato scritto in una costante, come pure la
dimensione del buffer di ricezione, in questo caso di 8 kbyte.
Qui sotto le finestre client (dietro) e server (in primo piano) al
termine dell'esecuzione, i tre puntini sono una richiesta di input
da tastiera per non far sparire subito le finestre in ambiente Windows.
Due classi per simulare tramite messaggi UDP/IP un dialogo su
porta seriale (con gli stessi metodi di pyserial)
Con le due classi ClientConnector e ServerConnector, derivate da
UDP_Connector, è
possibile inviare in modo semplificato stringhe tra
un'applicazione e l'altra, usando gli stessi metodi di pyserial, e
quindi lavorare come se le applicazioni fossero collegate tra loro
da porte seriali.
È mantenuto il concetto di applicazione client che interroga un'applicazione
server, e il client deve conoscere esattamente
l'indirizzo del server per poterlo raggiungere.
I metodi usabili sono riassunti nei commenti all'inizio. Una
differenza con pyserial è che chiamando read() senza specificare
la quantità di caratteri da leggere viene ritornato tutto, mentre
in pyserial verrebbe ritornato un solo carattere come se si fosse
scritto read(1).
Per inviare una stringa ASCII in giro per il mondo:
import UDP_Connector
cli = UDP_Connector.ClientConnector("206.190.60.37", 5555)
cli.write("hello yahoo".encode("ascii")))
cli.close()
In questo caso inviamo una stringa in un datagramma UDP verso un
server di yahoo, ma naturalmente non possiamo aspettarci alcuna
risposta in quanto non stiamo usando nessun protocollo da lui
riconosciuto, probabilmente non avrà alcun socket in ascolto
sulla porta 5555, anzi è ancora più probabile che il nostro
datagramma venga piallato già prima dal loro firewall.
Se invece usassimo un socket TCP puntando la porta 80 e inviando
una stringa correttamente formattata come richiesta HTTP, allora
risponderebbe il web server inviandoci il contenuto HTML
richiesto. Funzioni già pronte per compiere queste operazioni
si trovano nei moduli urllib e
urllib2, nel nostro caso ci limitiamo invece ad inviare delle
semplici stringhe verso un apposito programma server. Ad esempio
possiamo riprendere gli esempi client/server di prima e
riscriverli usando queste classi:
#---------------------------------------------------------
# Esempio client che comunica in stile pyserial
#---------------------------------------------------------
import UDP_Connector
cli = UDP_Connector.ClientConnector("192.168.1.22", 5010)
for h in range(10):
cli.write(str(h+1).encode("ascii"))
dati = cli.read()
if dati:
print("Eco: " + dati.decode("ascii"))
else:
print("Timeout risposta!")
cli.write("stop server".encode("ascii"))
cli.close()
try: raw_input("...")
except NameError: input("...")
#---------------------------------------------------------
# Esempio server che comunica in stile pyserial
#---------------------------------------------------------
import UDP_Connector
ser = UDP_Connector.ServerConnector("192.168.1.22", 5010)
while True:
dati = ser.read()
if dati:
print("Ricevuto: " + dati.decode("ascii"))
ser.write(dati)
if dati.decode("ascii") == "stop server": break
ser.close()
try: raw_input("...")
except NameError: input("...")
SOCKET SERVER
Il modulo socketserver permette di realizzare un server in modo ancora più
semplice.
#-----------------------------------------------------
# Esempio server UDP
#-----------------------------------------------------
from socketserver import UDPServer, BaseRequestHandler
LOCAL_ADDR = "192.168.1.22", 5000
#-----------------------------------------------------
class Gest(BaseRequestHandler):
def handle(self):
sock = self.request[1]
data = self.request[0]
addr = self.client_address
print(
"From (%s:%s) Recieved: %s" %
(addr[0], addr[1], data.decode("ascii"))
)
sock.sendto(data, addr)
#-----------------------------------------------------
server = UDPServer(LOCAL_ADDR, Gest)
server.serve_forever()
Subclassamento del server per renderlo stoppabile da comando:
#-----------------------------------------------------
from socketserver import UDPServer, BaseRequestHandler
LOCAL_ADDR = "192.168.1.22", 5000
#-----------------------------------------------------
class Handler(BaseRequestHandler):
def handle(self):
sock = self.request[1]
data = self.request[0]
addr = self.client_address
print(
"From (%s:%s) Recieved: %s" %
(addr[0], addr[1], data.decode("ascii"))
)
sock.sendto(data, addr)
if data.decode("ascii") == "stop server":
self.server.stop_serving()
#-----------------------------------------------------
class StoppableUDPServer(UDPServer):
def __init__(self, addr, handler):
UDPServer.__init__(self, addr, handler)
self.serving = True
def serve_forever(self):
# ridefinisce server_forever aggiungendo condizione di stop
while self.serving:
self.handle_request()
def stop_serving(self):
self.serving = False
#-----------------------------------------------------
server = StoppableUDPServer(LOCAL_ADDR, Handler)
server.serve_forever()
TELECONTROLLI
La comunicazione tramite socket apre le porte ai telecontrolli via
internet, basta pensare ad una connessione ethernet/internet come
ponte per creare un cavo seriale virtuale. O ancora, possiamo
creare un server per simulare la presenza di un apparato in modo
da testare il client, poi è sufficiente cambiare la classe da
cconnector a pyserial e dialogare con l'apparato vero e proprio.
In ogni caso l'applicazione server deve trovarsi ad un indirizzo
conoscibile e raggiungibile.
Per essere conoscibile o si appoggia ad una connessione con IP
statico, oppure è necessario un "servizio ponte" che viene
contattato periodicamente dall'applicazione stessa per rendere
noto il suo IP. Il client deve chiamare questo servizio ponte che
lo reindirizzerà all'IP del server.
Per essere raggiungibile occorre anche istruire il router/firewall
indicandogli che le connessioni in arrivo verso una data porta
possono passare e vanno reindirizzate verso un PC ben preciso
della LAN locale (che ovviamente dovrà avere un indirizzo fisso
all'interno della LAN).
Se interessano i telecontrolli non vanno dimenticati gli
ethernet serial server, scatolotti che
mettono a disposizione una o più porte seriali e che sono
collegati ad una LAN, vengono configurati con un proprio IP /
porta / protocollo, e possono essere contattati tramite UDP, TCP
ecc.