Ho riscontrato un problema ricorrente durante la comunicazione con apparecchiature dotate di collegamenti TCP separati per l'invio e la ricezione. Il problema di base è che lo stack TCP in genere non ti dice che un socket è chiuso quando stai solo provando a leggere:devi provare a scrivere per sapere che l'altra estremità del collegamento è stata interrotta. In parte, è proprio così che è stato progettato TCP (la lettura è passiva).
Immagino che la risposta di Blair funzioni nei casi in cui il socket è stato chiuso correttamente dall'altra parte (ovvero hanno inviato i messaggi di disconnessione corretti), ma non nel caso in cui l'altra estremità ha appena smesso di ascoltare in modo scortese.
C'è un'intestazione di formato abbastanza fisso all'inizio del tuo messaggio, che puoi iniziare inviando, prima che l'intera risposta sia pronta? per esempio. un doctype XML? Inoltre, sei in grado di farla franca inviando alcuni spazi extra in alcuni punti del messaggio - solo alcuni dati nulli che puoi produrre per assicurarti che il socket sia ancora aperto?
Il modulo select contiene ciò di cui avrai bisogno. Se hai solo bisogno del supporto Linux e hai un kernel sufficientemente recente, select.epoll()
dovrebbe darti le informazioni di cui hai bisogno. La maggior parte dei sistemi Unix supporterà select.poll()
.
Se hai bisogno di supporto multipiattaforma, il modo standard è usare select.select()
per verificare se il socket è contrassegnato come avente dati disponibili per la lettura. Se lo è, ma recv()
restituisce zero byte, l'altra estremità ha riagganciato.
Ho sempre trovato buona la Guida alla programmazione di rete di Beej (nota che è scritta per C, ma è generalmente applicabile alle operazioni socket standard), mentre la Guida alla programmazione socket ha una discreta panoramica di Python.
Modifica :Quello che segue è un esempio di come un semplice server potrebbe essere scritto per mettere in coda i comandi in entrata ma interrompere l'elaborazione non appena rileva che la connessione è stata chiusa all'estremità remota.
import select
import socket
import time
# Create the server.
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((socket.gethostname(), 7557))
serversocket.listen(1)
# Wait for an incoming connection.
clientsocket, address = serversocket.accept()
print 'Connection from', address[0]
# Control variables.
queue = []
cancelled = False
while True:
# If nothing queued, wait for incoming request.
if not queue:
queue.append(clientsocket.recv(1024))
# Receive data of length zero ==> connection closed.
if len(queue[0]) == 0:
break
# Get the next request and remove the trailing newline.
request = queue.pop(0)[:-1]
print 'Starting request', request
# Main processing loop.
for i in xrange(15):
# Do some of the processing.
time.sleep(1.0)
# See if the socket is marked as having data ready.
r, w, e = select.select((clientsocket,), (), (), 0)
if r:
data = clientsocket.recv(1024)
# Length of zero ==> connection closed.
if len(data) == 0:
cancelled = True
break
# Add this request to the queue.
queue.append(data)
print 'Queueing request', data[:-1]
# Request was cancelled.
if cancelled:
print 'Request cancelled.'
break
# Done with this request.
print 'Request finished.'
# If we got here, the connection was closed.
print 'Connection closed.'
serversocket.close()
Per usarlo, esegui lo script e in un altro terminale telnet su localhost, porta 7557. L'output di un esempio eseguito l'ho eseguito, accodando tre richieste ma chiudendo la connessione durante l'elaborazione della terza:
Connection from 127.0.0.1
Starting request 1
Queueing request 2
Queueing request 3
Request finished.
Starting request 2
Request finished.
Starting request 3
Request cancelled.
Connection closed.
alternativa a epoll
Un'altra modifica: Ho elaborato un altro esempio usando select.epoll
per monitorare gli eventi. Non penso che offra molto rispetto all'esempio originale poiché non riesco a vedere un modo per ricevere un evento quando l'estremità remota riattacca. Devi ancora monitorare l'evento dei dati ricevuti e verificare la presenza di messaggi di lunghezza zero (di nuovo, mi piacerebbe essere smentito su questa affermazione).
import select
import socket
import time
port = 7557
# Create the server.
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((socket.gethostname(), port))
serversocket.listen(1)
serverfd = serversocket.fileno()
print "Listening on", socket.gethostname(), "port", port
# Make the socket non-blocking.
serversocket.setblocking(0)
# Initialise the list of clients.
clients = {}
# Create an epoll object and register our interest in read events on the server
# socket.
ep = select.epoll()
ep.register(serverfd, select.EPOLLIN)
while True:
# Check for events.
events = ep.poll(0)
for fd, event in events:
# New connection to server.
if fd == serverfd and event & select.EPOLLIN:
# Accept the connection.
connection, address = serversocket.accept()
connection.setblocking(0)
# We want input notifications.
ep.register(connection.fileno(), select.EPOLLIN)
# Store some information about this client.
clients[connection.fileno()] = {
'delay': 0.0,
'input': "",
'response': "",
'connection': connection,
'address': address,
}
# Done.
print "Accepted connection from", address
# A socket was closed on our end.
elif event & select.EPOLLHUP:
print "Closed connection to", clients[fd]['address']
ep.unregister(fd)
del clients[fd]
# Error on a connection.
elif event & select.EPOLLERR:
print "Error on connection to", clients[fd]['address']
ep.modify(fd, 0)
clients[fd]['connection'].shutdown(socket.SHUT_RDWR)
# Incoming data.
elif event & select.EPOLLIN:
print "Incoming data from", clients[fd]['address']
data = clients[fd]['connection'].recv(1024)
# Zero length = remote closure.
if not data:
print "Remote close on ", clients[fd]['address']
ep.modify(fd, 0)
clients[fd]['connection'].shutdown(socket.SHUT_RDWR)
# Store the input.
else:
print data
clients[fd]['input'] += data
# Run when the client is ready to accept some output. The processing
# loop registers for this event when the response is complete.
elif event & select.EPOLLOUT:
print "Sending output to", clients[fd]['address']
# Write as much as we can.
written = clients[fd]['connection'].send(clients[fd]['response'])
# Delete what we have already written from the complete response.
clients[fd]['response'] = clients[fd]['response'][written:]
# When all the the response is written, shut the connection.
if not clients[fd]['response']:
ep.modify(fd, 0)
clients[fd]['connection'].shutdown(socket.SHUT_RDWR)
# Processing loop.
for client in clients.keys():
clients[client]['delay'] += 0.1
# When the 'processing' has finished.
if clients[client]['delay'] >= 15.0:
# Reverse the input to form the response.
clients[client]['response'] = clients[client]['input'][::-1]
# Register for the ready-to-send event. The network loop uses this
# as the signal to send the response.
ep.modify(client, select.EPOLLOUT)
# Processing delay.
time.sleep(0.1)
Nota :Questo rileva solo gli arresti corretti. Se l'estremità remota smette di ascoltare senza inviare i messaggi corretti, non lo saprai finché non provi a scrivere e ricevi un errore. La verifica è lasciata come esercizio per il lettore. Inoltre, probabilmente vorrai eseguire un controllo degli errori sull'intero ciclo in modo che il server stesso venga arrestato correttamente se qualcosa si rompe al suo interno.
L'opzione socket KEEPALIVE consente di rilevare questo tipo di scenari di "interruzione della connessione senza avvisare l'altra estremità".
Dovresti impostare l'opzione SO_KEEPALIVE a livello SOL_SOCKET. In Linux, puoi modificare i timeout per socket utilizzando TCP_KEEPIDLE (secondi prima dell'invio di sonde keepalive), TCP_KEEPCNT (sonde keepalive non riuscite prima di dichiarare morta l'altra estremità) e TCP_KEEPINTVL (intervallo in secondi tra le sonde keepalive).
In Python:
import socket
...
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
s.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 1)
s.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1)
s.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 5)
netstat -tanop
mostrerà che il socket è in modalità keepalive:
tcp 0 0 127.0.0.1:6666 127.0.0.1:43746 ESTABLISHED 15242/python2.6 keepalive (0.76/0/0)
mentre tcpdump
mostrerà le sonde keepalive:
01:07:08.143052 IP localhost.6666 > localhost.43746: . ack 1 win 2048 <nop,nop,timestamp 848683438 848683188>
01:07:08.143084 IP localhost.43746 > localhost.6666: . ack 1 win 2050 <nop,nop,timestamp 848683438 848682438>
01:07:09.143050 IP localhost.6666 > localhost.43746: . ack 1 win 2048 <nop,nop,timestamp 848683688 848683438>
01:07:09.143083 IP localhost.43746 > localhost.6666: . ack 1 win 2050 <nop,nop,timestamp 848683688 848682438>