Миттєво виявляє відключення клієнта від сокета сервера


82

Як я можу виявити, що клієнт відключився від мого сервера?

У моєму AcceptCallBackметоді є такий код

static Socket handler = null;
public static void AcceptCallback(IAsyncResult ar)
{
  //Accept incoming connection
  Socket listener = (Socket)ar.AsyncState;
  handler = listener.EndAccept(ar);
}

Мені потрібно знайти спосіб якнайшвидше виявити, що клієнт відключився від handlerрозетки.

Я пробував:

  1. handler.Available;
  2. handler.Send(new byte[1], 0, SocketFlags.None);
  3. handler.Receive(new byte[1], 0, SocketFlags.None);

Вищевказані підходи працюють, коли ви підключаєтеся до сервера і хочете виявити, коли сервер відключається, але вони не працюють, коли ви є сервером і хочете виявити відключення клієнта.

Будь-яка допомога буде вдячна.


10
@Samuel: ТСР і теги з'єднання є дуже доречним до цього повідомлення в тому , що TCP підтримує з'єднання ( в той час як інші мережеві протоколи , такі як UDP не роблять).
Noldorin

3
Більше про рішення серцебиття з мого блогу: Виявлення напіввідкритих (скинутих) з’єднань
Стівен Клірі

Рішення , описане тут , працює добре для мене: stackoverflow.com/questions/1387459 / ...
RAWK

Відповіді:


110

Оскільки відсутні події, які можуть сигналізувати про відключення розетки, вам доведеться проводити опитування на прийнятній для вас частоті.

Використовуючи цей метод розширення, ви можете мати надійний метод виявлення відключення розетки.

static class SocketExtensions
{
  public static bool IsConnected(this Socket socket)
  {
    try
    {
      return !(socket.Poll(1, SelectMode.SelectRead) && socket.Available == 0);
    }
    catch (SocketException) { return false; }
  }
}

1
Це спрацювало. Дякую. Я змінив метод повернення! (Socket.Available == 0 && socket.Poll (1, SelectMode.SelectRead)); тому що я підозрюю сокет. Доступний швидше, ніж Socket.Poll ()

28
Цей метод не працює, якщо інший кінець з'єднання фактично не закриває / вимикає сокет. Відключений мережевий / силовий кабель не буде помічений до закінчення періоду очікування. Єдиний спосіб сповіщення про відключення - це функція серцебиття для постійної перевірки з'єднання.
Каспер Холдум,

7
@Smart Alec: насправді, вам слід використовувати приклад, як показано вище. Якщо змінити порядок, існує потенційна умова перегони: якщо socket.Availableповертає 0 і ви отримуєте пакет безпосередньо перед тим, як socket.Pollйого викликають, Pollповернеться true, а метод повернеться false, хоча сокет насправді все ще справний.
Groo

4
Це спрацьовує добре у 99% випадків, іноді це дає помилковий розрив зв'язку.
Matthew Finlay

5
Так само, як зауважив Метью Фінлей, іноді це повідомляє про помилкові відключення, оскільки між результатом методу опитування та перевіркою властивості Available все ще існує перегоновий стан. Пакет може бути майже готовим до читання, але поки що, отже, Доступний - 0 - але через мілісекунду є дані для читання. Кращий варіант - спробувати отримати один байт і прапор SocketFlags.Peek. Або застосуйте якусь форму серцебиття та підтримуйте статус зв’язку на вищому рівні. Або покладайтесь на обробку помилок у ваших методах надсилання / отримання (і зворотних викликів, якщо використовуються асинхронні версії).
mbargiel

22

Хтось згадав про можливість KeepAlive TCP Socket. Тут це красиво описано:

http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html

