Por que é impossível, sem tentar E / S, detectar que o soquete TCP foi fechado normalmente pelo par?

92

Seguindo uma pergunta recente , eu me pergunto por que é impossível em Java, sem tentar ler / gravar em um soquete TCP, detectar que o soquete foi normalmente fechado pelo par? Este parece ser o caso, independentemente de se usar o pré-NIO Socketou o NIO SocketChannel.

Quando um par fecha normalmente uma conexão TCP, as pilhas TCP em ambos os lados da conexão sabem sobre o fato. O lado do servidor (aquele que inicia o desligamento) termina no estado FIN_WAIT2, enquanto o lado do cliente (aquele que não responde explicitamente ao desligamento) termina no estado CLOSE_WAIT. Por que não existe um método em Socketou SocketChannelque possa consultar a pilha TCP para ver se a conexão TCP subjacente foi encerrada? É que a pilha TCP não fornece essas informações de status? Ou é uma decisão de design para evitar uma chamada dispendiosa para o kernel?

Com a ajuda dos usuários que já postaram algumas respostas para essa pergunta, acho que vejo de onde o problema pode estar vindo. O lado que não fecha explicitamente a conexão termina no estado TCP, CLOSE_WAITo que significa que a conexão está em processo de encerramento e espera que o lado execute sua própria CLOSEoperação. Suponho que seja justo que isConnectedvolte truee isClosedvolte false, mas por que não existe algo assim isClosing?

Abaixo estão as classes de teste que usam soquetes pré-NIO. Mas resultados idênticos são obtidos 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 o cliente de teste se conecta ao servidor de teste, a saída permanece inalterada, mesmo após o servidor iniciar o desligamento da conexão:

connected: true, closed: false
connected: true, closed: false
...
Alexandre
fonte
Pensei que já mencionaria: O protocolo SCTP não tem esse "problema". O SCTP não tem um estado semi-fechado como o TCP, em outras palavras, um lado não pode continuar enviando dados enquanto a outra extremidade fechou seu soquete de envio. Isso deve tornar as coisas mais fáceis.
Tarnay Kálmán
2
Temos duas caixas de correio (Sockets) ........................................... ...... As caixas de correio enviam mensagens uma para a outra usando o RoyalMail (IP), esqueça o TCP ............................. .................... Tudo está bem e elegante, as caixas de correio podem enviar / receber e-mail entre si (cargas de lag recentemente) enviando sem receber nenhum problema. ............. Se uma caixa de correio fosse atropelada por um caminhão e falhasse .... como a outra caixa de correio saberia? Teria de ser notificado pelo Royal Mail, que por sua vez não saberia até que ele tentasse enviar / receber e-mail daquela caixa de correio com falha .. ...... erm .....
divinci
Se você não vai ler do soquete ou gravar no soquete, por que você se importa? E se você vai ler do socket ou escrever nele, por que fazer uma verificação extra? Qual é o caso de uso?
David Schwartz
2
Socket.closenão é um encerramento elegante.
user253751
@immibis Certamente é um fechamento normal, a menos que haja dados não lidos no buffer de recebimento do soquete ou você tenha mexido com SO_LINGER.
Marquês de Lorne

Respostas:

29

Tenho usado Sockets com frequência, principalmente com Seletores, e embora não seja um especialista em OSI de Rede, no meu entendimento, chamar shutdownOutput()um Socket na verdade envia algo na rede (FIN) que ativa meu Seletor do outro lado (mesmo comportamento em C língua). Aqui você tem detecção : realmente detectando uma operação de leitura que falhará quando você tentar.

No código que você fornecer, fechar o socket irá desligar os fluxos de entrada e saída, sem possibilidades de leitura dos dados que possam estar disponíveis, perdendo-os, portanto. O Socket.close()método Java executa uma desconexão "elegante" (oposta ao que eu pensava inicialmente) em que os dados deixados no fluxo de saída serão enviados seguidos por um FIN para sinalizar seu fechamento. O FIN será reconhecido pelo outro lado, como qualquer pacote regular faria 1 .

Se você precisar esperar que o outro lado feche seu soquete, você precisa esperar por seu FIN. E para conseguir isso, você tem que detectar Socket.getInputStream().read() < 0, o que significa que você não deve fechar o seu soquete, pois fecharia o seuInputStream .

