Обнаружить зависание сокета без отправки или получения? - PullRequest
25 голосов
/ 16 апреля 2011

Я пишу TCP-сервер, который может занять 15 секунд или более, чтобы начать генерировать тело ответа на определенные запросы. Некоторым клиентам нравится закрывать соединение по их окончании, если ответ занимает более нескольких секунд.

Поскольку генерация ответа очень загружает процессор, я бы предпочел остановить задачу, как только клиент закроет соединение. В настоящее время я не узнаю об этом, пока не отправлю первую полезную нагрузку и не получу различные ошибки зависания.

Как я могу определить, что одноранговый узел закрыл соединение без отправки или получения каких-либо данных? Для recv это означает, что все данные остаются в ядре, или для send, что данные фактически не передаются.

Ответы [ 6 ]

26 голосов
/ 16 апреля 2011

Модуль select содержит то, что вам нужно.Если вам нужна только поддержка Linux и у вас достаточно свежее ядро, select.epoll() предоставит вам необходимую информацию.Большинство систем Unix поддерживают select.poll().

. Если вам требуется межплатформенная поддержка, стандартным способом является использование select.select() для проверки, помечен ли сокет как имеющий данные, доступные для чтения.Если это так, но recv() возвращает ноль байтов, другой конец завис.

Я всегда находил Руководство Биджа по сетевому программированию хорошим (обратите внимание, он написан для C,но обычно применимо к стандартным операциям с сокетами), в то время как Руководство по программированию сокетов имеет хороший обзор Python.

Edit : Ниже приведен пример того, какпростой сервер может быть записан в очередь для входящих команд, но завершить обработку, как только он обнаружит, что соединение удалено на удаленном конце.

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

Чтобы использовать его, запустите скрипт и в другом терминале telnet дляlocalhost, порт 7557. Вывод из примера запуска, который я выполнил, поставил в очередь три запроса, но закрыл соединение во время обработки третьего:

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

Другойedit: Я разработал еще один пример, используя select.epoll для мониторинга событий.Я не думаю, что это предлагает больше по сравнению с оригинальным примером, так как я не вижу способа получить событие, когда удаленный конец зависает.Вам по-прежнему необходимо отслеживать событие получения данных и проверять наличие сообщений нулевой длины (опять же, я хотел бы, чтобы в этом утверждении было ошибочным).

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)

Примечание : это толькообнаруживает правильные отключения.Если удаленный конец просто перестает слушать, не отправляя правильные сообщения, вы не узнаете, пока не попробуете написать и получить ошибку.Проверка на это оставлена ​​в качестве упражнения для читателя.Кроме того, вы, вероятно, захотите выполнить некоторую проверку ошибок в общем цикле, чтобы сам сервер корректно завершил работу, если внутри него что-то сломалось.

17 голосов
/ 08 декабря 2011

Возникла повторяющаяся проблема связи с оборудованием, для которого были отправлены и получены отдельные каналы TCP.Основная проблема заключается в том, что стек TCP обычно не сообщает вам, что сокет закрыт, когда вы просто пытаетесь читать - вы должны попытаться написать, чтобы сообщить, что другой конец ссылки был отброшен.Частично, именно так был разработан TCP (чтение пассивно).

Я предполагаю, что ответ Блэра работает в тех случаях, когда сокет был выключен на другом конце (то есть они отправили правильное сообщение).сообщения о разъединении), но не в том случае, когда другой конец невежливо просто перестал слушать.

Есть ли заголовок с довольно фиксированным форматом в начале вашего сообщения, который вы можете начать, отправив перед целымответ готов?например, тип документа XML?Также вы можете отправлять лишние пробелы в некоторых точках сообщения - только некоторые нулевые данные, которые вы можете вывести, чтобы убедиться, что сокет все еще открыт?

12 голосов
/ 09 декабря 2011

Опция сокета KEEPALIVE позволяет обнаруживать такого рода сценарии «разорвать соединение, не сообщая о другом конце».

Вам следует установить опцию SO_KEEPALIVE на уровне SOL_SOCKET. В Linux вы можете изменить время ожидания для каждого сокета, используя TCP_KEEPIDLE (секунды перед отправкой тестов keepalive), TCP_KEEPCNT (сбой тестов keepalive перед объявлением другого конца мертвым) и TCP_KEEPINTVL (интервал в секундах между тестами keepalive).

В 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 покажет, что сокет находится в режиме keepalive:

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

, в то время как tcpdump покажет пробные сообщения активности:

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>
3 голосов
/ 09 февраля 2013

После борьбы с подобной проблемой я нашел решение, которое работает для меня, но оно требует вызова recv() в неблокирующем режиме и попытки чтения данных, например:

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

nosignal говорит ему не завершать программу при ошибке, а dontwait говорит ему не блокировать.В этом режиме recv() возвращает один из 3 возможных типов ответов:

  • -1, если нет данных для чтения или других ошибок.
  • 0, еслидругой конец прекрасно повесил трубку
  • 1 или более, если ожидали какие-то данные.

Таким образом, проверяя возвращаемое значение, если оно равно 0, это означает, что другой конецповесить трубку.Если это -1, тогда вы должны проверить значение errno.Если errno равно EAGAIN или EWOULDBLOCK, то соединение по-прежнему считается активным стеком tcp сервера.

Это решение потребует от вас вызова recv() в вашинтенсивный цикл обработки данных - или где-то в вашем коде, где он будет вызываться 10 раз в секунду, или как вам угодно, что дает вашей программе знания о пэре, который вешает трубку.

Это, конечно, не принесет пользыдля однорангового узла, который уходит, не выполнив правильную последовательность выключений соединения, но любой правильно реализованный tcp-клиент корректно прервет соединение.

Обратите внимание также, что если клиент отправляет кучу данных, то зависает, recv()вероятно, придется прочитать все эти данные из буфера, прежде чем он получит пустое чтение.

0 голосов
/ 05 декабря 2011

Вы можете выбрать с таймаутом, равным нулю, и читать с флагом MSG_PEEK.

Я думаю, что вы действительно должны объяснить, что именно вы подразумеваете под "не читать", и почему другой ответ не удовлетворяет.

0 голосов
/ 16 апреля 2011

Проверить выбрать модуль.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...