Я використовую його таким чином: після підключення сокета я викликаю цю функцію, яка встановлює keepAlive. keepAliveTimeПараметр визначає час очікування в мілісекундах, при відсутності активності до першого підтримки активності пакета не передаються. keepAliveIntervalПараметр визначає інтервал, в мілісекундах, між тим, коли послідовні Keep-Alive пакети надсилаються , якщо підтвердження не отримано.

    void SetKeepAlive(bool on, uint keepAliveTime, uint keepAliveInterval)
    {
        int size = Marshal.SizeOf(new uint());

        var inOptionValues = new byte[size * 3];

        BitConverter.GetBytes((uint)(on ? 1 : 0)).CopyTo(inOptionValues, 0);
        BitConverter.GetBytes((uint)keepAliveTime).CopyTo(inOptionValues, size);
        BitConverter.GetBytes((uint)keepAliveInterval).CopyTo(inOptionValues, size * 2);

        socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
    }

Я також використовую асинхронне читання:

socket.BeginReceive(packet.dataBuffer, 0, 128,
                    SocketFlags.None, new AsyncCallback(OnDataReceived), packet);

А при зворотному виклику тут виявляється тайм-аут SocketException, який збільшується, коли сокет не отримує сигнал ACK після збереження пакета.

public void OnDataReceived(IAsyncResult asyn)
{
    try
    {
        SocketPacket theSockId = (SocketPacket)asyn.AsyncState;

        int iRx = socket.EndReceive(asyn);
    }
    catch (SocketException ex)
    {
        SocketExceptionCaught(ex);
    }
}

Таким чином, я можу безпечно виявити відключення між клієнтом TCP та сервером.


6
Так, встановіть низьке значення інтервалу keepalive +, будь-які опитування / надсилання повинні провалитися після інтервалу keepalive + 10 * мілісекунд. Здається, 10 спроб були жорстко закодовані з часів Vista? Працює, навіть якщо ви відключите кабель, на відміну від більшості інших відповідей. Це має бути прийнятою відповіддю.
toster-cx

15

Це просто неможливо. Фізичний зв’язок між вами та сервером відсутній (за винятком надзвичайно рідкісних випадків, коли ви підключаєтесь між двома комп’ютерами за допомогою петлевого кабелю).

Коли з'єднання вимкнено витончено, інша сторона отримує повідомлення. Але якщо з'єднання від'єднано якимось іншим способом (скажімо, з'єднання користувачів перервано), то сервер не буде знати, поки не закінчиться час очікування (або не спробує записати на з'єднання і час очікування закінчиться). Тільки так працює TCP, і з ним потрібно жити.

Тому "миттєво" нереально. Найкраще, що ви можете зробити, - це період очікування, який залежить від платформи, на якій працює код.

РЕДАГУВАТИ: Якщо ви шукаєте лише витончені з'єднання, чому б просто не надіслати на сервер команду "ВІДКЛЮЧИТИ" від вашого клієнта?


1
Дякую, але насправді я маю на увазі витончений розрив програмного забезпечення, а не фізичний розрив. У мене працює Windows

1
Якщо він шукає лише витончених роз'єднань, йому не потрібно нічого надсилати. Його прочитане поверне кінець потоку.
user207421

6

"Тільки так працює TCP, і ти повинен з цим жити".

Так, ти маєш рацію. Це факт життя, який я зрозумів. Ви побачите однакову поведінку, виявлену навіть у професійних додатках, що використовують цей протокол (і навіть інших). Я навіть бачив, як це трапляється в онлайн-іграх; ти приятель каже "до побачення", і він, здається, буде в Інтернеті ще 1-2 хвилини, поки сервер "не прибере будинок".

Ви можете скористатися запропонованими методами тут або застосувати "серцебиття", як також пропонувалось. Я вибираю перше. Але якби я вибрав останнє, я просто змушував би сервер щоразу пінгувати кожного клієнта одним байтом і перевіряти, чи немає у нас тайм-ауту чи немає відповіді. Ви навіть можете використовувати фоновий потік, щоб досягти цього з точними термінами. Можливо, навіть комбінація може бути реалізована в якомусь списку параметрів (прапорці перерахування чи щось інше), якщо вас це насправді турбує. Але це не така вже велика справа, якщо ви трохи затримуєтесь в оновленні сервера, поки ви оновлюєтесь. Це Інтернет, і ніхто не очікує, що це буде магія! :)