Pelo que fiz em C e agora em Java, conseguir esse fechamento sincronizado deve ser feito assim:

  1. Saída de desligamento do soquete (envia FIN na outra extremidade, esta é a última coisa que será enviada por este soquete). A entrada ainda está aberta para que você possa read()detectar o controle remotoclose()
  2. Leia o soquete InputStreamaté recebermos a resposta FIN da outra extremidade (como ele detectará o FIN, ele passará pelo mesmo processo de diconnection). Isso é importante em alguns sistemas operacionais, pois eles não fecham o soquete, desde que um de seus buffer ainda contenha dados. Eles são chamados de soquete "fantasma" e usam números de descritor no sistema operacional (isso pode não ser mais um problema com o sistema operacional moderno)
  3. Feche o soquete (chamando Socket.close()ou fechando seu InputStreamou OutputStream)

Conforme mostrado no seguinte snippet 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
}

É claro que ambos os lados devem usar a mesma forma de fechamento, ou a parte de envio pode estar sempre enviando dados suficientes para manter o whileloop ocupado (por exemplo, se a parte de envio está apenas enviando dados e nunca lendo para detectar o encerramento da conexão. O que é desajeitado, mas você pode não ter controle sobre isso).

Como @WarrenDew apontou em seu comentário, o descarte dos dados no programa (camada de aplicativo) induz uma desconexão não normal na camada de aplicativo: embora todos os dados tenham sido recebidos na camada TCP (o whileloop), eles são descartados.

1 : Em " Fundamental Networking in Java ": veja a fig. 3.3 p.45, e todo §3.7, pp 43-48

Matthieu
fonte
Java executa um fechamento elegante. Não é 'brutal'.
Marquês de Lorne,
2
@EJP, "desconexão normal" é uma troca específica que ocorre ao nível do TCP, onde o Cliente deve sinalizar sua vontade de se desconectar ao Servidor, que, por sua vez, envia os dados restantes antes de fechar seu lado. A parte de "envio de dados restantes" deve ser tratada pelo programa (embora as pessoas não enviem nada na maioria das vezes). A chamada socket.close()é "brutal" por não respeitar esta sinalização Cliente / Servidor. O servidor seria notificado de uma desconexão do cliente somente quando seu próprio buffer de saída de soquete estivesse cheio (porque nenhum dado está sendo reconhecido pelo outro lado, que foi fechado).
Matthieu
Consulte MSDN para obter mais informações.
Matthieu
Java não força os aplicativos a chamarem Socket.close () em vez de 'enviar os dados restantes' primeiro, e não os impede de usar o desligamento primeiro. Socket.close () apenas faz o que close (sd) faz. Java não é diferente a esse respeito da própria API Berkeley Sockets. Na verdade, em muitos aspectos.
Marquês de Lorne
2
@LeonidUsov Isso é simplesmente incorreto. Java read()retorna -1 no final do fluxo e continua a fazê-lo quantas vezes você o chamar. AC read()or recv()retorna zero no final do stream e continua a fazê-lo quantas vezes você chamá-lo.
Marquês de Lorne
18

Eu acho que isso é mais uma questão de programação de socket. Java está apenas seguindo a tradição de programação de socket.

Da Wikipedia :

O TCP fornece entrega confiável e ordenada de um fluxo de bytes de um programa em um computador para outro programa em outro computador.

Depois que o handshake é feito, o TCP não faz nenhuma distinção entre dois pontos de extremidade (cliente e servidor). O termo "cliente" e "servidor" é principalmente para conveniência. Assim, o "servidor" pode estar enviando dados e o "cliente" pode estar enviando outros dados simultaneamente um para o outro.

O termo "Fechar" também é enganoso. Existe apenas a declaração FIN, que significa "Não vou enviar mais nada a você". Mas isso não significa que não haja pacotes em vôo ou que o outro não tenha mais nada a dizer. Se você implementar o correio tradicional como a camada de enlace de dados, ou se o seu pacote viajou por rotas diferentes, é possível que o destinatário receba os pacotes na ordem errada. O TCP sabe como consertar isso para você.

Além disso, você, como programa, pode não ter tempo para verificar o que está no buffer. Assim, conforme sua conveniência, você pode verificar o que está no buffer. Em suma, a implementação de soquete atual não é tão ruim. Se realmente houvesse isPeerClosed (), essa chamada extra que você terá que fazer toda vez que quiser chamar read.

