Socket-Aufhängung ohne Senden oder Empfangen erkennen?

Lesezeit: 12 Minuten

Benutzer-Avatar
Matt Tischler

Ich schreibe einen TCP-Server, der 15 Sekunden oder länger brauchen kann, um mit dem Generieren des Hauptteils einer Antwort auf bestimmte Anforderungen zu beginnen. Einige Clients schließen die Verbindung gerne an ihrem Ende, wenn die Antwort länger als ein paar Sekunden dauert.

Da das Generieren der Antwort sehr CPU-intensiv ist, würde ich es vorziehen, die Aufgabe anzuhalten, sobald der Client die Verbindung schließt. Derzeit finde ich das erst heraus, wenn ich die erste Payload sende und diverse Hang-up-Errors erhalte.

Wie kann ich erkennen, dass der Peer die Verbindung geschlossen hat, ohne Daten zu senden oder zu empfangen? Das bedeutet für recv dass alle Daten im Kernel bleiben, oder z send dass tatsächlich keine Daten übermittelt werden.

  • Können Sie setsockopt(...SOL_SOCKET, SO_KEEPALIVE...) von Python?

    – ninjalj

    2. Dezember 2011 um 0:55 Uhr

  • Was für ein Server ist das: http, Vanilla-TCP-Sockets oder ein anderes TCP-basiertes Protokoll?

    – Fon

    2. Dezember 2011 um 21:38 Uhr

  • @Foon: Es ist Vanilla TCP.

    – Matt Tischler

    3. Dezember 2011 um 7:28 Uhr

  • @Bashwork: Ich kann Twisted nicht ausstehen, es kehrt den Kontrollfluss vollständig um und macht Ihren Code ausführlich und unlesbar.

    – Matt Tischler

    8. Dezember 2011 um 22:44 Uhr

  • Nicht zu flammen, aber das klingt sehr übertrieben. Nichts auf dieser Seite war ausführlich, unlesbar oder “invertiert”.

    – Bashwork

    9. Dezember 2011 um 15:27 Uhr

Benutzer-Avatar
Blair

Das auswählen Modul enthält, was Sie brauchen. Wenn Sie nur Linux-Unterstützung benötigen und einen ausreichend aktuellen Kernel haben, select.epoll() sollte Ihnen die Informationen geben, die Sie benötigen. Die meisten Unix-Systeme werden unterstützt select.poll().

Wenn Sie plattformübergreifende Unterstützung benötigen, verwenden Sie standardmäßig select.select() um zu überprüfen, ob der Socket so markiert ist, dass Daten zum Lesen verfügbar sind. Wenn ja, aber recv() liefert null Bytes zurück, das andere Ende hat aufgelegt.

Ich habe immer gefunden Beejs Leitfaden zur Netzwerkprogrammierung gut (beachten Sie, dass es für C geschrieben wurde, aber allgemein auf Standard-Socket-Operationen anwendbar ist), während die Anleitung zur Socket-Programmierung hat eine anständige Python-Übersicht.

Bearbeiten: Das Folgende ist ein Beispiel dafür, wie ein einfacher Server so geschrieben werden könnte, dass er eingehende Befehle in eine Warteschlange stellt, aber die Verarbeitung beendet, sobald er feststellt, dass die Verbindung am entfernten Ende geschlossen wurde.

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

Um es zu verwenden, führen Sie das Skript aus und in einem anderen Terminal telnet zu localhost, Port 7557. Die Ausgabe eines von mir durchgeführten Beispiellaufs, bei dem drei Anforderungen in die Warteschlange gestellt wurden, die Verbindung jedoch während der Verarbeitung der dritten geschlossen wurde:

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

Noch eine Bearbeitung: Ich habe ein weiteres Beispiel mit ausgearbeitet select.epoll Ereignisse zu überwachen. Ich glaube nicht, dass es viel mehr bietet als das ursprüngliche Beispiel, da ich keine Möglichkeit sehe, ein Ereignis zu empfangen, wenn das entfernte Ende auflegt. Sie müssen immer noch das Ereignis “Daten empfangen” überwachen und nach Nachrichten mit der Länge null suchen (ich würde mich auch bei dieser Aussage gerne als falsch erweisen).

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)