3

Введення серцебиття у вашу систему може бути рішенням. Це можливо лише в тому випадку, якщо під вашим контролем і клієнт, і сервер. Ви можете мати об’єкт DateTime, який відстежує час, коли останні байти були отримані з сокета. І припустимо, що сокет не реагував через певний проміжок часу. Це спрацює, лише якщо у вас реалізовано серцебиття / нестандартне підтримання життя.


2

Я знайшов цілком корисне, ще одне вирішення цього!

Якщо ви використовуєте асинхронні методи для зчитування даних із мережевого сокета (я маю на увазі, використовуйте BeginReceive-EndReceive methods), щоразу, коли з'єднання розривається; з’являється одна з таких ситуацій: або повідомлення надсилається без даних (ви можете побачити його за допомогою Socket.Available- хоча BeginReceiveце спрацьовує, його значення буде нульовим), або Socket.Connectedзначення стає хибним у цьому виклику (не намагайтеся використовувати EndReceiveтоді).

Я публікую функцію, яку використовував, думаю, ви краще зрозумієте, що я мав на увазі з неї:


private void OnRecieve(IAsyncResult parameter) 
{
    Socket sock = (Socket)parameter.AsyncState;
    if(!sock.Connected || sock.Available == 0)
    {
        // Connection is terminated, either by force or willingly
        return;
    }

    sock.EndReceive(parameter);
    sock.BeginReceive(..., ... , ... , ..., new AsyncCallback(OnRecieve), sock);

    // To handle further commands sent by client.
    // "..." zones might change in your code.
}

2

Це спрацювало для мене, головне - вам потрібен окремий потік для аналізу стану сокета за допомогою опитування. робити це в тому ж потоці, що і сокет, виявляє помилку.

//open or receive a server socket - TODO your code here
socket = new Socket(....);

//enable the keep alive so we can detect closure
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);

//create a thread that checks every 5 seconds if the socket is still connected. TODO add your thread starting code
void MonitorSocketsForClosureWorker() {
    DateTime nextCheckTime = DateTime.Now.AddSeconds(5);

    while (!exitSystem) {
        if (nextCheckTime < DateTime.Now) {
            try {
                if (socket!=null) {
                    if(socket.Poll(5000, SelectMode.SelectRead) && socket.Available == 0) {
                        //socket not connected, close it if it's still running
                        socket.Close();
                        socket = null;    
                    } else {
                        //socket still connected
                    }    
               }
           } catch {
               socket.Close();
            } finally {
                nextCheckTime = DateTime.Now.AddSeconds(5);
            }
        }
        Thread.Sleep(1000);
    }
}

1

Приклад коду тут http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.connected.aspx показує, як визначити, чи сокет все ще підключений, не надсилаючи жодних даних.

Якщо ви викликали Socket.BeginReceive () у програмі сервера, а потім клієнт розірвав з'єднання "витончено", буде викликаний ваш зворотний виклик, і EndReceive () поверне 0 байт. Ці 0 байт означають, що клієнт "можливо" відключився. Потім ви можете використовувати техніку, показану в прикладі коду MSDN, щоб точно визначити, чи було з'єднання перервано.


1

Розширюючи коментарі mbargiel та mycelo до прийнятої відповіді, наступне можна використовувати з неблокуючим сокетом на кінці сервера, щоб повідомити, чи вимкнувся клієнт.

Цей підхід не страждає від стану перегонів, що впливає на метод опитування у прийнятій відповіді.

// Determines whether the remote end has called Shutdown
public bool HasRemoteEndShutDown
{
    get
    {
        try
        {
            int bytesRead = socket.Receive(new byte[1], SocketFlags.Peek);

            if (bytesRead == 0)
                return true;
        }
        catch
        {
            // For a non-blocking socket, a SocketException with 
            // code 10035 (WSAEWOULDBLOCK) indicates no data available.
        }

        return false;
    }
}