Eugene Yokota
fonte
1
Acho que não, você pode testar estados no código C no Windows e no Linux !!! Java por algum motivo pode não expor algumas das coisas, como seria bom expor as funções getsockopt que estão no Windows e no Linux. Na verdade, as respostas abaixo têm algum código Linux C para o lado Linux.
Dean Hiller
Não acho que ter o método 'isPeerClosed ()' comprometa você a chamá-lo antes de cada tentativa de leitura. Você pode simplesmente chamá-lo apenas quando você explicitamente precisar dele. Concordo que a implementação atual do soquete não é tão ruim, embora exija que você grave no fluxo de saída se quiser descobrir se a parte remota do soquete está fechada ou não. Porque se não for, você terá que lidar com seus dados escritos do outro lado também, e é simplesmente tão grande prazer quanto sentar no prego orientado verticalmente;)
Jan Hruby
1
Ele faz média 'não há mais pacotes em vôo'. O FIN é recebido após qualquer dado em vôo. No entanto, o que isso não significa é que o par fechou o soquete para entrada. Você tem que ** enviar algo * e obter um 'reset de conexão' para detectar isso. O FIN poderia significar apenas um desligamento para a produção.
Marquês de Lorne
11

A API de sockets subjacente não tem essa notificação.

De qualquer forma, a pilha TCP de envio não enviará o bit FIN até o último pacote, portanto, pode haver muitos dados armazenados em buffer a partir do momento em que o aplicativo de envio fechou logicamente seu soquete antes mesmo dos dados serem enviados. Da mesma forma, os dados armazenados em buffer porque a rede é mais rápida do que o aplicativo de recebimento (não sei, talvez você esteja retransmitindo-os por uma conexão mais lenta) podem ser significativos para o receptor e você não gostaria que o aplicativo de recebimento os descarte apenas porque o bit FIN foi recebido pela pilha.

Mike Dimmick
fonte
No meu exemplo de teste (talvez eu deva fornecer um aqui ...) não há dados enviados / recebidos pela conexão de propósito. Portanto, tenho quase certeza de que a pilha recebe FIN (normal) ou RST (em alguns cenários não normais). Isso também é confirmado pelo netstat.
Alexander
1
Claro - se nada for armazenado em buffer, o FIN será enviado imediatamente, em um pacote vazio (sem carga útil). Depois de FIN, porém, nenhum pacote de dados mais é enviado por esse final da conexão (ele ainda ACK qualquer coisa enviada a ele).
Mike Dimmick
1
O que acontece é que os lados da conexão terminam em <code> CLOSE_WAIT </code> e <code> FIN_WAIT_2 </code> e é neste estado o <code> isConcected () </code> e <code> isClosed () </code> ainda não vê que a conexão foi encerrada.
Alexander
Obrigado por suas sugestões! Acho que entendo melhor o problema agora. Fiz a pergunta mais específica (veja o terceiro parágrafo): por que não existe "Socket.isClosing" para testar conexões semifechadas?
Alexander
8

Como nenhuma das respostas até agora respondeu totalmente à pergunta, estou resumindo meu entendimento atual do problema.

Quando uma conexão TCP é estabelecida e um par faz uma chamada close()ou shutdownOutput()em seu soquete, o soquete do outro lado da conexão muda para o CLOSE_WAITestado. Em princípio, é possível descobrir na pilha TCP se um soquete está no CLOSE_WAITestado sem chamar read/recv(por exemplo, getsockopt()no Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), mas isso não portátil.

A Socketclasse Java parece ter sido projetada para fornecer uma abstração comparável a um soquete BSD TCP, provavelmente porque este é o nível de abstração com o qual as pessoas estão acostumadas ao programar aplicativos TCP / IP. Os sockets BSD são uma generalização que suporta sockets diferentes de INET (por exemplo, TCP), então eles não fornecem uma maneira portátil de descobrir o estado TCP de um socket.

Não há método isCloseWait()igual, porque as pessoas acostumadas a programar aplicativos TCP no nível de abstração oferecido pelos soquetes BSD não esperam que o Java forneça nenhum método extra.

