API del socket Java: come sapere se una connessione è stata chiusa?


92

Sto riscontrando alcuni problemi con l'API del socket Java. Sto cercando di visualizzare il numero di giocatori attualmente collegati al mio gioco. È facile determinare quando un giocatore si è connesso. Tuttavia, sembra inutilmente difficile determinare quando un giocatore si è disconnesso utilizzando l'API socket.

La chiamata isConnected()su una presa che è stata scollegata da remoto sembra sempre tornare true. Allo stesso modo, la chiamata isClosed()su un socket che è stato chiuso in remoto sembra sempre tornare false. Ho letto che per determinare effettivamente se un socket è stato chiuso o meno, i dati devono essere scritti nel flusso di output e deve essere rilevata un'eccezione. Sembra un modo davvero impuro per gestire questa situazione. Dovremmo semplicemente spammare costantemente un messaggio spazzatura sulla rete per sapere quando un socket si è chiuso.

c'è un'altra soluzione?

Risposte:


183

Non esiste un'API TCP che ti dirà lo stato corrente della connessione. isConnected()e isClosed()dirti lo stato corrente del tuo socket . Non è la stessa cosa.

  1. isConnected()ti dice se hai collegato questa presa . Sì, quindi restituisce vero.

  2. isClosed()ti dice se hai chiuso questo socket. Fino a quando non lo hai, restituisce false.

  3. Se il peer ha chiuso la connessione in modo ordinato

    • read() restituisce -1
    • readLine() ritorna null
    • readXXX()lanci EOFExceptionper qualsiasi altro XXX.

    • Una scrittura genererà un IOException: "connessione ripristinata dal peer", eventualmente, soggetta a ritardi nel buffering.

  4. Se la connessione si è interrotta per qualsiasi altro motivo, una scrittura genererà un IOException, alla fine, come sopra, e una lettura potrebbe fare la stessa cosa.

  5. Se il peer è ancora connesso ma non utilizza la connessione, è possibile utilizzare un timeout di lettura.

  6. Contrariamente a quanto potresti leggere altrove, ClosedChannelExceptionnon te lo dice. [Nessuno dei due SocketException: socket closed.] Ti dice solo che hai chiuso il canale e poi hai continuato a usarlo. In altre parole, un errore di programmazione da parte tua. Non indica una connessione chiusa .

  7. A seguito di alcuni esperimenti con Java 7 su Windows XP risulta inoltre che se:

    • stai selezionando OP_READ
    • select() restituisce un valore maggiore di zero
    • l'associato SelectionKeyè già non valido ( key.isValid() == false)

    significa che il peer ha ripristinato la connessione. Tuttavia questo può essere peculiare della versione o della piattaforma JRE.


18
È difficile credere che il protocollo TCP, che è orientato alla connessione, non possa nemmeno conoscere lo stato della sua connessione ... I ragazzi che escogitano questo protocollo guidano le loro auto con gli occhi chiusi?
PedroD

31
@PedroD Al contrario: è stato deliberato. Le suite di protocollo precedenti come SNA avevano un "segnale di linea". Il protocollo TCP è stato progettato per sopravvivere a una guerra nucleare e, più banalmente, a problemi di funzionamento del router: da qui la completa assenza di qualsiasi cosa come un segnale di linea, lo stato della connessione, ecc .; ed è anche il motivo per cui TCP keepalive è descritto nelle RFC come una caratteristica controversa, e perché è sempre disattivato per impostazione predefinita. TCP è ancora con noi. SNA? IPX? ISO? Non. Hanno capito bene.
Marchese di Lorne

3
Non credo che sia una buona scusa per nasconderci queste informazioni. Sapere che la connessione è andata persa non significa necessariamente che il protocollo sia meno resistente ai guasti, dipende sempre da quello che facciamo con quella conoscenza ... Per me il metodo isBound e isConnected da java sono puri metodi fittizi, non servono , ma esprimo la necessità di un listener di eventi di connessione ... Ma ribadisco: sapere che la connessione è andata persa non peggiora il protocollo. Ora, se quei protocolli che stai dicendo hanno interrotto la connessione non appena hanno rilevato che era persa, questa è una storia diversa.
PedroD

21
@Pedro Non capisci. Non è una "scusa". Non ci sono informazioni da nascondere. Non c'è il segnale di linea. TCP non sa se la connessione è fallita finché non si tenta di farci qualcosa. Questo era il criterio di progettazione fondamentale.
Marchese di Lorne

2
@EJP grazie per la tua risposta. Sapete forse come controllare se un socket è stato chiuso senza "bloccarlo"? L'utilizzo di read o readLine verrà bloccato. C'è un modo per rendersi conto che se l'altro socket è stato chiuso con il metodo disponibile, sembra tornare 0invece di -1.
insumity

9

È prassi generale in vari protocolli di messaggistica continuare a battere il cuore a vicenda (continuare a inviare pacchetti ping), non è necessario che il pacchetto sia molto grande. Il meccanismo di sondaggio ti permetterà di rilevare il client disconnesso anche prima che TCP lo capisca in generale (il timeout TCP è molto più alto) Invia un sondaggio e attendi 5 secondi per una risposta, se non vedi la risposta per dire 2-3 sondaggi successivi, il lettore viene disconnesso.

Inoltre, domanda correlata


6
È "pratica generale" in alcuni protocolli. Noto che HTTP, il protocollo applicativo più utilizzato al mondo, non ha un'operazione PING.
Marchese di Lorne