Notiz: Dies erkennt nur ordnungsgemäße Abschaltungen. Wenn das entfernte Ende einfach aufhört zuzuhören, ohne die richtigen Nachrichten zu senden, werden Sie es nicht wissen, bis Sie versuchen zu schreiben und einen Fehler erhalten. Dies zu überprüfen, bleibt dem Leser als Übung überlassen. Außerdem möchten Sie wahrscheinlich eine Fehlerprüfung für die Gesamtschleife durchführen, damit der Server selbst ordnungsgemäß heruntergefahren wird, wenn etwas in ihm kaputt geht.

  • Soweit mir bekannt ist, ist dies leider die einzige zuverlässige, plattformübergreifende Möglichkeit, nach einer geschlossenen Verbindung zu suchen (im Wesentlichen so, wie Sockets funktionieren). Ich möchte jedoch, dass mir jemand das Gegenteil beweist – ich bin in der Vergangenheit auf diese Art von Problem gestoßen. Die allgemeine Lösung, die ich verwendet habe, besteht darin, alle tatsächlichen Daten zu puffern, die aus der erhalten wurden recv() Aufforderung zur Bearbeitung in der Zukunft. Ich habe meine Antwort mit einem Beispiel dafür aktualisiert. Wenn Ihre Situation bedeutet, dass dies nicht verwendet werden kann, würde ich vorschlagen, Ihre Frage mit weiteren Details zu aktualisieren, um zu sehen, ob jemand eine geeignetere Lösung anbieten kann.

    – Blair

    17. April 2011 um 3:41 Uhr

  • War die Erwähnung von epoll versehentlich? Cross-Plattform ist hier nicht wirklich erforderlich, ich bin mehr als glücklich, auf Linux umzusteigen, nur wenn es die Dinge zuverlässiger macht.

    – Matt Tischler

    21. April 2011 um 13:01 Uhr

  • +1, select ist der richtige Weg. Puffern Sie alle Daten, die Sie beim Anruf erhalten recv und bei Null aussteigen (oder Fehler, den Sie erhalten können, wenn die Verbindung nicht ordnungsgemäß beendet wurde)

    – Hasturkun

    8. Dezember 2011 um 17:32 Uhr

  • @MattJoiner, ich habe ein Beispiel mit epoll hinzugefügt. Leider scheint es keinen wirklichen Vorteil zu bieten – soweit ich sehen kann, müssen Sie immer noch nach eingehenden Nachrichten der Länge Null suchen, um festzustellen, ob der Client die Verbindung schließt.

    – Blair

    8. Dezember 2011 um 21:41 Uhr

  • @MattJoiner: Wenn Sie in dieser Zeit wirklich keine Daten erwarten, müssen Sie sich nur darum kümmern recv Rückkehr 0 oder -1 und müssen nicht puffern. Um sicherzustellen, dass Sie auch für den Fall geweckt werden, dass das Remote-Ende einfach verschwindet (im Gegensatz zu einem ordnungsgemäßen oder nicht ordnungsgemäßen Herunterfahren), verwenden Sie Keepalive.

    – Hasturkun

    9. Dezember 2011 um 15:57 Uhr

Ich hatte ein wiederkehrendes Problem bei der Kommunikation mit Geräten, die separate TCP-Verbindungen zum Senden und Empfangen hatten. Das grundlegende Problem besteht darin, dass der TCP-Stack Ihnen im Allgemeinen nicht mitteilt, dass ein Socket geschlossen ist, wenn Sie nur versuchen zu lesen. Sie müssen versuchen, zu schreiben, um zu erfahren, dass das andere Ende der Verbindung unterbrochen wurde. Teilweise wurde TCP genau so entworfen (Lesen ist passiv).

Ich vermute, Blairs Antwort funktioniert in den Fällen, in denen der Socket am anderen Ende ordnungsgemäß heruntergefahren wurde (dh sie haben die richtigen Trennungsnachrichten gesendet), aber nicht in dem Fall, in dem das andere Ende unhöflicherweise einfach aufgehört hat zuzuhören.

Gibt es am Anfang Ihrer Nachricht einen Header mit ziemlich festem Format, den Sie mit dem Senden beginnen können, bevor die gesamte Antwort fertig ist? zB ein XML-Doctype? Können Sie auch damit durchkommen, an einigen Stellen in der Nachricht einige zusätzliche Leerzeichen zu senden – nur einige Nulldaten, die Sie ausgeben können, um sicherzustellen, dass der Socket noch offen ist?

Die KEEPALIVE-Option des Sockets ermöglicht es, diese Art von Szenarios zu erkennen, bei denen die Verbindung unterbrochen wird, ohne das andere Ende zu benachrichtigen.

Sie sollten die SO_KEEPALIVE-Option auf SOL_SOCKET-Ebene festlegen. Unter Linux können Sie die Timeouts pro Socket mit TCP_KEEPIDLE (Sekunden vor dem Senden von Keepalive-Prüfungen), TCP_KEEPCNT (fehlgeschlagene Keepalive-Prüfungen, bevor das andere Ende für tot erklärt wird) und TCP_KEEPINTVL (Intervall in Sekunden zwischen Keepalive-Prüfungen) ändern.

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 zeigt an, dass sich der Socket im Keepalive-Modus befindet:

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

