web-dev-qa-db-fra.com

Pourquoi est-il impossible, sans tenter d'E / S, de détecter que le socket TCP a été gracieusement fermé par un pair?

Pour faire suite à une question récente , je me demande pourquoi c'est impossible en Java, sans essayer de lire/écrire sur une prise TCP, pour détecter que la prise a été gracieusement fermé par le pair? Cela semble être le cas, que l'on utilise le pré-NIO Socket ou le NIO SocketChannel.

Lorsqu'un homologue ferme gracieusement une connexion TCP, les piles TCP des deux côtés de la connexion sont au courant. Le côté serveur (celui qui initie l'arrêt) se retrouve dans l'état FIN_WAIT2, tandis que le côté client (celui qui ne répond pas explicitement à l'arrêt) se retrouve dans l'état CLOSE_WAIT. Pourquoi n'y a-t-il pas une méthode dans Socket ou SocketChannel qui peut interroger la pile TCP pour voir si le sous-jacent TCP = la connexion a été interrompue? Est-ce que la pile TCP ne fournit pas ces informations d'état? Ou est-ce une décision de conception pour éviter un appel coûteux dans le noyau?

Avec l'aide des utilisateurs qui ont déjà posté des réponses à cette question, je pense que je vois d'où vient le problème. Le côté qui ne ferme pas explicitement la connexion se retrouve dans TCP state CLOSE_WAIT ce qui signifie que la connexion est en cours de fermeture et attend que le côté émette sa propre opération CLOSE. Je suppose qu'il est assez juste que isConnected renvoie true et isClosed renvoie false, mais pourquoi n'y a-t-il pas quelque chose comme isClosing?

Voici les classes de test qui utilisent des sockets pré-NIO. Mais des résultats identiques sont obtenus en utilisant 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);
  }
}

Lorsque le client de test se connecte au serveur de test, la sortie reste inchangée même après que le serveur a initié l'arrêt de la connexion:

connected: true, closed: false
connected: true, closed: false
...
89
Alexander

J'ai souvent utilisé des sockets, principalement avec des sélecteurs, et bien que je ne sois pas un expert en réseau OSI, à ma connaissance, appeler shutdownOutput() sur un socket envoie en fait quelque chose sur le réseau (FIN) qui réveille mon sélecteur sur le autre côté (même comportement en langage C). Ici, vous avez détection : détecter réellement une opération de lecture qui échouera lorsque vous l'essayerez.

Dans le code que vous donnez, la fermeture du socket arrêtera les flux d'entrée et de sortie, sans possibilité de lire les données qui pourraient être disponibles, donc de les perdre. La méthode Java Socket.close() effectue une déconnexion "gracieuse" (opposée à ce que je pensais initialement) en ce que les données laissées dans le flux de sortie seront envoyées suivi par un FIN pour signaler sa fermeture. Le FIN sera accusé de réception de l'autre côté, comme tout paquet normal1.

Si vous devez attendre que l'autre côté ferme sa prise, vous devez attendre sa FIN. Et pour y parvenir, vous devez détecter Socket.getInputStream().read() < 0, ce qui signifie que vous devez pas fermer votre socket, comme il le ferait fermer son InputStream.

D'après ce que j'ai fait en C, et maintenant en Java, la réalisation d'une telle fermeture synchronisée devrait se faire comme ceci:

  1. Sortie du socket d'arrêt (envoie FIN à l'autre extrémité, c'est la dernière chose qui sera envoyée par ce socket). L'entrée est toujours ouverte pour que vous puissiez read() et détecter la télécommande close()
  2. Lisez le socket InputStream jusqu'à ce que nous recevions le answer-FIN de l'autre extrémité (car il détectera le FIN, il passera par le même processus de déconnexion gracieux). Ceci est important sur certains systèmes d'exploitation car ils ne ferment pas réellement le socket tant que l'un de ses tampons contient toujours des données. Ils sont appelés socket "fantôme" et utilisent des numéros de descripteur dans le système d'exploitation (cela pourrait ne plus être un problème avec le système d'exploitation moderne)
  3. Fermez le socket (en appelant Socket.close() ou en fermant son InputStream ou OutputStream)

Comme illustré dans l'extrait suivant Java snippet:

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
}

Bien sûr, les deux côtés doivent utiliser la même façon de fermer, ou la partie d'envoi peut toujours envoyer suffisamment de données pour garder la boucle while occupée (par exemple si la partie d'envoi est envoyer uniquement des données et ne jamais lire pour détecter la fin de la connexion. Ce qui est maladroit, mais vous pourriez ne pas avoir le contrôle sur cela).

Comme @WarrenDew l'a souligné dans son commentaire, la suppression des données dans le programme (couche application) induit une déconnexion non gracieuse au niveau de la couche application: bien que toutes les données aient été reçues à TCP couche (la couche while loop), elles sont supprimées.

1: De " Fundamental Networking en Java ": voir fig. 3.3 p.45, et l'ensemble §3.7, pp 43-48

29
Matthieu

Je pense que c'est plus une question de programmation de socket. Java suit simplement la tradition de programmation des sockets.

De Wikipedia :

TCP fournit la livraison fiable et ordonnée d'un flux d'octets d'un programme sur un ordinateur à un autre programme sur un autre ordinateur.

Une fois la prise de contact terminée, TCP ne fait aucune distinction entre deux points d'extrémité (client et serveur). Le terme "client" et "serveur" est principalement utilisé pour des raisons de commodité. Ainsi, le "serveur "pourrait envoyer des données et" client "pourrait envoyer simultanément d'autres données.

