Wykryć zawieszenie gniazda bez wysyłania lub odbierania?

Piszę serwer TCP, który może zająć 15 sekund lub więcej, aby rozpocząć generowanie ciała odpowiedzi na określone żądania. Niektórzy klienci lubią zamykać połączenie na końcu, jeśli odpowiedź trwa dłużej niż kilka sekund.

Ponieważ generowanie odpowiedzi jest bardzo obciążające CPU, wolałbym wstrzymać zadanie, gdy tylko klient zamknie połączenie. W chwili obecnej nie dowiaduję się o tym, dopóki nie wyślę pierwszego ładunku i nie otrzymam różnych zawieszeń błędy.

Jak mogę wykryć, że peer zamknął połączenie bez wysyłania lub odbierania jakichkolwiek danych? Oznacza to dla recv, że wszystkie dane pozostają w jądrze, lub dla send, że żadne dane nie są faktycznie przesyłane.

Author: Matt Joiner, 2011-04-16

6 answers

Miałem powtarzający się problem z komunikacją ze sprzętem, który miał oddzielne łącza TCP do wysyłania i odbierania. Podstawowy problem polega na tym, że stos TCP zazwyczaj nie mówi, że gniazdo jest zamknięte, gdy próbujesz tylko czytać - musisz spróbować i napisać, aby powiedziano, że drugi koniec łącza został upuszczony. Częściowo tak właśnie został zaprojektowany TCP (odczyt jest pasywny).

Zgaduję, że odpowiedź działa w przypadkach, gdy gniazdo zostało ładnie zamknięte na drugim końcu (tzn. wysłali odpowiednie komunikaty o rozłączeniu), ale nie w przypadku, gdy drugi koniec nieumiejętnie przestał nasłuchiwać.

Czy na początku wiadomości jest nagłówek o dość stałym formacie, który możesz zacząć od wysłania, zanim cała odpowiedź będzie gotowa? np. XML doctype? Czy jesteś w stanie uciec od wysyłania dodatkowych spacji w niektórych punktach wiadomości - tylko niektóre dane null, które możesz wypisać, aby upewnić się, że gniazdo jest nadal otwarte?

 16
Author: asc99c,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-08 17:12:33

Moduł select Zawiera to, czego potrzebujesz. Jeśli potrzebujesz tylko wsparcia dla Linuksa i masz wystarczająco najnowsze jądro, select.epoll() powinno dać ci potrzebne informacje. Większość systemów uniksowych obsługuje select.poll().

Jeśli potrzebujesz wsparcia dla wielu platform, standardowym sposobem jest użycie select.select(), aby sprawdzić, czy gniazdo jest oznaczone jako posiadające dane do odczytu. Jeśli tak, ale recv() zwraca zero bajtów, drugi koniec się rozłączył.

I 've always found Beej' s Guide to Network Programowanie dobre (zauważ, że jest napisane dla języka C, ale ogólnie odnosi się do standardowych operacji na gniazdach), podczas gdy Programowanie gniazd ma przyzwoity przegląd Pythona.

Edit: poniżej przedstawiono przykład, jak prosty serwer może być zapisany do kolejki przychodzących poleceń, ale kończy przetwarzanie, gdy tylko stwierdzi, że połączenie zostało zamknięte na odległym końcu.

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

Aby go użyć, uruchom skrypt i w innym terminalu telnet do localhost, port 7557. Wyjście z przykładowego uruchomienia zrobiłem, w kolejce trzy żądania, ale zamknięcie połączenia podczas przetwarzania trzeciego:

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

Kolejna edycja: opracowałem kolejny przykład, używając select.epoll do monitorowania zdarzeń. Nie sądzę, aby oferował wiele w stosunku do oryginalnego przykładu, ponieważ nie widzę sposobu na odebranie zdarzenia, gdy zdalny koniec się zawiesza. Nadal musisz monitorować dane odebrane Zdarzenie i sprawdzić wiadomości o zerowej długości (ponownie, i ' D miłość do udowodnienia błędu w tym stwierdzeniu).

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)

Uwaga : wykrywa tylko właściwe wyłączenia. Jeśli zdalny koniec po prostu przestaje nasłuchiwać bez wysyłania odpowiednich wiadomości, nie będziesz wiedział, dopóki nie spróbujesz napisać i otrzymasz błąd. Sprawdzanie tego pozostaje jako ćwiczenie dla czytelnika. Ponadto prawdopodobnie chcesz wykonać pewne sprawdzanie błędów w ogólnej pętli, aby sam serwer był wyłączany z wdziękiem, jeśli coś się w nim zepsuje.

 25
Author: Blair,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-08 21:51:55

Opcja socket KEEPALIVE pozwala wykryć tego rodzaju "upuść połączenie bez informowania drugiego końca".

Powinieneś ustawić opcję SO_KEEPALIVE na poziomie SOL_SOCKET. W Linuksie można modyfikować timeouty dla gniazda używając TCP_KEEPIDLE (sekundy przed wysłaniem sond keepalive), TCP_KEEPCNT (nieudane sondy keepalive przed zadeklarowaniem drugiego końca Martwego) i TCP_KEEPINTVL (interwał w sekundach między sondami keepalive).

W Pythonie:

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 pokazuje, że gniazdo jest w trybie keepalive:

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

While tcpdump pokaże sondy 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
Author: ninjalj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-09 00:15:13

Po zmaganiu się z podobnym problemem znalazłem rozwiązanie, które działa dla mnie, ale wymaga wywołania recv() w trybie nieblokującym i próby odczytania danych, jak to:

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

Nosignal mówi, aby nie przerywać programu w przypadku błędu, a dontwait mówi, aby nie blokował. W tym trybie recv() zwraca jeden z 3 możliwych typów odpowiedzi:

  • -1 Jeśli nie ma danych do odczytu lub innych błędów.
  • 0 jeśli drugi koniec się rozłączył ładnie
  • 1 lub więcej, jeśli czekały jakieś dane.

Więc sprawdzając wartość zwracaną, jeśli jest to 0, to oznacza to, że drugi koniec jest zawieszony. Jeśli jest to -1 to musisz sprawdzić wartość errno. Jeśli {[7] } jest równe EAGAIN lub EWOULDBLOCK, połączenie jest nadal uważane za żywe przez stos TCP serwera.

To rozwiązanie wymagałoby umieszczenia wywołania do recv() W pętli intensywnego przetwarzania danych - lub gdzieś w kodzie, gdzie dostałoby się wywoływane 10 razy na sekundę czy jak chcesz, dając w ten sposób swoją wiedzę programową rówieśnikowi, który się rozłącza.

To oczywiście nie będzie dobre dla peera, który odejdzie bez wykonania prawidłowej sekwencji zamykania połączenia, ale każdy poprawnie zaimplementowany Klient tcp poprawnie zakończy połączenie.

Zauważ również, że jeśli klient wyśle kilka danych, a następnie się rozłączy, recv() prawdopodobnie będzie musiał odczytać te dane z bufora, zanim otrzyma pusty odczyt.

 3
Author: Jesse Gordon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-24 06:45:05

Możesz wybrać z limitem czasu równym zero i odczytać z znacznikiem MSG_PEEK.

Myślę, że naprawdę powinieneś wyjaśnić, co dokładnie masz na myśli mówiąc "nie czytanie" i dlaczego druga odpowiedź nie jest satysfakcjonująca.

 -1
Author: shodanex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-05 14:39:31

Sprawdź Wybierz moduł.

 -2
Author: Rumple Stiltskin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-16 12:56:23