während tcpdump zeigt die Keepalive-Sonden:

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>

  • Ich bin mir nicht sicher, ob Keep Alive die Verbindung mit einer ausreichend hohen Frequenz testet? Aus dem Gedächtnis sendet Keep Alive alle paar Minuten eine Nachricht, das ist nicht genug.

    – Matt Tischler

    7. Juli 2012 um 0:05 Uhr

  • @MattJoiner: Unter Linux ist es konfigurierbar.

    – ninjalj

    16. Juli 2012 um 18:47 Uhr

  • Danke, es hilft. Ähnliche Antwort hier: stackoverflow.com/a/14855726/821594

    – Stiel

    18. Dezember 2013 um 14:36 ​​Uhr

Benutzer-Avatar
Jesse Gordon

Nachdem ich mit einem ähnlichen Problem zu kämpfen hatte, habe ich eine Lösung gefunden, die für mich funktioniert, aber einen Anruf erfordert recv() im nicht blockierenden Modus und beim Versuch, Daten zu lesen, wie folgt:

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

Das nosignal weist es an, das Programm bei einem Fehler nicht zu beenden, und das dontwait weist es an, nicht zu blockieren. In diesem Modus recv() gibt eine von 3 möglichen Arten von Antworten zurück:

  • -1 wenn es keine zu lesenden Daten oder andere Fehler gibt.
  • 0 wenn das andere Ende gut aufgelegt hat
  • 1 oder mehr, wenn einige Daten warteten.

Wenn Sie also den Rückgabewert überprüfen, bedeutet dies, dass das andere Ende aufgelegt hat, wenn er 0 ist. Wenn ja -1 Dann müssen Sie den Wert von überprüfen errno. Wenn errno ist gleich EAGAIN oder EWOULDBLOCK dann wird die Verbindung vom TCP-Stack des Servers immer noch als aktiv angesehen.

Diese Lösung würde erfordern, dass Sie den Anruf auf stellen recv() in Ihre intensive Datenverarbeitungsschleife — oder irgendwo in Ihrem Code, wo es 10 Mal pro Sekunde oder was auch immer Sie möchten aufgerufen wird, wodurch Ihr Programm weiß, dass ein Peer auflegt.

Dies wird natürlich einem Peer nichts nützen, der weggeht, ohne die korrekte Sequenz zum Beenden der Verbindung auszuführen, aber jeder richtig implementierte TCP-Client wird die Verbindung korrekt beenden.

Beachten Sie auch, dass, wenn der Client eine Reihe von Daten sendet und dann auflegt, recv() müssen diese Daten wahrscheinlich alle aus dem Puffer lesen, bevor sie leer gelesen werden.

Dieser Code ist sehr einfach, stellt die Verbindung für immer wieder her und erfasst Strg + C, um das Programm abzuschließen, das den Port schließt. Passen Sie den Port an Ihre Bedürfnisse an

import select
import socket
import time
import sys
import threading

#create socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('', 2105)
print('starting up on {} port {}'.format(*server_address))
sock.bind(server_address)
sock.listen(1)

#main loop
while True:
    #waits for a new connection
    print('waiting for a connection')
    connection, client_address = sock.accept()
    try:
        print('connection from', client_address)
        #connection loop
        while True:
            try:
                r, w, e = select.select((connection,), (), (), 0)
                if r:
                    data = connection.recv(16)
                    if len(data) == 0:
                        break
                    print data
                    #example, return to client received data
                    connection.sendall(data)

            except KeyboardInterrupt:
                connection.close()
                sys.exit()

            except Exception as e:
                pass

            #let the socket receive some data
            time.sleep(0.1)

    except Exception as e:
        print e

    finally:
        #clean up connection
        connection.close()

Benutzer-Avatar
Rumple Stiltskin

Kasse auswählen Modul.

Benutzer-Avatar
Schodanex

Sie können mit einem Timeout von Null auswählen und mit dem MSG_PEEK-Flag lesen.

Ich denke, Sie sollten wirklich erklären, was Sie genau mit “nicht lesen” meinen und warum die andere Antwort nicht zufriedenstellend ist.

  • Das geht seitdem nicht mehr recv gibt einen Wert ungleich Null zurück, wenn Daten warten, auch wenn der Peer die Verbindung bereits geschlossen hat.

    – Matt Tischler

    8. Dezember 2011 um 23:20 Uhr

1365340cookie-checkSocket-Aufhängung ohne Senden oder Empfangen erkennen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy