Détecter socket raccrocher sans envoyer ou recevoir?

J'écris un serveur TCP qui peut prendre 15 secondes ou plus pour commencer à générer le corps d'une réponse à certaines requêtes. Certains clients aiment fermer la connexion à leur fin si la réponse prend plus de quelques secondes pour terminer.

Comme la génération de la réponse nécessite beaucoup de processeur, je préfère arrêter la tâche dès que le client ferme la connexion. À l'heure actuelle, je ne trouve pas cela jusqu'à ce que j'envoie la première charge utile et reçois divers raccrocher erreur.

Comment puis-je détecter que le pair a fermé la connexion sans envoyer ou recevoir de données? Cela signifie pour recv que toutes les données restent dans le noyau, ou pour send qu'aucune donnée n'est réellement transmise.

23
demandé sur Matt Joiner 2011-04-16 16:32:46

6 réponses

J'ai eu un problème récurrent de communication avec un équipement qui avait des liens TCP séparés pour envoyer et recevoir. Le problème de base est que la pile TCP ne vous dit généralement pas qu'un socket est fermé lorsque vous essayez simplement de lire - vous devez essayer d'écrire pour vous dire que l'autre extrémité du lien a été supprimée. En partie, C'est juste comment TCP a été conçu (la lecture est passive).

Je suppose que la réponse de Blair fonctionne dans les cas où le socket a été bien arrêté à l'autre extrémité (c'est-à-dire qu'ils ont envoyé les messages de déconnexion appropriés), mais pas dans le cas où l'autre extrémité a simplement cessé d'écouter.

Y a-t-il un en-tête au format assez fixe au début de votre message, que vous pouvez commencer par envoyer, avant que toute la réponse ne soit prête? par exemple un doctype XML? Êtes - vous également capable de vous en sortir en envoyant des espaces supplémentaires à certains points du message-juste quelques données nulles que vous pouvez produire pour être sûr que le socket est toujours ouvert?

16
répondu asc99c 2011-12-08 17:12:33

Le moduleselect contient ce dont vous aurez besoin. Si vous avez seulement besoin du support Linux et que vous avez un noyau suffisamment récent, select.epoll() devrait vous donner les informations dont vous avez besoin. La plupart des systèmes Unix prennent en charge select.poll().

Si vous avez besoin d'un support multi-plateforme, la méthode standard consiste à utiliser select.select() pour vérifier si le socket est marqué comme ayant des données disponibles à lire. Si c'est le cas, mais que recv() renvoie zéro octet, l'autre extrémité a raccroché.

J'ai toujours trouvé le Guide du réseau de Beej Programmation bon (notez qu'il est écrit pour C, mais est généralement applicable aux opérations de socket standard), tandis que le mode D'emploi de programmation Socket a un aperçu Python décent.

Edit : ce qui suit est un exemple de la façon dont un simple serveur peut être écrit pour mettre en file d'attente les commandes entrantes mais quitter le traitement dès qu'il trouve la connexion a été fermée à l'extrémité distante.

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

Pour l'utiliser, exécutez le script et dans un autre terminal telnet localhost, port 7557. La sortie d'un exemple d'exécution que j'ai fait, mettant en file d'attente trois requêtes mais fermant la connexion pendant le traitement de la troisième:

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.

EPoll alternative

Une autre édition: j'ai travaillé un autre exemple en utilisant select.epoll pour surveiller les événements. Je ne pense pas que cela offre beaucoup plus que l'exemple original car je ne peux pas voir un moyen de recevoir un événement lorsque l'extrémité distante raccroche. Vous devez toujours surveiller l'événement de données reçues et vérifier les messages de longueur nulle (encore une fois, je l'amour d'être prouvé faux sur cette déclaration).

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)

Remarque : cela ne détecte que les arrêts appropriés. Si l'extrémité distante arrête juste d'écouter sans envoyer les messages appropriés, vous ne saurez pas jusqu'à ce que vous essayez d'écrire et d'obtenir une erreur. La vérification de cela est laissée comme un exercice pour le lecteur. En outre, vous voulez probablement effectuer une vérification d'erreur sur la boucle globale afin que le serveur lui-même s'arrête gracieusement si quelque chose se casse à l'intérieur.

25
répondu Blair 2011-12-08 21:51:55

L'option socket KEEPALIVE permet de détecter ce genre de scénarios "supprimer la connexion sans dire l'autre extrémité".

Vous devez définir L'option SO_KEEPALIVE au niveau SOL_SOCKET. Sous Linux, vous pouvez modifier les délais d'attente par socket en utilisant TCP_KEEPIDLE (secondes avant l'envoi des sondes keepalive), TCP_KEEPCNT (échec des sondes keepalive avant de déclarer l'autre extrémité morte) et TCP_KEEPINTVL (intervalle en secondes entre les sondes keepalive).

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 indique que le socket est en mode keepalive:

tcp        0      0 127.0.0.1:6666          127.0.0.1:43746         ESTABLISHED 15242/python2.6     keepalive (0.76/0/0)

Alors que {[4] } affichera les sondes 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>
12
répondu ninjalj 2011-12-09 00:15:13

Après avoir lutté avec un problème similaire, j'ai trouvé une solution qui fonctionne pour moi, mais cela nécessite d'appeler recv() en mode non bloquant et d'essayer de lire les données, comme ceci:

bytecount=recv(connectionfd,buffer,1000,MSG_NOSIGNAL|MSG_DONTWAIT);

Le nosignal lui dit de ne pas terminer le programme en cas d'erreur, et le dontwait lui dit de ne pas bloquer. Dans ce mode, recv() renvoie l'un des 3 types de réponses possibles:

  • -1 s'il n'y a pas de données à lire ou d'autres erreurs.
  • 0 si l'autre extrémité a raccroché joliment
  • 1 ou plus s'il y avait des données en attente.

Donc, en vérifiant la valeur de retour, si c'est 0, alors cela signifie que l'autre extrémité raccroché. Si c'est -1, alors vous devez vérifier la valeur de errno. Si errno est égal à EAGAIN ou EWOULDBLOCK, la connexion est toujours considérée comme vivante par la pile tcp du serveur.

Cette solution vous obligerait à placer l'appel à recv() dans votre boucle de traitement de données intensive - ou quelque part dans votre code où il obtiendrait appelé 10 fois par seconde ou tout ce que vous voulez, donnant ainsi votre connaissance du programme d'un pair qui raccroche.

Bien sûr, cela ne servira à rien pour un pair qui s'en va sans faire la séquence d'arrêt de connexion correcte, mais tout client tcp correctement implémenté mettra fin à la connexion correctement.

Notez également que si le client envoie un tas de données puis raccroche, recv() devra probablement lire toutes ces données hors du tampon avant d'obtenir la lecture vide.

3
répondu Jesse Gordon 2013-02-24 06:45:05

Vous pouvez sélectionner avec un délai d'attente de zéro, et lire avec L'indicateur MSG_PEEK.

Je pense que vous devriez vraiment expliquer ce que vous entendez précisément par "ne pas lire", et pourquoi les autres réponses ne sont pas satisfaisantes.

-1
répondu shodanex 2011-12-05 14:39:31

Extraire Sélectionner module.

-2
répondu Rumple Stiltskin 2011-04-16 12:56:23