API de soquete Java: como saber se uma conexão foi fechada?

92

Estou tendo alguns problemas com a API de soquete Java. Estou tentando exibir o número de jogadores atualmente conectados ao meu jogo. É fácil determinar quando um jogador está conectado. No entanto, parece desnecessariamente difícil determinar quando um jogador se desconectou usando a API de soquete.

Chamar isConnected()um soquete que foi desconectado remotamente sempre parece retornar true. Da mesma forma, chamar isClosed()em um soquete que foi fechado remotamente sempre parece retornar false. Eu li que para realmente determinar se um soquete foi fechado ou não, os dados devem ser gravados no fluxo de saída e uma exceção deve ser detectada. Esta parece ser uma maneira muito suja de lidar com essa situação. Teríamos apenas que enviar spam constantemente uma mensagem de lixo pela rede para saber quando um soquete foi fechado.

existe alguma outra solução?

Dan Brouwer
fonte

Respostas:

183

Não há uma API TCP que informe o estado atual da conexão. isConnected()e isClosed()informar o estado atual do seu soquete . Não é a mesma coisa.

  1. isConnected()informa se você conectou este soquete . Você tem, então ele retorna verdadeiro.

  2. isClosed()informa se você fechou este soquete. Até que você tenha, ele retorna falso.

  3. Se o par fechou a conexão de maneira ordenada

    • read() retorna -1
    • readLine() retorna null
    • readXXX()lança EOFExceptionpara qualquer outro XXX.

    • Uma gravação lançará um IOException: 'conexão redefinida por par', eventualmente, sujeito a atrasos no buffer.

  4. Se a conexão caiu por qualquer outro motivo, uma gravação lançará um IOException, eventualmente, como acima, e uma leitura pode fazer a mesma coisa.

  5. Se o par ainda estiver conectado, mas não usando a conexão, um tempo limite de leitura pode ser usado.

  6. Ao contrário do que você pode ler em outro lugar, ClosedChannelExceptionnão diz isso. [Nem faz SocketException: socket closed.] Apenas informa que você fechou o canal e continuou a usá-lo. Em outras palavras, um erro de programação de sua parte. Não indica uma conexão fechada .

  7. Como resultado de alguns experimentos com Java 7 no Windows XP, também parece que se:

    • você está selecionando em OP_READ
    • select() retorna um valor maior que zero
    • o associado SelectionKeyjá é inválido ( key.isValid() == false)

    significa que o par reiniciou a conexão. No entanto, isso pode ser peculiar à versão ou à plataforma do JRE.

Marquês de Lorne
fonte
18
É difícil acreditar que o protocolo TCP, que é orientado a conexões, nem saiba o status de sua conexão ... Será que os caras que vêm com esses protocolos dirigem seus carros de olhos fechados?
PedroD
31
@PedroD Pelo contrário: foi deliberado. Os conjuntos de protocolos anteriores, como o SNA, tinham um 'tom de discagem'. O TCP foi projetado para sobreviver a uma guerra nuclear e, mais trivialmente, a interrupções e interrupções de roteadores: daí a completa ausência de qualquer coisa como tom de discagem, status de conexão etc .; e é também por isso que o keepalive do TCP é descrito nas RFCs como um recurso controverso e porque está sempre desativado por padrão. TCP ainda está conosco. SNA? IPX? ISO? Não. Eles acertaram.
Marquês de Lorne
3
Não acredito que seja uma boa desculpa para esconder essa informação de nós. Saber que a conexão foi perdida não significa necessariamente que o protocolo é menos resistente a falhas, sempre depende do que fazemos com esse conhecimento ... Para mim, o método isBound e isConnected de java são métodos puramente mock, eles não têm uso , mas expressa a necessidade de um ouvinte de evento de conexão ... Mas eu reitero: saber que a conexão foi perdida não torna o protocolo pior. Agora, se os protocolos que você está dizendo mataram a conexão assim que detectaram que ela foi perdida, a história é diferente.
PedroD
21
@Pedro Você não entende. Não é uma 'desculpa'. Não há informações a reter. Não há tom de discagem. O TCP não sabe se a conexão falhou até que você tente fazer algo a respeito. Esse foi o critério fundamental de design.
Marquês de Lorne
2
@EJP obrigado pela sua resposta. Você sabe como verificar se um soquete foi fechado sem "bloquear"? Usar read ou readLine irá bloquear. Existe uma maneira de perceber se o outro soquete foi fechado com o método disponível, ele parece retornar ao 0invés de -1.
insumidade
9

É uma prática geral em vários protocolos de mensagens manter a pulsação uns dos outros (continuar enviando pacotes de ping); o pacote não precisa ser muito grande. O mecanismo de sondagem permitirá que você detecte o cliente desconectado antes mesmo que o TCP descubra em geral (o tempo limite do TCP é muito maior) Envie uma sondagem e espere, digamos, 5 segundos por uma resposta, se você não vir a resposta por digamos 2-3 sondas subsequentes, o seu aparelho é desconectado.

Além disso, questão relacionada

