Виявлення відключення клієнта TCP


78

Скажімо, я працюю на простому сервері і accept()підключився до клієнта.

Який найкращий спосіб визначити, коли клієнт відключився? Зазвичай клієнт повинен надіслати команду закриття, але що, якщо він відключається вручну або взагалі втрачає мережеве підключення? Як сервер може це виявити чи впоратись?


Подивіться тут (для найгірших сценаріїв): tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html (Перевірка на наявність мертвих однолітків)
Blauohr

4
Оскільки існує так багато неправильних та оманливих відповідей, ось правильний: дотримуйтесь специфікації протоколу, який ви реалізуєте поверх TCP. У ньому повинно бути вказано, чи робиться це через очікування, помилки запису чи інший механізм. Якщо ви розробляєте протокол, обов’язково розробіть спосіб виявлення відключення клієнта, якщо це потрібно.
Девід Шварц,

Відповіді:


-3

select (із встановленою маскою читання) повернеться із сигналізованим дескриптором, але коли ви використовуєте ioctl * для перевірки кількості байтів, що очікують на зчитування, вона буде нульовою. Це ознака того, що розетка відключена.

Це чудова дискусія щодо різних методів перевірки відключення клієнта: Стівен Клірі, виявлення напіввідкритих (розірваних) з’єднань .

* для Windows використовуйте ioctlsocket.


79
Це абсолютно і позитивно НЕ є "ознакою відключення розетки". Це знак того, що в буфері прийому сокетів немає даних. Період. Це не одне і те ж, що відбувається за кілометр. У статті, яку ви цитуєте на підтримку своєї відповіді, навіть не згадується про цю техніку.
user207421

3
@MarkKCowan Дуже важко повірити. Дані навіть не повинні потрапляти в буфер прийому сокетів, поки вони не пройдуть перевірку контрольної суми. У вас є джерело або повторюваний експеримент для вашої претензії?
user207421

2
@MarkKCowan Це зафіксовано лише у помилці, яку ви цитували. Це не задокументовано у специфікації IOCTL. У будь-який час може бути прочитано нуль байтів, найчастіше тому, що одноранговий користувач нічого не надсилав. Це не правильна техніка.
user207421

