Natychmiast wykryj odłączenie klienta od gniazda serwera


82

Jak mogę wykryć, że klient odłączył się od mojego serwera?

Mam następujący kod w mojej AcceptCallBackmetodzie

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

Muszę znaleźć sposób, aby jak najszybciej odkryć, że klient odłączył się od handlergniazda.

Próbowałem:

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

Powyższe podejścia działają, gdy łączysz się z serwerem i chcesz wykryć, kiedy serwer się rozłącza, ale nie działają, gdy jesteś serwerem i chcesz wykryć rozłączenie klienta.

Każda pomoc zostanie doceniona.


10
@Samuel: Znaczniki TCP i połączenia bardzo istotne dla tego postu, ponieważ TCP utrzymuje połączenie (podczas gdy inne protokoły sieciowe, takie jak UDP, nie).
Noldorin

3
Więcej na temat rozwiązania bicia serca z mojego bloga: Detection of Half-Open (Dropped) Connections
Stephen Cleary

Opisane tutaj rozwiązanie działa dobrze dla mnie: stackoverflow.com/questions/1387459/…
Rawk

Odpowiedzi:


110

Ponieważ nie ma dostępnych zdarzeń, które sygnalizowałyby odłączenie gniazda, będziesz musiał odpytywać je na akceptowalnej częstotliwości.

Korzystając z tej metody rozszerzenia, możesz mieć niezawodną metodę wykrywania, czy gniazdo jest odłączone.

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
To zadziałało. Dzięki. Zmieniłem metodę powrotu! (Socket.Available == 0 && socket.Poll (1, SelectMode.SelectRead)); ponieważ podejrzewam gniazdo.Available jest szybsze niż Socket.Poll ()

28
Ta metoda nie działa, chyba że drugi koniec połączenia faktycznie zamyka / zamyka gniazdo. Odłączony kabel sieciowy / zasilający nie zostanie zauważony przed upływem limitu czasu. Jedynym sposobem natychmiastowego powiadamiania o rozłączeniach jest funkcja pulsu, która umożliwia ciągłe sprawdzanie połączenia.
Kasper Holdum

7
@Smart Alec: właściwie powinieneś użyć przykładu, jak pokazano powyżej. Jeśli zmienisz kolejność, może wystąpić sytuacja wyścigu: jeśli socket.Availablezwróci 0 i otrzymasz pakiet tuż przed socket.Pollwywołaniem, Pollzwróci wartość true i metoda zwróci wartość false, chociaż gniazdo jest nadal w dobrym stanie.
Groo

4
Działa to dobrze w 99% przypadków, czasami jednak powoduje fałszywe rozłączenie.
Matthew Finlay

5
Tak jak zauważył Matthew Finlay, czasami będzie to zgłaszać fałszywe rozłączenia, ponieważ między wynikiem metody Poll a sprawdzeniem właściwości Available nadal występuje wyścig. Pakiet może być prawie gotowy do odczytu, ale jeszcze nie, tak więc Dostępne jest 0 - ale milisekundę później są dane do odczytania. Lepszą opcją jest próba otrzymania jednego bajtu i flagi SocketFlags.Peek. Lub zaimplementuj jakąś formę pulsu i utrzymuj stan połączenia na wyższym poziomie. Lub polegaj na obsłudze błędów w metodach wysyłania / odbierania (i wywołań zwrotnych, jeśli używasz wersji asynchronicznych).
mbargiel

22

Ktoś wspomniał o możliwościach KeepAlive w TCP Socket. Tutaj jest to ładnie opisane:

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

Używam go w ten sposób: po podłączeniu gniazda wywołuję tę funkcję, która włącza keepAlive. keepAliveTimeParametr określa czas oczekiwania w milisekundach braku aktywności aż pierwszy pakiet keep-alive zostanie wysłana. keepAliveIntervalParametr określa odstęp w milisekundach między kiedy kolejne pakiety keep-alive są wysyłane jeśli otrzymano żadnego potwierdzenia.

    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);
    }

Używam również czytania asynchronicznego:

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

W wywołaniu zwrotnym jest tu przechwytywany limit czasu SocketException, który wzrasta, gdy gniazdo nie otrzymuje sygnału ACK po pakiecie utrzymywania aktywności.

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

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