Le terme "Fermer" est également trompeur. Il n'y a qu'une déclaration FIN, ce qui signifie "Je ne vais plus vous envoyer de trucs". Mais cela ne signifie pas qu'il n'y a pas de paquets en vol, ou que l'autre n'a plus rien à dire. Si vous implémentez le courrier postal comme couche de liaison de données, ou si votre paquet a parcouru différents itinéraires, il est possible que le destinataire reçoive les paquets dans le mauvais ordre. TCP sait comment résoudre ce problème pour vous.

De plus, en tant que programme, vous n'aurez peut-être pas le temps de vérifier ce qu'il y a dans le tampon. Ainsi, à votre convenance, vous pouvez vérifier le contenu du tampon. Dans l'ensemble, l'implémentation de socket actuelle n'est pas si mauvaise. S'il y avait réellement isPeerClosed (), c'est un appel supplémentaire que vous devez faire chaque fois que vous voulez appeler read.

18
Eugene Yokota

L'API sockets sous-jacente n'a pas une telle notification.

L'envoi TCP n'enverra de toute façon pas le bit FIN jusqu'au dernier paquet, donc il pourrait y avoir beaucoup de données en mémoire tampon à partir du moment où l'application d'envoi a fermé logiquement son socket avant même que ces données soient envoyées De même, les données qui sont mises en mémoire tampon parce que le réseau est plus rapide que l'application réceptrice (je ne sais pas, peut-être que vous les relayez sur une connexion plus lente) pourraient être importantes pour le récepteur et vous ne voudriez pas que l'application réceptrice se défasse juste parce que le bit FIN a été reçu par la pile.

11
Mike Dimmick

Étant donné qu'aucune des réponses à ce jour ne répond pleinement à la question, je résume ma compréhension actuelle du problème.

Lorsqu'une connexion TCP est établie et qu'un pair appelle close() ou shutdownOutput() sur son socket, le socket de l'autre côté de la connexion passe à CLOSE_WAIT. En principe, il est possible de découvrir à partir de la pile TCP si un socket est dans l'état CLOSE_WAIT Sans appeler read/recv (Par exemple , getsockopt() sur Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), mais ce n'est pas portable.

La classe Socket de Java semble être conçue pour fournir une abstraction comparable à une socket BSD TCP socket, probablement parce que c'est le niveau d'abstraction auquel les gens sont habitués lors de la programmation de TCP/Applications IP. Les sockets BSD sont une généralisation prenant en charge des sockets autres que simplement INET (par exemple, TCP), donc ils ne fournissent pas un moyen portable de découvrir l'état TCP d'un socket).

Il n'y a pas de méthode comme isCloseWait() parce que les gens ont l'habitude de programmer TCP au niveau d'abstraction offertes par les sockets BSD ne s'attendent pas à Java pour fournir des méthodes supplémentaires.

8
Alexander

La détection de la fermeture du côté distant d'une connexion de socket (TCP) peut être effectuée avec la méthode Java.net.Socket.sendUrgentData (int) et la capture de l'exception IOException qu'elle déclenche si le côté distant est en panne. Cela a été testé entre Java-Java et Java-C.

Cela évite le problème de la conception du protocole de communication pour utiliser une sorte de mécanisme de ping. En désactivant OOBInline sur un socket (setOOBInline (false), toutes les données OOB reçues sont supprimées en mode silencieux, mais les données OOB peuvent toujours être envoyées. Si le côté distant est fermé, une réinitialisation de la connexion est tentée, échoue et provoque la levée d'une exception IOException. .

Si vous utilisez réellement des données OOB dans votre protocole, votre kilométrage peut varier.

7
Uncle Per

la pile Java IO envoie définitivement FIN quand il est détruit lors d'un démontage brusque. Cela n'a aucun sens que vous ne puissiez pas le détecter, b/c la plupart des clients n'envoient le FIN que s'ils interrompent la connexion.

... une autre raison pour laquelle je commence vraiment à détester les NIO Java classes. Il semble que tout soit un peu à moitié).

4
Sean

C'est un sujet intéressant. J'ai creusé le code Java juste pour vérifier. D'après mes constatations, il y a deux problèmes distincts: le premier est le TCP RFC lui-même, qui permet à un socket fermé à distance de transmettre des données en semi-duplex, donc un socket fermé à distance est toujours à moitié ouvert. Selon le RFC, RST ne ferme pas la connexion, vous devez envoyer une commande explicite ABORT; donc Java permet d'envoyer des données via un socket à moitié fermé

(Il existe deux méthodes pour lire l'état de fermeture aux deux extrémités.)

L'autre problème est que l'implémentation dit que ce comportement est facultatif. Comme Java s'efforce d'être portable, ils ont implémenté la meilleure fonctionnalité commune. Le maintien d'une carte de (OS, implémentation de half duplex) aurait été un problème, je suppose.

4
Lorenzo Boccaccia

Il s'agit d'une faille de Java (et de toutes les autres que j'ai examinées) OO classes de socket - pas d'accès à l'appel système sélectionné.

Bonne réponse en 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 */  
}  
3
Joshua

Voici une solution de contournement boiteuse. Utilisez SSL;) et SSL fait une poignée de main étroite lors du démontage afin que vous soyez averti de la fermeture du socket (la plupart des implémentations semblent faire un démontage proprement dit).

2
Dean Hiller

La raison de ce comportement (qui n'est pas Java) est le fait que vous n'obtenez aucune information d'état de la pile TCP. Après tout, un socket est juste un autre descripteur de fichier et vous ne pouvez pas savoir s'il y a des données réelles à lire sans vraiment essayer (select(2) n'y aidera pas, cela signale seulement que vous pouvez essayer sans bloquer) .

Pour plus d'informations, consultez la FAQ sur les sockets Unix .

2
WMR

Seules les écritures nécessitent l'échange de paquets, ce qui permet de déterminer la perte de connexion. Une solution courante consiste à utiliser l'option KEEP ALIVE.

0
Ray