Java Socket API: jak sprawdzić, czy połączenie zostało zamknięte?


94

Mam pewne problemy z interfejsem API gniazd Java. Próbuję wyświetlić liczbę graczy aktualnie połączonych z moją grą. Łatwo jest określić, kiedy gracz się połączył. Jednak określenie, kiedy gracz rozłączył się za pomocą funkcji socket API, wydaje się niepotrzebnie trudne.

Dzwonienie isConnected()do gniazda, które zostało odłączone zdalnie, zawsze wydaje się powracać true. Podobnie, wywołanie isClosed()gniazda, które zostało zdalnie zamknięte, zawsze wydaje się powracać false. Przeczytałem, że aby faktycznie określić, czy gniazdo zostało zamknięte, czy nie, dane muszą zostać zapisane w strumieniu wyjściowym i wychwycony wyjątek. Wydaje się, że to naprawdę nieczysty sposób radzenia sobie z tą sytuacją. Musielibyśmy po prostu stale spamować wiadomością śmieciową w sieci, aby kiedykolwiek wiedzieć, kiedy gniazdo zostało zamknięte.

Czy jest jakieś inne rozwiązanie?

Odpowiedzi:


184

Nie ma interfejsu API TCP, który wskaże aktualny stan połączenia. isConnected()i isClosed()poda aktualny stan twojego gniazda . To nie to samo.

  1. isConnected()informuje, czy jesteś podłączony tego gniazda . Masz, więc zwraca prawdę.

  2. isClosed()powie Ci, czy masz zamknięciu tego gniazda. Dopóki tego nie zrobisz, zwraca fałsz.

  3. Jeśli peer zamknął połączenie w uporządkowany sposób

    • read() zwraca -1
    • readLine() zwroty null
    • readXXX()rzuca EOFExceptionza inne XXX.

    • Zapis wyrzuci IOException: 'reset połączenia przez peera', ostatecznie z zastrzeżeniem opóźnień buforowania.

  4. Jeśli połączenie zostało zerwane z jakiegokolwiek innego powodu, zapis wyrzuci w IOExceptionkońcu, jak powyżej, a odczyt może zrobić to samo.

  5. Jeśli peer jest nadal połączony, ale nie korzysta z połączenia, można użyć limitu czasu odczytu.

  6. W przeciwieństwie do tego, co możesz przeczytać gdzie indziej, ClosedChannelExceptionnie mówi ci tego. [Ani nie SocketException: socket closed.] tylko ona mówi ci, że jesteś zamknięty kanał, a następnie kontynuował ją wykorzystać. Innymi słowy, błąd programistyczny z Twojej strony. Nie oznacza zamkniętego połączenia.

  7. W wyniku niektórych eksperymentów z Javą 7 w systemie Windows XP okazuje się, że jeśli:

    • wybierasz OP_READ
    • select() zwraca wartość większą od zera
    • powiązany SelectionKeyjest już nieprawidłowy ( key.isValid() == false)

    oznacza to, że peer zresetował połączenie. Jednak może to być charakterystyczne dla wersji środowiska JRE lub platformy.


19
Aż trudno uwierzyć, że protokół TCP, który jest zorientowany na połączenie, nie może nawet poznać stanu swojego połączenia ... Czy ludzie, którzy wymyślili ten protokół, prowadzą samochody z zamkniętymi oczami?
PedroD,

33
@PedroD Wręcz przeciwnie: było to zamierzone. Poprzednie pakiety protokołów, takie jak SNA, miały „sygnał wybierania”. TCP został zaprojektowany, aby przetrwać wojnę nuklearną i, co bardziej trywialne, awarie i upadki routerów: stąd całkowity brak jakichkolwiek elementów, takich jak sygnał wybierania, stan połączenia itp .; i jest to również powód, dla którego funkcja utrzymywania aktywności TCP jest opisywana w specyfikacjach RFC jako kontrowersyjna funkcja i dlatego jest zawsze domyślnie wyłączona. TCP wciąż jest z nami. SNA? IPX? ISO? Nie. Zrozumieli.
Markiz Lorne

4
Nie sądzę, żeby to była dobra wymówka, by ukrywać przed nami te informacje. Świadomość, że połączenie zostało utracone, niekoniecznie oznacza, że ​​protokół jest mniej odporny na błędy, zawsze zależy to od tego, co zrobimy z tą wiedzą ... Dla mnie metody isBound i isConnected z java to czyste metody pozorowane, nie mają pożytku , ale wyrażam potrzebę nasłuchiwania zdarzeń połączenia ... Ale powtarzam: świadomość, że połączenie zostało utracone, nie pogarsza protokołu. Jeśli te protokoły, o których mówisz, przerywały połączenie, gdy tylko wykryły, że zostało utracone, to inna historia.
PedroD,

23
@Pedro Nie rozumiesz. To nie jest „wymówka”. Nie ma informacji do zatrzymania. Brak sygnału wybierania. TCP nie wie, czy połączenie nie powiodło się, dopóki nie spróbujesz czegoś z tym zrobić. To było podstawowe kryterium projektowe.
Markiz Lorne,

2
@EJP dzięki za odpowiedź. Może wiesz, jak sprawdzić, czy gniazdo zostało zamknięte bez „blokowania”? Użycie read lub readLine spowoduje zablokowanie. Czy istnieje sposób, aby stwierdzić, czy drugie gniazdo zostało zamknięte za pomocą dostępnej metody, wydaje się, że 0zamiast tego zwraca -1.
insumity

9

W różnych protokołach komunikacyjnych ogólną praktyką jest utrzymywanie pulsu (wysyłanie pakietów ping), pakiet nie musi być bardzo duży. Mechanizm sondujący pozwoli ci wykryć rozłączonego klienta nawet zanim TCP ogólnie to wykryje (limit czasu TCP jest znacznie wyższy) Wyślij sondę i poczekaj powiedzmy 5 sekund na odpowiedź, jeśli nie widzisz odpowiedzi, powiedzmy 2-3 kolejne próby, odtwarzacz zostanie rozłączony.

Również powiązane pytanie


6
W niektórych protokołach jest to „ogólna praktyka” . Zwracam uwagę, że HTTP, najczęściej używany protokół aplikacji na świecie, nie ma operacji PING.
Markiz Lorne

2
@ user207421, ponieważ HTTP / 1 jest protokołem jednostrzałowym. Wysyłasz zapytanie, otrzymujesz odpowiedź. Gotowe. Gniazdo jest zamknięte. Rozszerzenie Websocket ma operację PING, podobnie jak HTTP / 2.
Michał Zabielski

Jeśli to możliwe, polecam bicie serca jednym bajtem :)
Stefan Reich

@ MichałZabielski To dlatego, że projektanci HTTP tego nie określili. Nie wiesz, dlaczego nie. HTTP / 1.1 nie jest protokołem jednorazowym. Gniazdo nie jest zamknięte: połączenia HTTP są domyślnie trwałe. FTP, SMTP, POP3, IMAP, TLS, ... nie mają pulsu.
Markiz Lorne

2

Widzę, że inna odpowiedź właśnie została opublikowana, ale myślę, że jesteś interaktywny z klientami grającymi w twoją grę, więc mogę przedstawić inne podejście (podczas gdy BufferedReader jest zdecydowanie poprawny w niektórych przypadkach).

Gdybyś chciał… mógłbyś delegować odpowiedzialność za „rejestrację” na klienta. Oznacza to, że miałbyś kolekcję połączonych użytkowników z sygnaturą czasową ostatniej wiadomości otrzymanej od każdego z nich ... jeśli klient przekroczy limit czasu, wymusisz ponowną rejestrację klienta, ale to prowadzi do cytatu i pomysłu poniżej.

Przeczytałem, że aby faktycznie określić, czy gniazdo zostało zamknięte, dane muszą zostać zapisane w strumieniu wyjściowym i należy wychwycić wyjątek. Wydaje się, że to naprawdę nieczysty sposób radzenia sobie z tą sytuacją.

Jeśli twój kod Java nie zamknął / nie rozłączył Socket, to w jaki inny sposób zostałbyś powiadomiony, że zdalny host zamknął twoje połączenie? Ostatecznie twoja próba / złapanie robi mniej więcej to samo, co robiłby ankieter nasłuchujący zdarzeń na gnieździe ACTUAL. Rozważ następujące:

  • twój lokalny system może zamknąć twoje gniazdo bez powiadamiania cię ... to jest tylko implementacja Socket (tj. nie odpytuje sprzętu / sterownika / oprogramowania układowego / czegokolwiek o zmianę stanu).
  • new Socket (Proxy p) ... istnieje wiele stron (tak naprawdę 6 punktów końcowych), które mogą zamykać połączenie na tobie ...

Myślę, że jedną z cech abstrakcyjnych języków jest to, że jesteś oderwany od drobiazgów. Pomyśl o słowie kluczowym using w C # (spróbuj / w końcu) dla SqlConnection s lub czegokolwiek ... to tylko koszt prowadzenia biznesu ... Myślę, że try / catch / w końcu jest akceptowanym i niezbędnym wzorcem do użycia Socket.


Twój system lokalny nie może „zamknąć gniazda”, powiadamiając Cię lub nie. Twoje pytanie zaczyna się „czy kod Java nie zamknął / nie rozłączył gniazda ...?” też nie ma sensu.
Markiz Lorne

1
Co dokładnie uniemożliwia systemowi (na przykład Linux) wymuszenie zamknięcia gniazda? Czy nadal można to zrobić z „gdb” za pomocą polecenia „call close”?
Andrey Lebedenko

@AndreyLebedenko Nic nie „dokładnie temu zapobiega”, ale to nie robi.
Markiz Lorne

@EJB kto to robi?
Andrey Lebedenko

@AndreyLebedenko Nikt tego nie robi. Tylko aplikacja może zamknąć swoje własne gniazda, chyba że zamknie się bez tego. W takim przypadku system operacyjny zostanie wyczyszczony.
Markiz Lorne

1

Myślę, że taka jest natura połączeń TCP, w tych standardach potrzeba około 6 minut ciszy podczas transmisji, zanim stwierdzimy, że połączenie zostało przerwane! Więc nie sądzę, aby można było znaleźć dokładne rozwiązanie tego problemu. Może lepszym sposobem jest napisanie przydatnego kodu do odgadnięcia, kiedy serwer powinien założyć, że połączenie użytkownika jest zamknięte.


2
Może to zająć dużo więcej czasu, do dwóch godzin.
Markiz Lorne

0

Jak mówi @ user207421, nie ma możliwości poznania aktualnego stanu połączenia ze względu na model architektury protokołu TCP / IP. Serwer musi więc Cię zauważyć przed zamknięciem połączenia lub sam to sprawdzisz.
Oto prosty przykład, który pokazuje, jak sprawdzić, czy gniazdo jest zamknięte przez serwer:

sockAdr = new InetSocketAddress(SERVER_HOSTNAME, SERVER_PORT);
socket = new Socket();
timeout = 5000;
socket.connect(sockAdr, timeout);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream());
while ((data = reader.readLine())!=null) 
      log.e(TAG, "received -> " + data);
log.e(TAG, "Socket closed !");

0

Miałem podobny problem. W moim przypadku klient musi okresowo przesyłać dane. Mam nadzieję, że masz takie same wymagania. Następnie ustawiam SO_TIMEOUT, socket.setSoTimeout(1000 * 60 * 5);który jest rzucany, java.net.SocketTimeoutExceptiongdy upłynie określony czas. Wtedy mogę łatwo wykryć martwego klienta.


-2

Tak sobie z tym radzę

 while(true) {
        if((receiveMessage = receiveRead.readLine()) != null ) {  

        System.out.println("first message same :"+receiveMessage);
        System.out.println(receiveMessage);      

        }
        else if(receiveRead.readLine()==null)
        {

        System.out.println("Client has disconected: "+sock.isClosed()); 
        System.exit(1);
         }    } 

jeśli wynik.code == null


Nie musisz dzwonić readLine()dwa razy. Wiesz już, że w elsebloku było to zerowe .
Markiz Lorne

-4

W Linuksie, kiedy zapisujesz () w gnieździe, którego druga strona, nieznana Tobie, zamknęła, sprowokuje sygnał / wyjątek SIGPIPE, jakkolwiek chcesz to wywołać. Jeśli jednak nie chcesz dać się złapać SIGPIPE, możesz użyć funkcji send () z flagą MSG_NOSIGNAL. Wywołanie send () zwróci wartość -1 iw tym przypadku możesz sprawdzić errno, które powie ci, że próbowałeś napisać przerwany potok (w tym przypadku gniazdo) o wartości EPIPE, która zgodnie z errno.h jest równoważna 32. W reakcji na EPIPE możesz cofnąć się i spróbować ponownie otworzyć gniazdo i spróbować ponownie wysłać informacje.


send()Wywołanie zwróci -1 tylko wtedy, gdy dane wychodzące ale buforowane przez wystarczająco długi dla timerów Prześlij wygasa. Prawie na pewno nie nastąpi to przy pierwszym wysyłaniu po rozłączeniu, ze względu na buforowanie na obu końcach i asynchroniczny charakter send()pod maską.
Markiz Lorne
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.