W ten sposób jestem w stanie bezpiecznie wykryć rozłączenie między klientem TCP a serwerem.


6
Tak, ustaw interwał keepalive + na niską wartość, wszelkie odpytywania / wysyłania powinny kończyć się niepowodzeniem po utrzymaniu aktywności + 10 * milisekundach. 10 ponowień wydaje się być zakodowanych na stałe od czasu Vista? Działa nawet po odłączeniu kabla w przeciwieństwie do większości innych odpowiedzi. To powinna być akceptowana odpowiedź.
toster-cx

15

To jest po prostu niemożliwe. Nie ma fizycznego połączenia między Tobą a serwerem (z wyjątkiem niezwykle rzadkich przypadków, gdy łączysz się między dwoma komputerami za pomocą kabla sprzężenia zwrotnego).

Gdy połączenie zostanie prawidłowo zamknięte, druga strona jest powiadamiana. Ale jeśli połączenie zostanie zerwane w inny sposób (powiedzmy, że połączenie użytkowników zostanie zerwane), serwer nie będzie wiedział, dopóki nie wygaśnie (lub spróbuje zapisać połączenie i upłynie limit czasu potwierdzenia). Tak właśnie działa TCP i trzeba z tym żyć.

Dlatego „natychmiastowe” jest nierealne. Najlepsze, co możesz zrobić, to zachować limit czasu, który zależy od platformy, na której działa kod.

EDYCJA: Jeśli szukasz tylko dobrych połączeń, to dlaczego nie wysłać po prostu polecenia „ROZŁĄCZ” do serwera ze swojego klienta?


1
Dzięki, ale tak naprawdę mam na myśli pełne wdzięku odłączenie oprogramowania, a nie fizyczne odłączenie. Używam systemu Windows

1
Jeśli szuka tylko zgrabnych rozłączeń, nie musi niczego wysyłać. Jego odczyt przywróci koniec strumienia.
user207421

6

„Po prostu tak działa TCP i trzeba z tym żyć”.

Tak, masz rację. Zrozumiałem, że to fakt życia. Zobaczysz to samo zachowanie nawet w profesjonalnych aplikacjach używających tego protokołu (a nawet innych). Widziałem to nawet w grach online; Twój kumpel mówi „do widzenia” i wydaje się być online przez kolejne 1-2 minuty, aż serwer „sprząta dom”.

Możesz użyć sugerowanych metod tutaj lub zaimplementować „bicie serca”, jak również zasugerowano. Wybieram to pierwsze. Ale gdybym wybrał to drugie, po prostu kazałbym serwerowi „pingować” każdego klienta co jakiś czas jednym bajtem i sprawdzać, czy mamy limit czasu, czy nie ma odpowiedzi. Możesz nawet użyć wątku w tle, aby osiągnąć to dzięki precyzyjnemu czasowi. Może nawet kombinacja mogłaby zostać zaimplementowana na jakiejś liście opcji (flagi wyliczenia lub coś takiego), jeśli naprawdę się tym martwisz. Ale to nie jest takie duże opóźnienie w aktualizacji serwera, o ile aktualizujesz. To internet i nikt nie oczekuje, że będzie magiczny! :)


3

Wdrożenie pulsu w systemie może być rozwiązaniem. Jest to możliwe tylko wtedy, gdy zarówno klient, jak i serwer są pod Twoją kontrolą. Możesz mieć obiekt DateTime, który śledzi czas odebrania ostatnich bajtów z gniazda. I załóżmy, że gniazdo, które nie odpowiedziało w określonym przedziale czasu, zostanie utracone. To zadziała tylko wtedy, gdy zaimplementowano bicie serca / niestandardowe utrzymywanie aktywności.


2

Znalazłem całkiem przydatne, kolejne obejście tego problemu!

Jeśli używasz asynchronicznych metod do odczytu danych z gniazda sieciowego (mam na myśli BeginReceive- EndReceive metody), zawsze, gdy połączenie zostanie zakończone; pojawia się jedna z tych sytuacji: albo wiadomość jest wysyłana bez danych (możesz to zobaczyć Socket.Available- nawet jeśli BeginReceivejest wyzwalana, jej wartość będzie wynosić zero) lub Socket.Connectedwartość stanie się fałszywa w tym wywołaniu (nie próbuj EndReceivewtedy używać ).

