Подтвердить что ты не робот

Обнаружение зависания сокета без отправки или получения?

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

Я всегда находил Beej Guide to Network Programming хорошо (обратите внимание, что он написан для C, но, как правило, применим к стандартным операциям сокета), а Socket Programming How-To имеет достойный обзор Python.

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

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

Чтобы использовать его, запустите script и в другом терминальном 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.

альтернатива эпохи

Другое редактирование: Я разработал еще один пример, используя 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)

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

Ответ 3

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

Вы должны установить SO_KEEPALIVE на уровне SOL_SOCKET. В Linux вы можете изменить тайм-ауты на один сокет, используя TCP_KEEPIDLE (за несколько секунд до отправки зондов keepalive), TCP_KEEPCNT (недействительные контрольные зонды до объявления другого мертвого конца) и 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>

Ответ 4

После того, как я столкнулся с подобной проблемой, я нашел решение, которое работает для меня, но для него требуется вызов 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(), вероятно, придется прочитать эти данные из буфера, прежде чем он получит пустое чтение.

Ответ 5

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

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

Ответ 6

Отъезд select.