W odpowiedzi na niedawne pytanie zastanawiam się, dlaczego w Javie nie jest możliwe wykrycie, że gniazdo zostało bezpiecznie zamknięte przez równorzędnego, bez próby odczytu / zapisu w gnieździe TCP? Wydaje się, że tak jest niezależnie od tego, czy używa się pre-NIO, Socket
czy NIO SocketChannel
.
Kiedy peer z wdziękiem zamyka połączenie TCP, stos TCP po obu stronach połączenia wie o tym fakcie. Strona serwera (ta, która inicjuje zamknięcie) kończy się w stanie FIN_WAIT2
, podczas gdy strona klienta (ta, która nie reaguje jawnie na zamknięcie) kończy się w stanie CLOSE_WAIT
. Dlaczego nie jest to metoda w Socket
lub SocketChannel
że może zapytać stos TCP, aby zobaczyć, czy połączenie TCP zostało zakończone? Czy to dlatego, że stos TCP nie dostarcza takich informacji o stanie? A może jest to decyzja projektowa, aby uniknąć kosztownego wywołania jądra?
Z pomocą użytkowników, którzy już opublikowali kilka odpowiedzi na to pytanie, myślę, że wiem, skąd może pochodzić problem. Strona, która nie zamyka jawnie połączenia, przechodzi w stan TCP, CLOSE_WAIT
co oznacza, że połączenie jest w trakcie zamykania i czeka, aż strona wykona własną CLOSE
operację. Przypuszczam, że to sprawiedliwe, że isConnected
wraca true
i isClosed
wraca false
, ale dlaczego nie ma czegoś takiego isClosing
?
Poniżej znajdują się klasy testowe, które używają gniazd pre-NIO. Ale identyczne wyniki uzyskuje się przy użyciu NIO.
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer {
public static void main(String[] args) throws Exception {
final ServerSocket ss = new ServerSocket(12345);
final Socket cs = ss.accept();
System.out.println("Accepted connection");
Thread.sleep(5000);
cs.close();
System.out.println("Closed connection");
ss.close();
Thread.sleep(100000);
}
}
import java.net.Socket;
public class MyClient {
public static void main(String[] args) throws Exception {
final Socket s = new Socket("localhost", 12345);
for (int i = 0; i < 10; i++) {
System.out.println("connected: " + s.isConnected() +
", closed: " + s.isClosed());
Thread.sleep(1000);
}
Thread.sleep(100000);
}
}
Gdy klient testowy łączy się z serwerem testowym, dane wyjściowe pozostają niezmienione nawet po zainicjowaniu przez serwer zamykania połączenia:
connected: true, closed: false
connected: true, closed: false
...
źródło
Socket.close
nie jest wdzięcznym zamknięciem.Odpowiedzi:
Często korzystałem z gniazd, głównie z selektorami, i chociaż nie jestem ekspertem od Network OSI, z mojego rozumienia, wywołanie
shutdownOutput()
Socket w rzeczywistości wysyła coś w sieci (FIN), co budzi mój Selector po drugiej stronie (to samo zachowanie w C język). Tutaj masz wykrywanie : faktycznie wykrywanie operacji odczytu, która zakończy się niepowodzeniem, gdy spróbujesz.W podanym przez Ciebie kodzie zamknięcie gniazda spowoduje zamknięcie zarówno strumienia wejściowego, jak i wyjściowego, bez możliwości odczytu danych, które mogą być dostępne, a tym samym ich utrata. Metoda Java
Socket.close()
wykonuje „wdzięczne” rozłączenie (odwrotnie niż początkowo sądziłem) w ten sposób, że dane pozostawione w strumieniu wyjściowym zostaną wysłane, a następnie FIN, aby zasygnalizować jego zamknięcie. Druga strona potwierdzi FIN, tak jak każdy zwykły pakiet 1 .Jeśli musisz poczekać, aż druga strona zamknie gniazdo, musisz poczekać na jego FIN. I aby to osiągnąć, to trzeba wykryć
Socket.getInputStream().read() < 0
, co oznacza, że powinna nie zamknąć gniazdo, gdyż zamknięcie jejInputStream
.Z tego, co zrobiłem w C, a teraz w Javie, osiągnięcie takiego zsynchronizowanego zamknięcia powinno odbywać się w następujący sposób:
read()
i wykryć pilotaclose()
InputStream
dopóki nie otrzymamy odpowiedzi-FIN z drugiego końca (ponieważ wykryje FIN, przejdzie przez ten sam wdzięczny proces odłączania). Jest to ważne w niektórych systemach operacyjnych, ponieważ w rzeczywistości nie zamykają one gniazda, o ile jeden z jego buforów nadal zawiera dane. Nazywa się je gniazdem „duchowym” i zużywa numery deskryptorów w systemie operacyjnym (może to już nie stanowić problemu w przypadku współczesnego systemu operacyjnego)Socket.close()
lub zamknięcie jegoInputStream
lubOutputStream
)Jak pokazano na poniższym fragmencie kodu Java:
public void synchronizedClose(Socket sok) { InputStream is = sok.getInputStream(); sok.shutdownOutput(); // Sends the 'FIN' on the network while (is.read() > 0) ; // "read()" returns '-1' when the 'FIN' is reached sok.close(); // or is.close(); Now we can close the Socket }
Oczywiście obie strony muszą stosować ten sam sposób zamykania lub część wysyłająca może zawsze wysyłać wystarczającą ilość danych, aby
while
pętla była zajęta (np. Jeśli część wysyłająca wysyła tylko dane i nigdy nie czyta w celu wykrycia zakończenia połączenia. ale możesz nie mieć nad tym kontroli).Jak zauważył @WarrenDew w swoim komentarzu, odrzucenie danych w programie (warstwie aplikacji) wywołuje niezręczne rozłączenie w warstwie aplikacji: chociaż wszystkie dane zostały odebrane w warstwie TCP (
while
pętla), są one odrzucane.1 : Z „ Podstawowej sieci w Javie ”: patrz rys. 3,3 s. 45 i cały § 3.7, s. 43-48
źródło
socket.close()
jest „brutalne”, ponieważ nie uwzględnia sygnalizacji klienta / serwera. Serwer byłby powiadamiany o rozłączeniu klienta tylko wtedy, gdy jego własny bufor wyjściowy gniazda jest pełny (ponieważ żadne dane nie są potwierdzane przez drugą stronę, która została zamknięta).read()
zwraca -1 na końcu strumienia i nadal to robi, niezależnie od tego, ile razy to wywołasz. ACread()
lubrecv()
zwraca zero na końcu strumienia i nadal to robi, niezależnie od tego, ile razy to wywołasz.Myślę, że jest to bardziej kwestia programowania gniazd. Java po prostu podąża za tradycją programowania w gniazdach.
Z Wikipedii :
Po zakończeniu uzgadniania TCP nie rozróżnia dwóch punktów końcowych (klienta i serwera). Określenia „klient” i „serwer” są używane głównie dla wygody. Zatem „serwer” może wysyłać dane, a „klient” może wysyłać do siebie inne dane jednocześnie.
Termin „zamknij” również wprowadza w błąd. Jest tylko deklaracja FIN, co oznacza: „Nie zamierzam wysyłać Ci więcej rzeczy”. Ale to nie znaczy, że nie ma żadnych pakietów w locie lub drugi nie ma nic więcej do powiedzenia. Jeśli zaimplementujesz pocztę ślimakową jako warstwę łącza danych lub jeśli Twój pakiet podróżował różnymi trasami, możliwe jest, że odbiorca odbierze pakiety w złej kolejności. TCP wie, jak to naprawić.
Również Ty, jako program, możesz nie mieć czasu na sprawdzanie zawartości bufora. Tak więc w dogodnym dla Ciebie czasie możesz sprawdzić, co jest w buforze. Podsumowując, obecna implementacja gniazd nie jest taka zła. Jeśli faktycznie istniała isPeerClosed (), to jest to dodatkowe wywołanie, które musisz wykonać za każdym razem, gdy chcesz wywołać read.
źródło
Podstawowy interfejs API gniazd nie ma takiego powiadomienia.
Wysyłający stos TCP i tak nie wyśle bitu FIN aż do ostatniego pakietu, więc może być wiele danych buforowanych od momentu, gdy aplikacja wysyłająca logicznie zamknęła swoje gniazdo, zanim te dane zostały wysłane. Podobnie dane, które są buforowane, ponieważ sieć jest szybsza niż aplikacja odbierająca (nie wiem, może przekazujesz je przez wolniejsze połączenie) mogą mieć znaczenie dla odbiorcy i nie chcesz, aby aplikacja odbierająca je odrzuciła tylko dlatego, że bit FIN został odebrany przez stos.
źródło
Ponieważ żadna z dotychczasowych odpowiedzi w pełni nie odpowiada na pytanie, podsumowuję moje obecne rozumienie problemu.
Po ustanowieniu połączenia TCP i wywołaniu jednego z równorzędnych
close()
lubshutdownOutput()
w jego gnieździe gniazdo po drugiej stronie połączenia przechodzi wCLOSE_WAIT
stan. Zasadniczo można dowiedzieć się ze stosu TCP, czy gniazdo jest wCLOSE_WAIT
stanie bez wywoływaniaread/recv
(np. Wgetsockopt()
systemie Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), ale tak nie jest przenośny.Socket
Wydaje się, że klasa Javy ma na celu zapewnienie abstrakcji porównywalnej z gniazdem BSD TCP, prawdopodobnie dlatego, że jest to poziom abstrakcji, do którego ludzie są przyzwyczajeni podczas programowania aplikacji TCP / IP. Gniazda BSD to uogólnienie obsługujące gniazda inne niż tylko INET (np. TCP), więc nie zapewniają przenośnego sposobu sprawdzania stanu TCP gniazda.Nie ma takiej metody,
isCloseWait()
ponieważ ludzie przyzwyczajeni do programowania aplikacji TCP na poziomie abstrakcji oferowanej przez gniazda BSD nie oczekują, że Java zapewni dodatkowe metody.źródło
isCloseWait()
ponieważ nie jest obsługiwana na wszystkich platformach.Wykrywanie, czy zdalna strona połączenia gniazda (TCP) zostało zamknięte, można wykonać za pomocą metody java.net.Socket.sendUrgentData (int) i przechwycić IOException, który zgłasza, jeśli strona zdalna nie działa. Zostało to przetestowane między Java-Java i Java-C.
Pozwala to uniknąć problemu projektowania protokołu komunikacyjnego tak, aby używał pewnego rodzaju mechanizmu pingowania. Wyłączając OOBInline w gnieździe (setOOBInline (false), wszystkie odebrane dane OOB są dyskretnie odrzucane, ale dane OOB mogą być nadal wysyłane. Jeśli strona zdalna jest zamknięta, próba zresetowania połączenia kończy się niepowodzeniem i powoduje wyrzucenie wyjątku IOException .
Jeśli faktycznie używasz danych OOB w swoim protokole, Twój przebieg może się różnić.
źródło
stos Java IO zdecydowanie wysyła FIN, gdy zostanie zniszczony podczas nagłego rozerwania. Po prostu nie ma sensu, że nie możesz tego wykryć, b / c większość klientów wysyła FIN tylko wtedy, gdy zamykają połączenie.
... kolejny powód, dla którego naprawdę zaczynam nienawidzić klas NIO Java. Wygląda na to, że wszystko jest trochę pół dupkiem.
źródło
To ciekawy temat. Przed chwilą przekopałem kod Java, aby to sprawdzić. Z moich ustaleń wynika, że istnieją dwa różne problemy: pierwszy to sam protokół TCP RFC, który umożliwia zdalnie zamkniętemu gniazdu przesyłanie danych w trybie półdupleksu, więc zdalnie zamknięte gniazdo jest nadal w połowie otwarte. Zgodnie z RFC, RST nie zamyka połączenia, musisz wysłać jawną komendę ABORT; więc Java pozwala na przesyłanie danych przez pół zamknięte gniazdo
(Istnieją dwie metody odczytywania stanu zamknięcia w obu punktach końcowych).
Innym problemem jest to, że implementacja mówi, że to zachowanie jest opcjonalne. Ponieważ Java stara się być przenośna, zaimplementowała najlepszą wspólną funkcję. Wydaje mi się, że utrzymanie mapy (system operacyjny, implementacja półdupleksu) byłoby problemem.
źródło
Jest to wada Javy (i wszystkich innych, które widziałem) klas gniazd OO - brak dostępu do wybranego wywołania systemowego.
Prawidłowa odpowiedź w C:
struct timeval tp; fd_set in; fd_set out; fd_set err; FD_ZERO (in); FD_ZERO (out); FD_ZERO (err); FD_SET(socket_handle, err); tp.tv_sec = 0; /* or however long you want to wait */ tp.tv_usec = 0; select(socket_handle + 1, in, out, err, &tp); if (FD_ISSET(socket_handle, err) { /* handle closed socket */ }
źródło
getsocketop(... SOL_SOCKET, SO_ERROR, ...)
.Oto kiepskie obejście. Użyj SSL;), a SSL wykonuje ścisłe uzgadnianie po rozerwaniu, więc jesteś powiadamiany o zamknięciu gniazda (większość implementacji wydaje się robić właściwe porzucenie uzgadniania).
źródło
Przyczyną tego zachowania (które nie jest specyficzne dla języka Java) jest fakt, że nie otrzymujesz żadnych informacji o statusie ze stosu TCP. W końcu gniazdo jest po prostu kolejnym uchwytem pliku i nie możesz dowiedzieć się, czy istnieją rzeczywiste dane do odczytania z niego bez faktycznej próby (
select(2)
nic w tym nie pomoże, tylko sygnalizuje, że możesz spróbować bez blokowania).Więcej informacji można znaleźć w często zadawanych pytaniach dotyczących gniazd Unix .
źródło
select()
informuje, czy są dostępne dane lub EOS do odczytu bez blokowania. „Sygnały, które możesz wypróbować bez blokowania” nie mają znaczenia. Jeśli jesteś w trybie bez blokowania, zawsze możesz spróbować bez blokowania.select()
jest sterowany danymi w buforze odbioru gniazda lub oczekującym FIN lub spacją w buforze wysyłania gniazda.getsockopt(SO_ERROR)
? W rzeczywistości nawetgetpeername
powie ci, czy gniazdo jest nadal podłączone.Tylko zapisy wymagają wymiany pakietów, co pozwala na określenie utraty połączenia. Typowym obejściem jest użycie opcji KEEP ALIVE.
źródło
Jeśli chodzi o półotwarte gniazda Java, warto przyjrzeć się isInputShutdown () i isOutputShutdown () .
źródło