Piszę funkcję, z której korzystałem, myślę, że możesz lepiej zobaczyć, o co mi chodziło:


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

To zadziałało dla mnie, kluczem jest to, że potrzebujesz osobnego wątku do analizy stanu gniazda z odpytywaniem. robi to w tym samym wątku, w którym gniazdo nie jest wykrywane.

//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

Przykładowy kod tutaj http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.connected.aspx pokazuje, jak określić, czy Socket jest nadal połączony bez wysyłania żadnych danych.

Jeśli wywołałeś Socket.BeginReceive () w programie serwera, a następnie klient zamknął połączenie "wdzięcznie", zostanie wywołane wywołanie zwrotne odbierania, a EndReceive () zwróci 0 bajtów. Te 0 bajtów oznacza, że ​​klient „mógł” się rozłączyć. Następnie możesz użyć techniki pokazanej w przykładowym kodzie MSDN, aby ustalić na pewno, czy połączenie zostało zamknięte.


1

Rozwijając komentarze mbargiela i mycelo na temat zaakceptowanej odpowiedzi, można użyć poniższego z gniazdem nieblokującym po stronie serwera, aby poinformować, czy klient się wyłączył.

Takie podejście nie wpływa na stan wyścigu, który wpływa na metodę Sonda w zaakceptowanej odpowiedzi.

// 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;
    }
}

Podejście opiera się na fakcie, że Socket.Receivemetoda zwraca zero natychmiast po tym, jak zdalny koniec zamknie swoje gniazdo i odczytaliśmy z niego wszystkie dane. Od Socket.Receive dokumentacji :

Jeśli host zdalny zamknie połączenie Socket za pomocą metody Shutdown, a wszystkie dostępne dane zostały odebrane, metoda Receive zostanie natychmiast zakończona i zwróci zero bajtów.

Jeśli jesteś w trybie bez blokowania i nie ma danych dostępnych w buforze stosu protokołów, metoda Receive zostanie natychmiast zakończona i zgłosi SocketException.

Drugi punkt wyjaśnia potrzebę próby złapania.

Użycie SocketFlags.Peekflagi pozostawia wszystkie odebrane dane nietknięte, aby umożliwić ich odczytanie przez oddzielny mechanizm odbioru.

Powyższe będzie działać również z gniazdem blokującym , ale należy pamiętać, że kod będzie blokował wywołanie Receive (do momentu odebrania danych lub upływu limitu czasu odbioru, co ponownie spowoduje a SocketException).


0

Nie możesz po prostu użyć Select?

Użyj opcji wyboru na podłączonym gnieździe. Jeśli wybór wraca z gniazdem jako Gotowe, ale kolejne Odbiór zwraca 0 bajtów, co oznacza, że ​​klient rozłączył połączenie. AFAIK, to najszybszy sposób określenia, czy klient się rozłączył.

Nie znam języka C #, więc po prostu zignoruj, jeśli moje rozwiązanie nie pasuje do C # (C # zapewnia jednak wybór ) lub jeśli źle zrozumiałem kontekst.


0

Używając metody SetSocketOption, będziesz mógł ustawić KeepAlive, który poinformuje Cię o rozłączeniu Socket

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

Mam nadzieję, że to pomoże! Ramiro Rinaldi


2
Nie poinformuje Cię, gdy gniazdo zostanie odłączone. W końcu wykryje to, jeśli wygaśnie licznik czasu utrzymywania aktywności. Domyślnie są to dwie godziny. Nie możesz tego opisać jako „kiedykolwiek”. Nie możesz również opisać tego jako „daj [t] wiedzieć: nadal musisz wykonać odczyt lub zapis, aby błąd został wykryty. -1
user207421

0

miałem ten sam problem, spróbuj tego:

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

To jest w VB, ale wydaje mi się, że działa dobrze. Wygląda na 0 bajtów powrotu, tak jak w poprzednim wpisie.

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

Możesz również sprawdzić właściwość .IsConnected gniazda, jeśli chcesz sondować.


2
To nie zadziała w żadnym ze scenariuszy (serwer / klient), które przedstawiłem powyżej

1
Właściwość nosi również nazwę .Connected.
Qwertie

To nie wykrywa przypadkowych rozłączeń. Nadal musisz wykonać kilka operacji we / wy.
user207421

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.