Alexandre
fonte
3
Nem o Java pode fornecer qualquer método extra, portável. Talvez eles pudessem fazer um método isCloseWait () que retornaria falso se a plataforma não o suportasse, mas quantos programadores seriam pegos por esse pegadinho se testassem apenas em plataformas suportadas?
efemiente de
1
parece que pode ser portátil para mim ... o windows tem este msdn.microsoft.com/en-us/library/windows/desktop/… e linux este pubs.opengroup.org/onlinepubs/009695399/functions/…
Dean Hiller
1
Não é que os programadores estejam acostumados; é que a interface de socket é o que é útil para os programadores. Lembre-se de que a abstração de soquete é usada para mais do que o protocolo TCP.
Warren Dew
Não há nenhum método em Java como isCloseWait()porque não é compatível com todas as plataformas.
Marquês de Lorne
O protocolo ident (RFC 1413) permite que o servidor mantenha a conexão aberta após o envio de uma resposta ou feche-a sem enviar mais dados. Um cliente de identificação Java pode escolher manter a conexão aberta para evitar o handshake de 3 vias na próxima pesquisa, mas como ele sabe que a conexão ainda está aberta? Ele deve apenas tentar responder a qualquer erro reabrindo a conexão? Ou isso é um erro de projeto de protocolo?
David
7

Detectar se o lado remoto de uma conexão de soquete (TCP) foi fechado pode ser feito com o método java.net.Socket.sendUrgentData (int) e capturar a IOException lançada se o lado remoto estiver inativo. Isso foi testado entre Java-Java e Java-C.

Isso evita o problema de projetar o protocolo de comunicação para usar algum tipo de mecanismo de ping. Ao desativar OOBInline em um soquete (setOOBInline (false), quaisquer dados OOB recebidos são silenciosamente descartados, mas os dados OOB ainda podem ser enviados. Se o lado remoto for fechado, uma redefinição de conexão é tentada, falha e faz com que alguma IOException seja lançada .

Se você realmente usa dados OOB em seu protocolo, sua milhagem pode variar.

Tio Per
fonte
4

a pilha Java IO definitivamente envia FIN quando é destruída em uma desmontagem abrupta. Simplesmente não faz sentido que você não possa detectar isso, porque a maioria dos clientes só envia o FIN se estiverem encerrando a conexão.

... outra razão pela qual estou realmente começando a odiar as classes NIO Java. Parece que tudo é meio burro.


fonte
1
além disso, parece que eu só obtenho um final de fluxo na leitura (retorno -1) quando um FIN está presente. Portanto, esta é a única maneira que vejo para detectar um desligamento no lado da leitura.
3
Você pode detectá-lo. Você obtém EOS ao ler. E o Java não envia FINs. TCP faz isso. Java não implementa TCP / IP, apenas usa a implementação da plataforma.
Marquês de Lorne
4

É um assunto interessante. Eu vasculhei o código java agora para verificar. Pelo que descobri, há dois problemas distintos: o primeiro é o próprio TCP RFC, que permite que o soquete fechado remotamente transmita dados em half-duplex, portanto, um soquete fechado remotamente ainda fica semiaberto. De acordo com o RFC, o RST não fecha a conexão, você precisa enviar um comando ABORT explícito; então Java permite o envio de dados por meio de soquete semi-fechado

(Existem dois métodos para ler o status de fechamento em ambos os terminais.)

O outro problema é que a implementação diz que esse comportamento é opcional. Como o Java se esforça para ser portátil, eles implementaram o melhor recurso comum. Manter um mapa de (SO, implementação de half duplex) teria sido um problema, eu acho.

Lorenzo Boccaccia
fonte
1
Suponho que você esteja falando sobre RFC 793 ( faqs.org/rfcs/rfc793.html ) Seção 3.5 Fechando uma conexão. Não tenho certeza se isso explica o problema, porque ambos os lados concluem o desligamento normal da conexão e terminam em estados em que não deveriam enviar / receber nenhum dado.
Alexander
Depende. quantos FIN você vê no soquete? Além disso, pode ser um problema específico da plataforma: talvez o Windows responda a cada FIN com um FIN e a conexão seja fechada em ambas as extremidades, mas outros sistemas operacionais podem não se comportar dessa maneira, e é aí que surge o problema 2
Lorenzo Boccaccia
1
Não, infelizmente não é o caso. isOutputShutdown e isInputShutdown é a primeira coisa que todos tentam ao se deparar com essa "descoberta", mas ambos os métodos retornam falso. Acabei de testá-lo no Windows XP e Linux 2.6. Os valores de retorno de todos os 4 métodos permanecem os mesmos, mesmo após uma tentativa de leitura
Alexander
1
Só para constar, não é half-duplex. Half-duplex significa que apenas um lado pode enviar por vez; ambos os lados ainda podem enviar.
rbp
1
isInputShutdown e isOutputShutdown testam a extremidade local da conexão - são testes para descobrir se você chamou shutdownInput ou shutdownOutput neste Socket. Eles não dizem nada sobre a extremidade remota da conexão.
Mike Dimmick
3

Esta é uma falha das classes de soquete OO do Java (e de todos os outros que eu examinei) - nenhum acesso à chamada de sistema select.

Resposta correta em 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 */  
}  
Joshua
fonte
Você pode fazer a mesma coisa com getsocketop(... SOL_SOCKET, SO_ERROR, ...).
David Schwartz,
1
O conjunto do descritor de arquivo de erro não indica conexão fechada. Leia o manual selecionado: 'exceptfds - Este conjunto é observado para "condições excepcionais". Na prática, apenas uma dessas condições excecionais é comum: a disponibilidade de dados fora de banda (OOB) para leitura de um soquete TCP. ' FIN não são dados OOB.
Jan Wrobel,
1
Você pode usar a classe 'Seletor' para acessar a escala 'select ()'. No entanto, ele usa NIO.
Matthieu
1
Não há nada de excepcional em uma conexão que foi encerrada pelo outro lado.
David Schwartz
1
Muitas plataformas, incluindo Java, fazer fornecer acesso à chamada de sistema select ().
Marquês de Lorne
2