Підхід заснований на тому, що Socket.Receiveметод повертає нуль відразу після того, як віддалений кінець вимикає свій сокет, і ми прочитали з нього всі дані. Отримати документацію з Socket .

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

Якщо ви перебуваєте в неблокуючому режимі, і в буфері стека протоколів відсутні дані, метод Receive негайно завершиться і видасть SocketException.

Другий пункт пояснює необхідність спроби улову.

Використання SocketFlags.Peekпрапора залишає всі отримані дані недоторканими для окремого механізму отримання для зчитування.

Вищезазначене також буде працювати з блокуючим сокетом, але майте на увазі, що код заблокує при виклику отримання (до отримання даних або закінчення часу очікування прийому, що знову призводить до а SocketException).


0

Ви не можете просто використовувати Select?

Використовуйте select на підключеному гнізді. Якщо вибір повертається з вашим сокетом як Готовий, але наступний Отримати повертає 0 байт, це означає, що клієнт відключив з'єднання. AFAIK, це найшвидший спосіб визначити, чи відключився клієнт.

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


0

Використовуючи метод SetSocketOption, ви зможете встановити KeepAlive, який повідомлятиме вас, коли сокет відключається

Socket _connectedSocket = this._sSocketEscucha.EndAccept(asyn);
                _connectedSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, 1);

http://msdn.microsoft.com/en-us/library/1011kecd(v=VS.90).aspx

Сподіваюся, це допоможе! Раміро Рінальді


2
Він не повідомлятиме вас, коли сокет відключається. Це виявить його, в кінці кінців, якщо закінчується підтримки активності таймера. За замовчуванням встановлено дві години. Ви не можете описати це як "будь-коли". Ви також не можете описати це як "повідомте [тинг], що ви знаєте: вам все одно доведеться прочитати або записати, щоб виявити помилку. -1
user207421

0

у мене була та ж проблема, спробуйте це:

void client_handler(Socket client) // set 'KeepAlive' true
{
    while (true)
    {
        try
        {
            if (client.Connected)
            {

            }
            else
            { // client disconnected
                break;
            }
        }
        catch (Exception)
        {
            client.Poll(4000, SelectMode.SelectRead);// try to get state
        }
    }
}

0

Це у VB, але, здається, мені це вдається. Він шукає повернення в 0 байт, як і попереднє повідомлення.

Private Sub RecData(ByVal AR As IAsyncResult)
    Dim Socket As Socket = AR.AsyncState

    If Socket.Connected = False And Socket.Available = False Then
        Debug.Print("Detected Disconnected Socket - " + Socket.RemoteEndPoint.ToString)
        Exit Sub
    End If
    Dim BytesRead As Int32 = Socket.EndReceive(AR)
    If BytesRead = 0 Then
        Debug.Print("Detected Disconnected Socket - Bytes Read = 0 - " + Socket.RemoteEndPoint.ToString)
        UpdateText("Client " + Socket.RemoteEndPoint.ToString + " has disconnected from Server.")
        Socket.Close()
        Exit Sub
    End If
    Dim msg As String = System.Text.ASCIIEncoding.ASCII.GetString(ByteData)
    Erase ByteData
    ReDim ByteData(1024)
    ClientSocket.BeginReceive(ByteData, 0, ByteData.Length, SocketFlags.None, New AsyncCallback(AddressOf RecData), ClientSocket)
    UpdateText(msg)
End Sub

-1

Ви також можете перевірити властивість .IsConnected сокета, якщо ви хотіли опитувати.


2
Це не буде працювати в жодному із сценаріїв (сервер / клієнт), які я представив вище

1
Також властивість називається .Connected.
Кверті

Це не виявляє довільних роз'єднань. Вам все одно доведеться виконати деякі операції вводу-виводу.
користувач207421

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