2
@EJP не означає, що прочитане 0 байтів означає EOF (тобто рівний перервав з'єднання)? Якщо на сокеті нічого немає, і якщо ви спробуєте прочитати, це дасть вам помилку EWOULDBLOCK / EAGAIN, а не 0 байт читання.
простуляція

1
@Matthieu: Можете, будь ласка, вказати мені одну? Я не думаю, що ви зможете коли-небудь прочитати 0 байт у TCP на рівні програми (так, ви можете отримати його для ACK тощо, але це не поширюється на користувача сокета), що не означає EOF.
устуляція

126

У TCP існує лише один спосіб виявити впорядкований розрив зв'язку, і це отримання нуля як поверненого значення read()/recv()/recvXXX()при читанні.

Існує також лише один надійний спосіб виявити розірваний зв’язок: написавши на нього. Після достатньої кількості записів на розірване з’єднання, TCP зробить достатню кількість спроб та тайм-аутів, щоб знати, що воно розірване, і врешті-решт призведе write()/send()/sendXXX()до повернення -1 зі errno/WSAGetLastError()значенням ECONNRESET,або, в деяких випадках, “з’єднання минуло”. Зверніть увагу, що останній відрізняється від "тайм-ауту підключення", який може відбуватися у фазі підключення.

Слід також встановити розумний час очікування для читання та відмовитись від з’єднань, які не вдаються.

Відповідь тут про ioctl()і FIONREADє змагання дурниці. Все, що вам потрібно, це сказати вам, скільки байтів зараз знаходиться в буфері прийому сокетів, доступних для читання без блокування. Якщо клієнт протягом п'яти хвилин не надсилає вам нічого, що не означає роз'єднання, але це спричиняє FIONREADнуль. Не те саме: навіть близько.


2
@Jay Питання полягає в тому, як виявити відключення TCP, а не в тому, що спричиняє скидання з'єднання. Причин "скидання з'єднання" багато, і я не згоден, що жодна з них є "нормальною роботою". За визначенням це ненормальний стан.
user207421

2
@ user1055568 Один запис зазвичай буферизується і надсилається по мережі асинхронно, якщо він не дуже великий. Вам потрібно виконати достатньо записів, щоб усі внутрішні таймери та повторні спроби були вичерпані на початковій записі для виявлення помилки.
user207421

2
Якщо програма не продовжує видавати записи, немає гарантії, що вона видасть будь-які записи після розриву зв'язку. Хоча достатньо одного запису, виданого після збою з'єднань, з'єднання може вийти з ладу в будь-який час, і якщо ви коли-небудь припините писати на невизначений час, ви не знаєте, як видали навіть один запис після того, як з'єднання не вдалося.
Девід Шварц,

3
@EJP І я вже неодноразово говорив, що якщо програма чекає на select / epoll / kevent для готовності до читання, вона отримає попередження про читання, щоб виявити помилку. Ви заперечували це, неодноразово наполягаючи на тому, що він повинен робити більше записів. Ви нічого не сказали про читання, і насправді з epoll немає необхідності читати чи писати, оскільки epoll може безпосередньо сигналізувати про час очікування. Можливо, теж кевент.
user1055568

2
@ user1055568 Якщо ви виконуєте лише читання, ви нічого не робите з мережею, тому ви не зіткнетеся з будь-якими умовами помилок, якщо тільки рівний не зобов'язує зробити скидання. Якщо ви пишете, ви робите щось у мережі, тож, врешті-решт, ви гарантуєте виникнення помилки, якщо така є.
user207421

13

Щоб розширити це ще трохи:

Якщо у вас запущений сервер, вам або потрібно використовувати TCP_KEEPALIVE для моніторингу клієнтських з'єднань, або зробити щось подібне самостійно, або мати знання про дані / протокол, які ви використовуєте через з'єднання.

В основному, якщо з’єднання загине (тобто не буде належним чином закрито), сервер не помітить цього, поки не спробує щось написати клієнту, чого досягає для вас програма keepalive. Як варіант, якщо ви краще знаєте протокол, у будь-якому випадку ви можете просто від’єднатися на час очікування бездіяльності.


Сервер також повинен встановити розумний тайм-аут читання та відмовитись від з’єднань, які не вдаються до нього.
user207421

Перервати з’єднання, яке не вдається? Що робити, якщо тайм-аут відповідає за замовчуванням рекомендованому 200 мсек? Чи не слід це зробити назад до певного розумного часу очікування? Можливо, це спричинить для вас занадто багато переключення контексту? Досі розривати зв’язок, коли такий Timeoutнастільки низький, не є слушною порадою ...
Джей,

на Winsock2, keepalive проводить опитування кожні 5 секунд, і у мене є якийсь блокуючий виклик надсилання або відновлення, тоді чи буде Keepalive працювати належним чином? Крім того, які мінімальні обмеження для часу очікування та інтервалу збереження?
Anurag Daware

1
@EJP, що це за ОС? Типовий тайм-аут для читання для більшості ОС становив 0,5 - 5 секунд, коли я останній раз перевіряв ... rfc для tcp конкретно говорить, що у tcp за замовчуванням 0,2 секунди ....
Jay,

@Jay, я не знаю, про що ти. Значення за замовчуванням для SO_RCVTIMEO нескінченне для всіх операційних систем. Інакше всі отримували б час очікування для читання весь час. Ваші пропозиції щодо 200 мс тощо є абсурдними.
user207421

2

Якщо ви використовуєте перекриваються (тобто асинхронні) введення-виведення з підпрограмами завершення або портами завершення, вам буде повідомлено негайно (за умови, що у вас є непогашене зчитування), коли клієнтська сторона закриває з'єднання.


Не зовсім. Ви одразу отримаєте сповіщення про прочитання до кінця потоку. Це могло зайняти кінцевий час, якби клієнт перед закриттям мав значні дані в польоті.
user207421


0

TCP має в протоколі процедури "відкрито" та "закрито". Після "відкриття" з'єднання утримується до "закриття". Але є багато речей, які можуть зупинити потік даних ненормально. З огляду на це, методи визначення, чи можна використовувати посилання, сильно залежать від рівня програмного забезпечення між протоколом і прикладною програмою. Згадані вище зосереджуються на програмісті, який намагається використовувати сокет неінвазивним способом (читання або запис 0 байт), мабуть, найпоширеніші. Деякі шари в бібліотеках забезпечують "опитування" для програміста. Наприклад, виклики Win32 asych (затримка) можуть розпочати читання, яке повернеться без помилок і 0 байт, щоб сигналізувати про сокет, який більше неможливо прочитати (імовірно, процедура TCP FIN). Інші середовища можуть використовувати "події" як визначено в їх обгорткових шарах. Однозначної відповіді на це питання немає. Механізм виявлення, коли сокет не можна використовувати і повинен бути закритим, залежить від обгортки, що постачається в бібліотеках. Варто також зазначити, що самі сокети можуть бути повторно використані шарами під бібліотекою програм, тому розумно з’ясувати, як ваше середовище працює з інтерфейсом Berkley Sockets.


-1
"""
tcp_disconnect.py
Echo network data test program in python. This easily translates to C & Java.

A server program might want to confirm that a tcp client is still connected 
before it sends a data. That is, detect if its connected without reading from socket.
This will demonstrate how to detect a TCP client disconnect without reading data.

The method to do this:
1) select on socket as poll (no wait)
2) if no recv data waiting, then client still connected
3) if recv data waiting, the read one char using PEEK flag 
4) if PEEK data len=0, then client has disconnected, otherwise its connected.
Note, the peek flag will read data without removing it from tcp queue.

To see it in action: 0) run this program on one computer 1) from another computer, 
connect via telnet port 12345, 2) type a line of data 3) wait to see it echo, 
4) type another line, 5) disconnect quickly, 6) watch the program will detect the 
disconnect and exit.

John Masinter, 17-Dec-2008
"""

import socket
import time
import select

HOST = ''       # all local interfaces
PORT = 12345    # port to listen

# listen for new TCP connections
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(1)
# accept new conneciton
conn, addr = s.accept()
print 'Connected by', addr
# loop reading/echoing, until client disconnects
try:
    conn.send("Send me data, and I will echo it back after a short delay.\n")
    while 1:
        data = conn.recv(1024)                          # recv all data queued
        if not data: break                              # client disconnected
        time.sleep(3)                                   # simulate time consuming work
        # below will detect if client disconnects during sleep
        r, w, e = select.select([conn], [], [], 0)      # more data waiting?
        print "select: r=%s w=%s e=%s" % (r,w,e)        # debug output to command line
        if r:                                           # yes, data avail to read.
            t = conn.recv(1024, socket.MSG_PEEK)        # read without remove from queue
            print "peek: len=%d, data=%s" % (len(t),t)  # debug output
            if len(t)==0:                               # length of data peeked 0?
                print "Client disconnected."            # client disconnected
                break                                   # quit program
        conn.send("-->"+data)                           # echo only if still connected
finally:
    conn.close()

перевірка готовності сокета, але відсутність даних працює дуже добре для мого проекту. Це просте рішення
luc

3
@luc Це взагалі не працює. Це просте, неправильне, неприпустиме рішення. Це тест на обсяг даних, які можна прочитати без блокування, а не тест на відключення. Вам потрібно прочитати, щоб перевірити це. Якщо клієнт нічого не надсилає вам протягом п'яти хвилин, FIONREAD буде нульовим, але він все одно може бути підключений.
user207421

1
Це Python, але на тезі написано C ++
Greg Schmit

-1

У python ви можете зробити такий вираз:

try:
  conn.send("{you can send anything to check connection}")
except BrokenPipeError:
  print("Client has Disconnected")

Це працює, тому що коли клієнт / сервер закриває програму, python повертає пошкоджену помилку pip серверу або клієнту в залежності від того, хто саме відключився.


-3

Зробити це дуже просто: надійно і не брудно:

        Try
            Clients.Client.Send(BufferByte)
        Catch verror As Exception
            BufferString = verror.ToString
        End Try
        If BufferString <> "" Then
            EventLog.Text &= "User disconnected: " + vbNewLine
            Clients.Close()
        End If

Це не надійно. Він не розрізняє впорядкованого та безладного закриття, і навіть не працює, поки не відбулося щонайменше два надсилання, через буфер надсилання сокета.
user207421

-3

Я погрався з кількома рішеннями, але, здається, це найкраще працює для виявлення відключення хоста та / або клієнта в Windows. Він призначений для неблокуючих сокетів і походить із прикладу IBM .

char buf;
int length=recv(socket, &buf, 0, 0);
int nError=WSAGetLastError();
if(nError!=WSAEWOULDBLOCK&&nError!=0){
    return 0;
}   
if (nError==0){
    if (length==0) return 0;
}

Recv () нічого не робить на дроті, тому він не може ініціювати виявлення натягування кабелю тощо. Тільки send () може це зробити.
user207421

-3

Повернене значення отримання буде -1, якщо з'єднання втрачено, інакше це буде розмір буфера.

void ReceiveStream(void *threadid)
{
    while(true)
    {
        while(ch==0)
        {
            char buffer[1024];
            int newData;
            newData = recv(thisSocket, buffer, sizeof(buffer), 0);
            if(newData>=0)
            {
                std::cout << buffer << std::endl;
            }
            else
            {
                std::cout << "Client disconnected" << std::endl;
                if (thisSocket)
                {
                    #ifdef WIN32
                        closesocket(thisSocket);
                        WSACleanup();
                    #endif
                    #ifdef LINUX
                        close(thisSocket);
                    #endif
                }
                break;
            }
        }
        ch = 1;
        StartSocket();
    }
}

2
-1 повертається лише в тому випадку, якщо виникає помилка, а не в разі відключення. Я перевірив у Windows та Linux, що коли одноранговий мережево відключається, recv просто поверне буфер, заповнений нулями.
TekuConcept

@TekuConcept Невірно. Він поверне -1 з errno == ECONNRESET, і він взагалі нічого не зробить для буфера.
user207421

Згідно з інформаційною сторінкою, ви маєте рацію! Думаю, я пропустив, що рядок "Додаткові помилки можуть генеруватися та повертатися з базових модулів протоколу"
TekuConcept
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.