Aqui está uma solução alternativa. Use SSL;) e o SSL faz um handshake de fechamento na desmontagem para que você seja notificado sobre o fechamento do soquete (a maioria das implementações parece fazer uma desmontagem de handshake de propriedade).

Dean Hiller
fonte
2
Como alguém é "notificado" sobre o fechamento do soquete ao usar SSL em java?
Dave B
2

A razão para esse comportamento (que não é específico do Java) é o fato de que você não obtém nenhuma informação de status da pilha TCP. Afinal, um soquete é apenas outro identificador de arquivo e você não pode descobrir se há dados reais para ler dele sem realmente tentar ( select(2)não vai ajudar nisso, ele apenas indica que você pode tentar sem bloquear).

Para obter mais informações, consulte o FAQ sobre soquetes Unix .

WMR
fonte
2
Sockets REALbasic (no Mac OS X e Linux) são baseados em sockets BSD, mas RB consegue dar a você um bom erro 102 quando a conexão é interrompida pelo outro lado. Portanto, concordo com o autor da postagem original, isso deveria ser possível e é lamentável que o Java (e o Cocoa) não forneçam isso.
Joe Strout
1
@JoeStrout RB só pode fazer isso quando você faz algum I / O. Não há API que forneça o status da conexão sem fazer I / O. Período. Esta não é uma deficiência do Java. Na verdade, é devido à falta de um 'tom de discagem' no TCP, que é um recurso de design deliberado.
Marquês de Lorne
select() informa se há dados ou EOS disponíveis para serem lidos sem bloqueio. 'Sinais que você pode tentar sem bloquear' não tem sentido. Se você estiver no modo sem bloqueio, sempre poderá tentar sem bloquear. select()é conduzido por dados no buffer de recebimento do socket ou um FIN pendente ou espaço no buffer de envio do socket.
Marquês de Lorne
@EJP Sobre o quê getsockopt(SO_ERROR)? Na verdade, o even getpeernameinformará se o soquete ainda está conectado.
David Schwartz
0

Apenas as gravações exigem que os pacotes sejam trocados, o que permite que a perda de conexão seja determinada. Uma solução alternativa comum é usar a opção KEEP ALIVE.

Raio
fonte
Acho que um ponto de extremidade pode iniciar um desligamento normal da conexão, enviando um pacote com FIN definido, sem gravar nenhuma carga.
Alexander,
@Alexander Claro que sim, mas isso não é relevante para esta resposta, que é sobre como detectar menos conexão.
Marquês de Lorne
-3

Quando se trata de lidar com soquetes Java entreabertos, pode-se dar uma olhada em isInputShutdown () e isOutputShutdown () .

JimmyB
fonte
Não. Isso só diz o que você chamou, não o que o colega chamou.
Marquês de Lorne,
Importa-se de compartilhar sua fonte para essa declaração?
JimmyB de
3
Importa-se de compartilhar sua fonte para o oposto? É a sua declaração. Se você tem uma prova, vamos tê-la. Eu afirmo que você está incorreto. Faça o experimento e prove que estou errado.
Marquês de Lorne
Três anos depois e nenhum experimento. QED
Marquês de Lorne