Kalpak Gadre
fonte
6
É 'prática geral' em alguns protocolos. Observo que o HTTP, o protocolo de aplicação mais utilizado no planeta, não possui operação PING.
Marquês de Lorne
2
@ user207421 porque HTTP / 1 é um protocolo único. Você envia um pedido e recebe uma resposta. E você está pronto. O soquete está fechado. A extensão Websocket não tem operação PING, assim como HTTP / 2.
Michał Zabielski
Eu recomendei batimentos cardíacos com um único byte, se possível :)
Stefan Reich
@ MichałZabielski É porque os designers do HTTP não o especificaram. Você não sabe por que não. HTTP / 1.1 não é um protocolo único. O soquete não está fechado: as conexões HTTP são persistentes por padrão. FTP, SMTP, POP3, IMAP, TLS, ... não tem pulsações.
Marquês de Lorne
2

Vejo a outra resposta que acabei de postar, mas acho que você é interativo com os clientes que jogam seu jogo, então posso propor outra abordagem (enquanto BufferedReader é definitivamente válido em alguns casos).

Se você quiser ... você pode delegar a responsabilidade do "registro" ao cliente. Ou seja, você teria uma coleção de usuários conectados com um carimbo de data / hora na última mensagem recebida de cada um ... se um cliente expirar, você forçaria um novo registro do cliente, mas isso leva à cotação e ideia abaixo.

Eu li que, para realmente determinar se um soquete foi fechado ou não, os dados devem ser gravados no fluxo de saída e uma exceção deve ser detectada. Esta parece ser uma maneira muito suja de lidar com essa situação.

Se seu código Java não fechou / desconectou o Socket, de que outra forma você seria notificado de que o host remoto fechou sua conexão? Em última análise, seu try / catch está fazendo quase a mesma coisa que um poller que escuta eventos no soquete ACTUAL estaria fazendo. Considere o seguinte:

  • seu sistema local pode fechar seu soquete sem notificá-lo ... isso é apenas a implementação do Socket (ou seja, ele não faz o polling do hardware / driver / firmware / qualquer que seja a mudança de estado).
  • new Socket (Proxy p) ... há várias partes (6 endpoints na verdade) que podem estar fechando a conexão com você ...

Acho que uma das características das linguagens abstratas é que você é abstraído das minúcias. Pense na palavra-chave using em C # (try / finally) para SqlConnection s ou qualquer outra coisa ... é apenas o custo de fazer negócios ... Acho que try / catch / finally é o padrão aceito e necessário para uso de Socket.

Scottley
fonte
Seu sistema local não pode 'fechar seu soquete', com ou sem notificá-lo. Sua pergunta começando com 'se seu código Java não fechou / desconectou o soquete ...?' também não faz sentido.
Marquês de Lorne
1
O que exatamente impede o sistema (Linux, por exemplo) de forçar o fechamento do soquete? Ainda é possível fazer isso a partir de 'gdb' usando o comando 'call close'?
Andrey Lebedenko
@AndreyLebedenko Nada 'impede exatamente', mas não o faz.
Marquês de Lorne
@EJB quem sabe então?
Andrey Lebedenko
@AndreyLebedenko Ninguém faz isso. Apenas o aplicativo pode fechar seus próprios soquetes, a menos que saia sem fazer isso, caso em que o sistema operacional será limpo.
Marquês de Lorne
1

Acho que essa é a natureza das conexões tcp, nos padrões que leva cerca de 6 minutos de silêncio na transmissão antes de concluirmos que nossa conexão acabou! Portanto, não acho que você possa encontrar uma solução exata para este problema. Talvez a melhor maneira seja escrever algum código útil para adivinhar quando o servidor deve supor que a conexão do usuário foi fechada.

Ehsan Khodarahmi
fonte
2
Pode demorar muito mais do que isso, até duas horas.
Marquês de Lorne
0

Como @ user207421 diz que não há como saber o estado atual da conexão devido ao modelo de arquitetura de protocolo TCP / IP. Portanto, o servidor deve notá-lo antes de fechar a conexão ou você verificar por si mesmo.
Este é um exemplo simples que mostra como saber se o soquete foi fechado pelo servidor:

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 !");
ucMedia
fonte
0

Eu enfrentei um problema semelhante. No meu caso, o cliente deve enviar dados periodicamente. Espero que você tenha o mesmo requisito. Então eu defino SO_TIMEOUT socket.setSoTimeout(1000 * 60 * 5);que é lançado java.net.SocketTimeoutExceptionquando o tempo especificado expira. Assim, posso detectar facilmente o cliente morto.

hurelhuyag
fonte
-2

É assim que eu lido com isso

 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 o result.code == null

Petar Ceho
fonte
Você não precisa ligar readLine()duas vezes. Você já sabe que era nulo no elsebloco.
Marquês de Lorne,
-4

No Linux, ao escrever () em um socket que o outro lado, desconhecido para você, fechou irá provocar um sinal / exceção SIGPIPE como você quiser chamá-lo. No entanto, se você não quiser ser pego pelo SIGPIPE, pode usar send () com o sinalizador MSG_NOSIGNAL. A chamada send () retornará com -1 e neste caso você pode verificar errno que lhe dirá que você tentou escrever um pipe quebrado (neste caso um socket) com o valor EPIPE que de acordo com errno.h é equivalente a 32. Como reação ao EPIPE, você pode voltar e tentar reabrir o soquete e tentar enviar suas informações novamente.

MikeK
fonte
A send()chamada retornará -1 apenas se os dados de saída forem armazenados em buffer por tempo suficiente para que os temporizadores de envio expirem. É quase certo que isso não acontecerá no primeiro envio após a desconexão, devido ao armazenamento em buffer em ambas as extremidades e à natureza assíncrona dos send()bastidores.
Marquês de Lorne