Tuve un problema recurrente al comunicarme con equipos que tenían enlaces TCP separados para enviar y recibir. El problema básico es que la pila TCP generalmente no le dice que un socket está cerrado cuando solo está tratando de leer:debe intentar escribir para que le digan que se cayó el otro extremo del enlace. En parte, así es como se diseñó TCP (la lectura es pasiva).
Supongo que la respuesta de Blair funciona en los casos en que el enchufe se ha cerrado correctamente en el otro extremo (es decir, han enviado los mensajes de desconexión adecuados), pero no en el caso en que el otro extremo ha dejado de escuchar de manera descortés.
¿Hay un encabezado de formato bastante fijo al comienzo de su mensaje, que puede comenzar a enviar, antes de que esté lista la respuesta completa? p.ej. un tipo de documento XML? Además, ¿puede salirse con la suya enviando algunos espacios adicionales en algunos puntos del mensaje, solo algunos datos nulos que puede generar para asegurarse de que el socket aún esté abierto?
El módulo de selección contiene lo que necesitará. Si solo necesita soporte para Linux y tiene un kernel lo suficientemente reciente, select.epoll()
debe darle la información que necesita. La mayoría de los sistemas Unix admitirán select.poll()
.
Si necesita soporte multiplataforma, la forma estándar es usar select.select()
para verificar si el socket está marcado como que tiene datos disponibles para leer. Si lo es, pero recv()
devuelve cero bytes, el otro extremo ha colgado.
Siempre me ha parecido buena la Guía de programación de redes de Beej (tenga en cuenta que está escrita para C, pero generalmente se aplica a las operaciones de socket estándar), mientras que el Procedimiento de programación de sockets tiene una descripción general decente de Python.
Editar :El siguiente es un ejemplo de cómo se podría escribir un servidor simple para poner en cola los comandos entrantes pero dejar de procesar tan pronto como encuentre que la conexión se ha cerrado en el extremo remoto.
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()
Para usarlo, ejecute el script y en otro terminal telnet a localhost, puerto 7557. El resultado de una ejecución de ejemplo lo hice, poniendo en cola tres solicitudes pero cerrando la conexión durante el procesamiento de la tercera:
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 de encuesta electrónica
Otra edición: He elaborado otro ejemplo usando select.epoll
para monitorear eventos. No creo que ofrezca mucho más que el ejemplo original, ya que no puedo ver una forma de recibir un evento cuando cuelga el extremo remoto. Todavía tiene que monitorear el evento de datos recibidos y verificar los mensajes de longitud cero (nuevamente, me encantaría estar equivocado en esta afirmación).
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 :Esto solo detecta apagados adecuados. Si el extremo remoto simplemente deja de escuchar sin enviar los mensajes adecuados, no lo sabrá hasta que intente escribir y obtenga un error. Verificar eso se deja como ejercicio para el lector. Además, probablemente desee realizar una comprobación de errores en el ciclo general para que el servidor se apague correctamente si algo se rompe en su interior.
La opción socket KEEPALIVE permite detectar este tipo de escenarios de "caer la conexión sin decirle al otro extremo".
Debe establecer la opción SO_KEEPALIVE en el nivel SOL_SOCKET. En Linux, puede modificar los tiempos de espera por socket usando TCP_KEEPIDLE (segundos antes de enviar sondas de mantenimiento), TCP_KEEPCNT (pruebas fallidas de mantenimiento de actividad antes de declarar el otro extremo inactivo) y TCP_KEEPINTVL (intervalo en segundos entre sondas de mantenimiento de actividad).
En 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
mostrará que el socket está en modo keepalive:
tcp 0 0 127.0.0.1:6666 127.0.0.1:43746 ESTABLISHED 15242/python2.6 keepalive (0.76/0/0)
mientras tcpdump
mostrará las sondas 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>