2
@ user207421 perché HTTP / 1 è un protocollo one shot. Invia richiesta, ricevi risposta. E hai finito. La presa è chiusa. L'estensione Websocket ha un'operazione PING, così come HTTP / 2.
Michał Zabielski

Ho consigliato di battere il cuore con un singolo byte, se possibile :)
Stefan Reich

@ MichałZabielski È perché i progettisti di HTTP non lo hanno specificato. Non sai perché no. HTTP / 1.1 non è un protocollo one shot. Il socket non è chiuso: le connessioni HTTP sono persistenti per impostazione predefinita. FTP, SMTP, POP3, IMAP, TLS, ... non hanno battiti cardiaci.
Marchese di Lorne

2

Vedo l'altra risposta appena pubblicata, ma penso che tu sia interattivo con i clienti che giocano al tuo gioco, quindi posso porre un altro approccio (mentre BufferedReader è sicuramente valido in alcuni casi).

Se volessi ... potresti delegare la responsabilità di "registrazione" al cliente. Cioè avresti una raccolta di utenti connessi con un timestamp sull'ultimo messaggio ricevuto da ciascuno ... se un client scade, forzerai una nuova registrazione del cliente, ma questo porta al preventivo e all'idea di seguito.

Ho letto che per determinare effettivamente se un socket è stato chiuso o meno, i dati devono essere scritti nel flusso di output e deve essere rilevata un'eccezione. Sembra un modo davvero impuro per gestire questa situazione.

Se il tuo codice Java non ha chiuso / disconnesso il Socket, in quale altro modo verresti informato che l'host remoto ha chiuso la tua connessione? In definitiva, il tuo try / catch sta facendo più o meno la stessa cosa che farebbe un poller che ascolta gli eventi sul socket ACTUAL. Considera quanto segue:

  • il tuo sistema locale potrebbe chiudere il tuo socket senza avvisarti ... questa è solo l'implementazione di Socket (cioè non esegue il polling dell'hardware / driver / firmware / qualunque cosa per il cambiamento di stato).
  • nuovo Socket (Proxy p) ... ci sono più parti (6 endpoint in realtà) che potrebbero chiudere la connessione su di te ...

Penso che una delle caratteristiche dei linguaggi astratti sia che sei astratto dalla minuzia. Pensa alla parola chiave using in C # (prova / finalmente) per SqlConnection o qualsiasi altra cosa ... è solo il costo per fare affari ... Penso che try / catch / finalmente sia il modello accettato e necessario per l'uso di Socket.


Il tuo sistema locale non può "chiudere il tuo socket", con o senza avvisarti. La tua domanda che inizia "se il tuo codice Java non ha chiuso / disconnesso il socket ...?" non ha alcun senso neanche.
Marchese di Lorne

1
Cosa impedisce esattamente al sistema (ad esempio Linux) di forzare la chiusura del socket? È ancora possibile farlo da "gdb" utilizzando il comando "call close"?
Andrey Lebedenko

@AndreyLebedenko Niente "lo impedisce esattamente", ma non lo fa.
Marchese di Lorne

@EJB chi lo fa allora?
Andrey Lebedenko

@AndreyLebedenko Nessuno lo fa. Solo l'applicazione può chiudere i propri socket, a meno che non esca senza farlo, nel qual caso il sistema operativo verrà pulito.
Marchese di Lorne

1

Penso che questa sia la natura delle connessioni TCP, in quanto standard ci vogliono circa 6 minuti di silenzio in trasmissione prima di concludere che la connessione è andata! Quindi non penso che tu possa trovare una soluzione esatta per questo problema. Forse il modo migliore è scrivere del codice utile per indovinare quando il server dovrebbe supporre che una connessione utente sia chiusa.


2
Può richiedere molto di più, fino a due ore.
Marchese di Lorne

0

Come dice @ user207421, non c'è modo di conoscere lo stato corrente della connessione a causa del modello di architettura del protocollo TCP / IP. Quindi il server deve notarti prima di chiudere la connessione o tu lo controlli da solo.
Questo è un semplice esempio che mostra come sapere che il socket è chiuso dal server:

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

Ho affrontato un problema simile. Nel mio caso il cliente deve inviare periodicamente i dati. Spero che tu abbia lo stesso requisito. Quindi ho impostato SO_TIMEOUT socket.setSoTimeout(1000 * 60 * 5);che viene lanciato java.net.SocketTimeoutExceptionquando il tempo specificato è scaduto. Quindi posso rilevare facilmente il cliente morto.


-2

Ecco come lo gestisco

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

se il codice result.code == null


Non devi chiamare readLine()due volte. Sai già che era nullo nel elseblocco.
Marchese di Lorne

-4

Su Linux, quando write () in un socket chiuso dall'altra parte, a voi sconosciuta, provocherà un segnale / eccezione SIGPIPE comunque vogliate chiamarlo. Tuttavia, se non vuoi essere scoperto da SIGPIPE puoi usare send () con il flag MSG_NOSIGNAL. La chiamata send () tornerà con -1 e in questo caso puoi controllare errno che ti dirà che hai provato a scrivere una pipe rotta (in questo caso un socket) con il valore EPIPE che secondo errno.h è equivalente a 32. Come reazione all'EPIPE potresti tornare indietro e provare a riaprire il socket e provare a inviare nuovamente le tue informazioni.


La send()chiamata restituirà -1 solo se i dati in uscita sono stati bufferizzati per un tempo sufficientemente lungo da far scadere i timer di invio. Quasi certamente non accadrà al primo invio dopo la disconnessione, a causa del buffering su entrambe le estremità e della natura asincrona di send()sotto il cofano.
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.