Perché è impossibile, senza tentare di I / O, rilevare che il socket TCP è stato chiuso con grazia dal peer?


92

A seguito di una recente domanda , mi chiedo perché sia ​​impossibile in Java, senza tentare di leggere / scrivere su un socket TCP, rilevare che il socket è stato chiuso con grazia dal peer? Questo sembra essere il caso indipendentemente dal fatto che si utilizzi il pre-NIO Socketo il NIO SocketChannel.

Quando un peer chiude con grazia una connessione TCP, gli stack TCP su entrambi i lati della connessione ne sono a conoscenza. Il lato server (quello che avvia l'arresto) finisce nello stato FIN_WAIT2, mentre il lato client (quello che non risponde esplicitamente allo spegnimento) finisce nello stato CLOSE_WAIT. Perché non c'è un metodo Socketo SocketChannelche può interrogare lo stack TCP per vedere se la connessione TCP sottostante è stata terminata? È che lo stack TCP non fornisce tali informazioni sullo stato? O è una decisione progettuale per evitare una chiamata costosa nel kernel?

Con l'aiuto degli utenti che hanno già pubblicato alcune risposte a questa domanda, penso di vedere da dove potrebbe provenire il problema. Il lato che non chiude esplicitamente la connessione finisce nello stato TCP, CLOSE_WAITil che significa che la connessione è in fase di chiusura e attende che il lato esegua la propria CLOSEoperazione. Suppongo sia abbastanza giusto che isConnectedritorni truee isClosedritorni false, ma perché non c'è qualcosa di simile isClosing?

Di seguito sono riportate le classi di test che utilizzano socket pre-NIO. Ma risultati identici si ottengono usando 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);
  }
}

Quando il client di prova si connette al server di prova, l'output rimane invariato anche dopo che il server ha avviato l'arresto della connessione:

connected: true, closed: false
connected: true, closed: false
...

Pensavo di menzionare: il protocollo SCTP non ha questo "problema". SCTP non ha uno stato semichiuso come TCP, in altre parole un lato non può continuare a inviare dati mentre l'altra estremità ha chiuso il suo socket di invio. Questo dovrebbe rendere le cose più facili.
Tarnay Kálmán

2
Abbiamo due caselle di posta (prese) ........................................... ...... Le caselle di posta si inviano messaggi di posta utilizzando la RoyalMail (IP) dimentica il TCP ............................. .................... Tutto va bene e dandy, le caselle di posta possono inviare / ricevere posta a vicenda (carichi di ritardo di recente) inviando durante la ricezione senza problemi. ............. Se una cassetta postale dovesse essere investita da un camion e fallire ... come farebbe a saperlo l'altra cassetta postale? Dovrebbe essere avvisato dalla Royal Mail, che a sua volta non lo saprebbe fino a quando non avrà tentato di inviare / ricevere posta da quella casella di posta fallita .. ...... erm .....
divinci

Se non leggi dal socket o non scrivi nel socket, perché ti interessa? E se hai intenzione di leggere dal socket o scrivere sul socket, perché fare un controllo extra? Qual è il caso d'uso?
David Schwartz

2
Socket.closenon è una chiusura graziosa.
user253751

@immibis È sicuramente una chiusura gradevole, a meno che non ci siano dati non letti nel buffer di ricezione del socket o che tu abbia pasticciato con SO_LINGER.
Marchese di Lorne

Risposte:


29

Ho usato spesso i socket, principalmente con i selettori, e sebbene non sia un esperto di OSI di rete, dalla mia comprensione, chiamare shutdownOutput()un socket invia effettivamente qualcosa sulla rete (FIN) che sveglia il mio selettore dall'altra parte (stesso comportamento in C linguaggio). Qui hai il rilevamento : rilevare effettivamente un'operazione di lettura che non andrà a buon fine quando la proverai.

Nel codice che fornisci, la chiusura del socket interromperà sia i flussi di input che quelli di output, senza possibilità di leggere i dati che potrebbero essere disponibili, perdendoli quindi. Il Socket.close()metodo Java esegue una disconnessione "aggraziata" (opposta a quanto pensavo inizialmente) in quanto i dati rimasti nel flusso di output verranno inviati seguiti da un FIN per segnalare la sua chiusura. La FIN verrà RICONOSCIUTA dall'altra parte, come farebbe un normale pacchetto 1 .

Se devi aspettare che l'altro lato chiuda il suo socket, devi aspettare il suo FIN. E per raggiungere questo, si deve rilevare Socket.getInputStream().read() < 0, il che significa che dovrebbe non vicino la vostra presa di corrente, in quanto sarebbe chiudere il suoInputStream .

Da quello che ho fatto in C, e ora in Java, ottenere una chiusura così sincronizzata dovrebbe essere fatto in questo modo:

  1. Uscita del socket di spegnimento (invia FIN dall'altra parte, questa è l'ultima cosa che verrà mai inviata da questo socket). L'input è ancora aperto in modo da poter read()rilevare il telecomandoclose()
  2. Leggi il socket InputStreamfino a quando non riceviamo la risposta-FIN dall'altra estremità (poiché rileverà la FIN, passerà attraverso lo stesso grazioso processo di disconnessione). Questo è importante su alcuni sistemi operativi poiché in realtà non chiudono il socket fintanto che uno dei suoi buffer contiene ancora dati. Sono chiamati socket "fantasma" e utilizzano i numeri dei descrittori nel sistema operativo (questo potrebbe non essere più un problema con il sistema operativo moderno)
  3. Chiudere il socket (chiamando Socket.close()o chiudendo il suo InputStreamor OutputStream)

Come mostrato nel seguente frammento di 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
}

Ovviamente entrambe le parti devono usare lo stesso modo di chiusura, o la parte mittente potrebbe sempre inviare dati sufficienti per mantenere il whileciclo occupato (ad esempio se la parte mittente sta solo inviando dati e non sta mai leggendo per rilevare la terminazione della connessione. Il che è goffo, ma potresti non avere il controllo su questo).

Come ha sottolineato @WarrenDew nel suo commento, scartare i dati nel programma (livello applicazione) induce una disconnessione non gradevole a livello applicazione: sebbene tutti i dati siano stati ricevuti a livello TCP (il whileciclo), vengono scartati.

1 : Da " Networking fondamentale in Java ": vedi fig. 3.3 p.45, e l'intero §3.7, pp 43-48


Java esegue una chiusura graziosa. Non è "brutale".
Marchese di Lorne

2
@EJP, "graceful disconnection" è uno scambio specifico che avviene a livello TCP, dove il Cliente deve segnalare la sua volontà di disconnettersi al Server, il quale, a sua volta, invia i dati rimanenti prima di chiudere il suo lato. La parte "invia i dati rimanenti" deve essere gestita dal programma (anche se la maggior parte delle volte le persone non invierebbero nulla). La chiamata socket.close()è "brutale" in quanto non rispetta questa segnalazione client / server. Il server verrà avvisato di una disconnessione del client solo quando il proprio buffer di output del socket è pieno (perché nessun dato viene acquisito dall'altra parte, che è stata chiusa).
Matthieu

Vedere MSDN per ulteriori informazioni.
Matthieu

Java non forza le applicazioni a chiamare Socket.close () invece di "inviare [ing] i dati rimanenti" prima e non impedisce loro di utilizzare prima un arresto. Socket.close () fa solo quello che fa close (sd). Java non è diverso in questo senso dalla stessa Berkeley Sockets API. Anzi, sotto molti aspetti.
Marchese di Lorne

2
@LeonidUsov Questo è semplicemente sbagliato. Java read()restituisce -1 alla fine del flusso e continua a farlo tutte le volte che lo chiami. AC read()o recv()restituisce zero alla fine del flusso e continua a farlo tutte le volte che lo chiami.
Marchese di Lorne

18

Penso che questa sia più una questione di programmazione socket. Java sta solo seguendo la tradizione della programmazione socket.

Da Wikipedia :

TCP fornisce una consegna ordinata e affidabile di un flusso di byte da un programma su un computer a un altro programma su un altro computer.

Una volta terminato l'handshake, TCP non fa alcuna distinzione tra due endpoint (client e server). Il termine "client" e "server" è principalmente per comodità. Quindi, il "server" potrebbe inviare dati e il "client" potrebbe inviare altri dati contemporaneamente tra loro.

Anche il termine "Chiudi" è fuorviante. C'è solo la dichiarazione FIN, che significa "Non ti invierò altre cose". Ma questo non significa che non ci siano pacchetti in volo, o che l'altro non abbia altro da dire. Se si implementa la posta ordinaria come livello di collegamento dati o se il pacchetto ha percorso percorsi diversi, è possibile che il destinatario riceva i pacchetti nell'ordine sbagliato. TCP sa come risolvere questo problema per te.

Inoltre, come programma, potresti non avere il tempo di continuare a controllare cosa c'è nel buffer. Quindi, a tuo piacimento puoi controllare cosa c'è nel buffer. Tutto sommato, l'attuale implementazione del socket non è così male. Se effettivamente ci fosse isPeerClosed (), questa è una chiamata extra che devi fare ogni volta che vuoi chiamare read.


1
Non credo, puoi testare gli stati nel codice C sia su Windows che su Linux !!! Java per qualche motivo potrebbe non esporre alcune delle cose, come sarebbe bello esporre le funzioni getsockopt che sono su Windows e Linux. In effetti, le risposte seguenti contengono del codice C linux per il lato linux.
Dean Hiller

Non penso che avere il metodo 'isPeerClosed ()' in qualche modo ti impegni a chiamarlo prima di ogni tentativo di lettura. Puoi semplicemente chiamarlo solo quando ne hai esplicitamente bisogno. Sono d'accordo sul fatto che l'implementazione del socket corrente non è così male, anche se richiede di scrivere nel flusso di output se si desidera scoprire se la parte remota del socket è chiusa o meno. Perché se non lo è, devi gestire i tuoi dati scritti anche dall'altra parte, ed è semplicemente una grande quantità di piacere come stare seduti sull'unghia orientata verticalmente;)
Jan Hruby

1
E fa media 'non c'è più pacchetti in volo'. La FIN viene ricevuta dopo ogni dato in volo. Tuttavia ciò che non significa è che il peer ha chiuso il socket per l'input. Devi ** inviare qualcosa * e ottenere un "ripristino della connessione" per rilevarlo. Il FIN avrebbe potuto significare solo un arresto per l'output.
Marchese di Lorne

11

L'API dei socket sottostante non dispone di tale notifica.

Lo stack TCP mittente non invierà comunque il bit FIN fino all'ultimo pacchetto, quindi potrebbero esserci molti dati memorizzati nel buffer da quando l'applicazione mittente ha chiuso logicamente il suo socket prima ancora che i dati vengano inviati. Allo stesso modo, i dati memorizzati nel buffer perché la rete è più veloce dell'applicazione ricevente (non lo so, forse lo stai trasmettendo su una connessione più lenta) potrebbero essere significativi per il ricevitore e non vorresti che l'applicazione ricevente lo scarti solo perché il bit FIN è stato ricevuto dallo stack.


Nel mio esempio di prova (forse dovrei fornirne uno qui ...) non ci sono dati inviati / ricevuti tramite la connessione di proposito. Quindi, sono abbastanza sicuro che lo stack riceva FIN (grazioso) o RST (in alcuni scenari non graziosi). Ciò è confermato anche da netstat.
Alexander

1
Certo: se non è presente alcun buffer, la FIN verrà inviata immediatamente, su un pacchetto altrimenti vuoto (nessun payload). Dopo FIN, tuttavia, non vengono più inviati pacchetti di dati da quella estremità della connessione (continuerà a ACK qualsiasi cosa gli venga inviata).
Mike Dimmick

1
Quello che succede è che i lati della connessione finiscono in <code> CLOSE_WAIT </code> e <code> FIN_WAIT_2 </code> ed è in questo stato <code> isConcected () </code> e <code> isClosed () </code> continua a non vedere che la connessione è stata terminata.
Alexander il

Grazie per i vostri suggerimenti! Penso di capire meglio la questione ora. Ho reso la domanda più specifica (vedi terzo paragrafo): perché non c'è "Socket.isClosing" per verificare le connessioni semichiuse?
Alexander

8

Dal momento che nessuna delle risposte finora risponde completamente alla domanda, riassumo la mia attuale comprensione del problema.

Quando viene stabilita una connessione TCP e un peer chiama close()o shutdownOutput()sul proprio socket, il socket sull'altro lato della connessione passa allo CLOSE_WAITstato. In linea di principio, è possibile scoprire dallo stack TCP se un socket è in CLOSE_WAITstato senza chiamare read/recv(ad esempio, getsockopt()su Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), ma non è così portatile.

La Socketclasse di Java sembra essere progettata per fornire un'astrazione paragonabile a un socket TCP BSD, probabilmente perché questo è il livello di astrazione a cui le persone sono abituate quando programmano applicazioni TCP / IP. I socket BSD sono una generalizzazione che supporta socket diversi da quelli INET (ad esempio, TCP), quindi non forniscono un modo portabile per scoprire lo stato TCP di un socket.

Non esiste un metodo simile isCloseWait() perché le persone abituate a programmare applicazioni TCP al livello di astrazione offerto dai socket BSD non si aspettano che Java fornisca metodi aggiuntivi.


3
Né Java può fornire metodi aggiuntivi, in modo portabile. Forse potrebbero creare un metodo isCloseWait () che restituirebbe falso se la piattaforma non lo supportasse, ma quanti programmatori verrebbero morsi da quel trucco se provassero solo su piattaforme supportate?
effimero

1
sembra che potrebbe essere portatile per me ... Windows ha questo msdn.microsoft.com/en-us/library/windows/desktop/… e Linux questo pubs.opengroup.org/onlinepubs/009695399/functions/…
Dean Hiller

1
Non è che i programmatori ci siano abituati; è che l'interfaccia socket è ciò che è utile ai programmatori. Tieni presente che l'astrazione del socket viene utilizzata per più del protocollo TCP.
Warren Dew

Non esiste un metodo in Java come isCloseWait()perché non è supportato su tutte le piattaforme.
Marchese di Lorne

Il protocollo ident (RFC 1413) consente al server di mantenere la connessione aperta dopo aver inviato una risposta o chiuderla senza inviare altri dati. Un client ident Java potrebbe scegliere di mantenere la connessione aperta per evitare l'handshake a 3 vie nella ricerca successiva, ma come fa a sapere che la connessione è ancora aperta? Dovrebbe solo provare, rispondere a qualsiasi errore riaprendo la connessione? O è un errore di progettazione del protocollo?
david

7

È possibile rilevare se il lato remoto di una connessione socket (TCP) è stato chiuso con il metodo java.net.Socket.sendUrgentData (int) e rilevare l'IOException che genera se il lato remoto è inattivo. Questo è stato testato tra Java-Java e Java-C.

Ciò evita il problema di progettare il protocollo di comunicazione per utilizzare una sorta di meccanismo di ping. Disabilitando OOBInline su un socket (setOOBInline (false), tutti i dati OOB ricevuti vengono eliminati silenziosamente, ma i dati OOB possono comunque essere inviati. Se il lato remoto viene chiuso, viene tentato un ripristino della connessione, non riesce e causa la generazione di alcune IOException .

Se utilizzi effettivamente i dati OOB nel tuo protocollo, il tuo chilometraggio potrebbe variare.


4

lo stack Java IO invia definitivamente FIN quando viene distrutto in seguito a un improvviso smontaggio. Non ha senso che tu non possa rilevarlo, b / c la maggior parte dei client invia il FIN solo se stanno interrompendo la connessione.

... un altro motivo per cui sto davvero iniziando a odiare le classi Java di NIO. Sembra che sia tutto un po 'per metà.


1
inoltre, sembra che ricevo solo la fine del flusso in lettura (-1 ritorno) quando è presente una FIN. Quindi questo è l'unico modo in cui posso vedere per rilevare un arresto sul lato di lettura.

3
Lo puoi rilevare. Ottieni EOS durante la lettura. E Java non invia FIN. TCP lo fa. Java non implementa TCP / IP, utilizza solo l'implementazione della piattaforma.
Marchese di Lorne

4

È un argomento interessante. Ho scavato nel codice Java solo ora per verificare. Dalla mia scoperta, ci sono due problemi distinti: il primo è lo stesso TCP RFC, che consente al socket chiuso da remoto di trasmettere dati in half-duplex, quindi un socket chiuso da remoto è ancora mezzo aperto. Come per l'RFC, RST non chiude la connessione, è necessario inviare un comando ABORT esplicito; quindi Java consente l'invio di dati tramite socket semichiusi

(Esistono due metodi per leggere lo stato di chiusura su entrambi gli endpoint.)

L'altro problema è che l'implementazione dice che questo comportamento è opzionale. Poiché Java si sforza di essere portabile, ha implementato la migliore funzionalità comune. Il mantenimento di una mappa di (OS, implementazione di half duplex) sarebbe stato un problema, immagino.


1
Suppongo che tu stia parlando della RFC 793 ( faqs.org/rfcs/rfc793.html ) Sezione 3.5 Chiusura di una connessione. Non sono sicuro che spieghi il problema, perché entrambe le parti completano il regolare arresto della connessione e finiscono in stati in cui non dovrebbero inviare / ricevere alcun dato.
Alexander

Dipende. quante FIN vedi sulla presa? Inoltre, potrebbe essere un problema specifico della piattaforma: forse Windows risponde a ogni FIN con una FIN e la connessione è chiusa su entrambe le estremità, ma altri sistemi operativi potrebbero non comportarsi in questo modo, ed è qui che sorge il problema 2
Lorenzo Boccaccia

1
No, sfortunatamente non è così. isOutputShutdown e isInputShutdown sono la prima cosa che tutti provano di fronte a questa "scoperta", ma entrambi i metodi restituiscono false. L'ho appena testato su Windows XP e Linux 2.6. I valori di ritorno di tutti e 4 i metodi rimangono gli stessi anche dopo un tentativo di lettura
Alexander

1
Per la cronaca, questo non è half-duplex. Half-duplex significa che solo un lato può inviare alla volta s; entrambe le parti possono ancora inviare.
rbp

1
isInputShutdown e isOutputShutdown testano la fine locale della connessione: sono test per scoprire se hai chiamato shutdownInput o shutdownOutput su questo Socket. Non ti dicono nulla sull'estremità remota della connessione.
Mike Dimmick

3

Questo è un difetto delle classi socket OO di Java (e di tutte le altre che ho esaminato): nessun accesso alla chiamata di sistema select.

Risposta corretta in 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 */  
}  

Puoi fare la stessa cosa con getsocketop(... SOL_SOCKET, SO_ERROR, ...).
David Schwartz

1
Il set di descrittori di file di errore non indica la connessione chiusa. Si prega di leggere il manuale selezionato: 'trannefds - Questo set è controllato per "condizioni eccezionali". In pratica, solo una di queste condizioni è comune: la disponibilità di dati fuori banda (OOB) per la lettura da un socket TCP. " FIN non è dati OOB.
Jan Wrobel

1
Puoi usare la classe 'Selector' per accedere a 'select ()' syscall. Però usa NIO.
Matthieu

1
Non c'è niente di eccezionale in una connessione che è stata interrotta dall'altra parte.
David Schwartz

1
Molte piattaforme, tra cui Java, fanno fornire l'accesso alla chiamata di sistema select ().
Marchese di Lorne

2

Ecco una soluzione alternativa scadente. Usa SSL;) e SSL esegue una stretta stretta di mano durante lo smontaggio in modo da essere avvisato della chiusura del socket (la maggior parte delle implementazioni sembra eseguire un corretto smontaggio della stretta di mano).


2
Come si viene "avvisati" della chiusura del socket quando si utilizza SSL in java?
Dave B

2

La ragione di questo comportamento (che non è specifico di Java) è il fatto che non si ottengono informazioni sullo stato dallo stack TCP. Dopotutto, un socket è solo un altro handle di file e non puoi scoprire se ci sono dati effettivi da leggere da esso senza effettivamente provare a ( select(2)non aiuta lì, segnala solo che puoi provare senza bloccare).

Per ulteriori informazioni, vedere le domande frequenti sul socket Unix .


2
I socket REALbasic (su Mac OS X e Linux) sono basati su socket BSD, ma RB riesce a darti un bel errore 102 quando la connessione viene interrotta dall'altra parte. Quindi sono d'accordo con il poster originale, questo dovrebbe essere possibile ed è zoppo che Java (e Cocoa) non lo forniscano.
Joe Strout

1
@ JoeStrout RB può farlo solo quando fai un po 'di I / O. Non esiste alcuna API che ti fornirà lo stato della connessione senza eseguire operazioni di I / O. Periodo. Questa non è una carenza di Java. È infatti dovuto alla mancanza di un "segnale di linea" in TCP, che è una caratteristica di progettazione deliberata.
Marchese di Lorne

select() ti dice se ci sono dati o EOS disponibili per essere letti senza blocco. "Segnali che puoi provare senza bloccare" non ha senso. Se sei in modalità non bloccante, puoi sempre provare senza bloccare. select()è guidato dai dati nel buffer di ricezione del socket o da una FIN in sospeso o da uno spazio nel buffer di invio del socket.
Marchese di Lorne

@EJP Di cosa si tratta getsockopt(SO_ERROR)? In effetti, anche getpeernameti dirà se la presa è ancora collegata.
David Schwartz

0

Solo le scritture richiedono lo scambio di pacchetti, il che consente di determinare la perdita di connessione. Una soluzione comune consiste nell'usare l'opzione KEEP ALIVE.


Penso che un endpoint possa avviare un grazioso arresto della connessione inviando un pacchetto con FIN impostato, senza scrivere alcun payload.
Alexander

@Alexander Certo che sì, ma questo non è rilevante per questa risposta, che riguarda il rilevamento di una minore connessione.
Marchese di Lorne

-3

Quando si tratta di gestire socket Java semiaperti, si potrebbe voler dare un'occhiata a isInputShutdown () e isOutputShutdown () .


No. Questo ti dice solo come hai chiamato, non come ha chiamato il collega.
Marchese di Lorne

Ti interessa condividere la tua fonte per quella dichiarazione?
JimmyB

3
Ti interessa condividere la tua fonte per il contrario? È la tua dichiarazione. Se hai una prova, facciamola. Affermo che sei sbagliato. Fai l'esperimento e dimostrami che ho torto.
Marchese di Lorne

Tre anni dopo e nessun esperimento. QED
Marchese di Lorne
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.