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

92

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

Lorsqu'un homologue ferme en douceur une connexion TCP, les piles TCP des deux côtés de la connexion sont au courant du fait. 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) finit dans l'état CLOSE_WAIT. Pourquoi n'y a-t-il pas de méthode dans Socketou SocketChannelqui puisse interroger la pile TCP pour voir si la connexion TCP sous-jacente a été interrompue? Est-ce que la pile TCP ne fournit pas de telles 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à publié des réponses à cette question, je pense voir d'où vient le problème. Le côté qui ne ferme pas explicitement la connexion se retrouve dans l'état TCP, CLOSE_WAITce qui signifie que la connexion est en train de s'arrêter et attend que le côté émette sa propre CLOSEopération. Je suppose que c'est assez juste que les isConnectedretours trueet les isClosedretours false, mais pourquoi n'y a-t-il pas quelque chose comme ça isClosing?

Vous trouverez ci-dessous 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
...
Alexandre
la source
J'ai pensé que je mentionnerais: le protocole SCTP n'a pas ce "problème". SCTP n'a pas un état semi-fermé comme TCP, en d'autres termes un côté ne peut pas continuer à envoyer des données alors que l'autre a fermé son socket d'envoi. Cela devrait faciliter les choses.
Tarnay Kálmán
2
Nous avons deux boîtes aux lettres (prises) ........................................... ...... Les boîtes aux lettres s'envoient du courrier en utilisant le RoyalMail (IP) oubliez TCP ............................. .................... Tout va bien et dandy, les boîtes aux lettres peuvent envoyer / recevoir du courrier entre elles (beaucoup de retard récemment) en envoyant tout en ne recevant aucun problème. ............. Si une boîte aux lettres devait être heurtée par un camion et échouer ... comment l'autre boîte aux lettres le saurait-elle? Il devrait être notifié par Royal Mail, qui à son tour ne saurait pas jusqu'à ce qu'il essaie d'envoyer / recevoir du courrier à partir de cette boîte aux lettres défaillante .. ...... euh .....
divinci
Si vous n'allez pas lire à partir du socket ou écrire sur le socket, pourquoi vous souciez-vous? Et si vous allez lire à partir du socket ou écrire sur le socket, pourquoi faire une vérification supplémentaire? Quel est le cas d'utilisation?
David Schwartz
2
Socket.closen'est pas une fin gracieuse.
user253751
@immibis C'est très certainement une fermeture gracieuse, à moins qu'il y ait des données non lues dans le tampon de réception de socket ou que vous ayez joué avec SO_LINGER.
Marquis of Lorne

Réponses:

29

J'utilise souvent Sockets, principalement avec des sélecteurs, et bien que ce ne soit pas un expert en OSI réseau, d'après ce que je comprends, appeler shutdownOutput()un socket envoie en fait quelque chose sur le réseau (FIN) qui réveille mon sélecteur de l'autre côté (même comportement en C Langue). Ici, vous avez la détection : détecter réellement une opération de lecture qui échouera lorsque vous l'essayez.

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 Socket.close()méthode Java effectue une déconnexion «gracieuse» (contrairement à ce que je pensais au départ) en ce que les données laissées dans le flux de sortie seront envoyées suivies d'un FIN pour signaler sa fermeture. Le FIN sera ACK par l'autre côté, comme n'importe quel paquet régulier 1 .

Si vous devez attendre que l'autre côté ferme son socket, vous devez attendre son FIN. Et pour cela, vous devez détecter Socket.getInputStream().read() < 0, ce qui signifie que vous ne devriez pas fermer votre prise, car il fermer sonInputStream .

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. Shutdown socket output (envoie FIN à l'autre extrémité, c'est la dernière chose qui sera jamais envoyée par cette socket). L'entrée est toujours ouverte afin que vous puissiez read()et détecter la télécommandeclose()
  2. Lisez le socket InputStreamjusqu'à ce que nous recevions la réponse-FIN de l'autre extrémité (comme il détectera le FIN, il passera par le même processus de déconnexion gracieux). Ceci est important sur certains OS car ils ne ferment pas réellement le socket tant que l'un de ses tampons contient encore des données. Ils sont appelés socket "fantôme" et utilisent des numéros de descripteurs dans le système d'exploitation (ce qui 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 InputStreamou OutputStream)

Comme indiqué dans l'extrait de code Java suivant:

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 méthode de fermeture, ou la partie émettrice peut toujours envoyer suffisamment de données pour garder la whileboucle occupée (par exemple si la partie émettrice envoie uniquement des données et ne lit jamais 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 au niveau de la couche TCP (la whileboucle), elles sont rejetées.

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

Matthieu
la source
Java effectue une fermeture gracieuse. Ce n'est pas «brutal».
Marquis of Lorne
2
@EJP, la "déconnexion gracieuse" est un échange spécifique se déroulant au niveau TCP, où le Client doit signaler sa volonté de se déconnecter au Serveur qui, à son tour, envoie les données restantes avant de fermer son côté. La partie «envoyer les données restantes» doit être gérée par le programme (bien que les gens n'envoient rien la plupart du temps). L'appel socket.close()est "brutal" dans la mesure où il ne respecte pas cette signalisation Client / Serveur. Le serveur serait averti d'une déconnexion client uniquement lorsque son propre tampon de sortie de socket est plein (car aucune donnée n'est acquittée par l'autre côté, qui a été fermé).
Matthieu
Consultez MSDN pour plus d'informations.
Matthieu
Java ne force pas les applications à appeler Socket.close () au lieu de «envoyer d'abord les données restantes», et cela ne les empêche pas d'utiliser un arrêt en premier. Socket.close () fait exactement ce que close (sd) fait. Java n'est pas différent à cet égard de l'API Berkeley Sockets elle-même. En effet, à bien des égards.
Marquis de Lorne
2
@LeonidUsov C'est tout simplement incorrect. Java read()renvoie -1 à la fin du flux, et continue de le faire aussi souvent que vous l'appelez. AC read()ou recv()renvoie zéro à la fin du flux, et continue de le faire aussi souvent que vous l'appelez.
Marquis of Lorne
18

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

De Wikipedia :

TCP fournit une 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 négociation effectuée, TCP ne fait aucune distinction entre deux points d'extrémité (client et serveur). Les termes «client» et «serveur» sont principalement utilisés pour des raisons de commodité. Ainsi, le "serveur" pourrait envoyer des données et le "client" pourrait s'envoyer d'autres données simultanément.

Le terme «Fermer» est également trompeur. Il n'y a que la déclaration FIN, ce qui signifie "Je ne vais plus vous envoyer de trucs." Mais cela ne veut pas dire 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 des itinéraires différents, 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 qui se trouve dans la mémoire tampon. Ainsi, à votre convenance, vous pouvez vérifier ce qu'il y a dans la mémoire tampon. Dans l'ensemble, l'implémentation actuelle des sockets 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.

Eugene Yokota
la source
1
Je ne pense pas, vous pouvez tester les états en code C sur Windows et Linux !!! Java pour une raison quelconque peut ne pas exposer certaines choses, comme ce serait bien d'exposer les fonctions getsockopt qui sont sur Windows et Linux. En fait, les réponses ci-dessous ont du code Linux C pour le côté Linux.
Dean Hiller
Je ne pense pas qu'avoir la méthode 'isPeerClosed ()' vous engage d'une manière ou d'une autre à l'appeler avant chaque tentative de lecture. Vous pouvez simplement l'appeler uniquement lorsque vous en avez explicitement besoin. Je suis d'accord que l'implémentation actuelle de la socket n'est pas si mauvaise, même si elle vous oblige à écrire dans le flux de sortie si vous voulez savoir si la partie distante de la socket est fermée ou non. Parce que si ce n'est pas le cas, vous devez également gérer vos données écrites de l'autre côté, et c'est tout simplement aussi grand plaisir que de vous asseoir sur l'ongle orienté verticalement;)
Jan Hruby
1
Il ne moyenne 'il n'y a pas plus paquet en vol. Le FIN est reçu après toutes les données en vol. Cependant, ce que cela ne signifie pas , c'est que le pair a fermé le socket pour l'entrée. Vous devez ** envoyer quelque chose * et obtenir une «réinitialisation de la connexion» pour le détecter. Le FIN aurait pu simplement signifier un arrêt pour la sortie.
Marquis of Lorne le
11

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

De toute façon, la pile TCP émettrice n'enverra pas le bit FIN avant le dernier paquet, il peut donc y avoir beaucoup de données mises en mémoire tampon à partir du moment où l'application d'envoi a fermé logiquement son socket avant même que ces données ne soient envoyées. De même, les données 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 via une connexion plus lente) pourraient être importantes pour le récepteur et vous ne voudriez pas que l'application réceptrice la rejette simplement parce que le bit FIN a été reçu par la pile.

Mike Dimmick
la source
Dans mon exemple de test (peut-être que je devrais en fournir un ici ...) il n'y a pas de données envoyées / reçues sur la connexion exprès. Donc, je suis à peu près sûr que la pile reçoit le FIN (gracieux) ou RST (dans certains scénarios non gracieux). Ceci est également confirmé par netstat.
Alexander
1
Bien sûr - si rien n'est mis en mémoire tampon, le FIN sera envoyé immédiatement, sur un paquet autrement vide (pas de charge utile). Après FIN, cependant, plus aucun paquet de données n'est envoyé par cette extrémité de la connexion (il ACK toujours tout ce qui lui est envoyé).
Mike Dimmick
1
Ce qui se passe, c'est que les côtés de la connexion se retrouvent dans <code> CLOSE_WAIT </code> et <code> FIN_WAIT_2 </code> et c'est dans cet état que <code> isConcected () </code> et <code> isClosed () </code> ne voit toujours pas que la connexion a été interrompue.
Alexander
Merci pour vos suggestions! Je pense que je comprends mieux la question maintenant. J'ai précisé la question (voir le troisième paragraphe): pourquoi n'y a-t-il pas "Socket.isClosing" pour tester les connexions semi-fermées?
Alexander
8

Étant donné qu'aucune des réponses à ce jour ne répond entièrement à 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 à l' CLOSE_WAITétat. En principe, il est possible de savoir à partir de la pile TCP si un socket est en CLOSE_WAITétat sans appeler read/recv(par exemple, getsockopt()sous Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), mais ce n'est pas portable.

Java Socket classe semble être conçue pour fournir une abstraction comparable à une socket BSD TCP, probablement parce que c'est le niveau d'abstraction auquel les gens sont habitués lors de la programmation d'applications TCP / IP. Les sockets BSD sont une généralisation prenant en charge les sockets autres que les sockets 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 habitués à programmer des applications TCP au niveau d'abstraction offert par les sockets BSD ne s'attendent pas à ce que Java fournisse des méthodes supplémentaires.

Alexandre
la source
3
Java ne peut pas non plus fournir de méthodes supplémentaires, de manière portable. Peut-être qu'ils pourraient créer une méthode isCloseWait () qui renverrait false si la plate-forme ne la supportait pas, mais combien de programmeurs seraient mordus par ce piège s'ils ne testaient que sur des plates-formes prises en charge?
éphémère
1
on dirait que cela pourrait être portable pour moi ... windows a ce msdn.microsoft.com/en-us/library/windows/desktop/… et linux ce pubs.opengroup.org/onlinepubs/009695399/functions/…
Dean Hiller
1
Ce n'est pas que les programmeurs y sont habitués; c'est que l'interface socket est ce qui est utile aux programmeurs. Gardez à l'esprit que l'abstraction de socket est utilisée pour plus que le protocole TCP.
Warren Dew
Il n'y a pas de méthode en Java, isCloseWait()car elle n'est pas prise en charge sur toutes les plates-formes.
Marquis of Lorne le
Le protocole ident (RFC 1413) permet au serveur soit de maintenir la connexion ouverte après l'envoi d'une réponse, soit de la fermer sans envoyer plus de données. Un client Java ident peut choisir de garder la connexion ouverte pour éviter la négociation à trois lors de la recherche suivante, mais comment sait-il que la connexion est toujours ouverte? Doit-il simplement essayer, répondre à toute erreur en rouvrant la connexion? Ou est-ce une erreur de conception de protocole?
david
7

Détecter si le côté distant d'une connexion de socket (TCP) est fermé peut être effectué avec la méthode java.net.Socket.sendUrgentData (int) et intercepter l'exception IOException qu'elle émet 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 ignorées en silence, 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 l'émission d'une exception IOException. .

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

Oncle Per
la source
4

la pile Java IO envoie définitivement FIN lorsqu'elle est détruite lors d'un démontage brutal. Cela n'a aucun sens que vous ne puissiez pas détecter cela, car la plupart des clients n'envoient le FIN que s'ils arrêtent la connexion.

... une autre raison pour laquelle je commence vraiment à détester les classes NIO Java. On dirait que tout est un petit demi-cul.


la source
1
aussi, il semble que je ne reçois et fin de flux qu'en lecture (-1 retour) quand un FIN est présent. C'est donc le seul moyen que je peux voir pour détecter un arrêt du côté lecture.
3
Vous pouvez le détecter. Vous obtenez EOS lors de la lecture. Et Java n'envoie pas de FIN. TCP fait cela. Java n'implémente pas TCP / IP, il utilise simplement l'implémentation de la plate-forme.
Marquis de Lorne
4

C'est un sujet intéressant. Je viens de fouiller dans le code java pour vérifier. D'après ma conclusion, 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, de sorte qu'un socket fermé à distance est toujours à moitié ouvert. Conformément à la RFC, RST ne ferme pas la connexion, vous devez envoyer une commande ABORT explicite; donc Java permet d'envoyer des données via un socket à moitié fermé

(Il existe deux méthodes pour lire l'état de fermeture des deux points de terminaison.)

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, mise en œuvre du semi-duplex) aurait été un problème, je suppose.

Lorenzo Boccaccia
la source
1
Je suppose que vous parlez de la RFC 793 ( faqs.org/rfcs/rfc793.html ) Section 3.5 Clôture d'une connexion. Je ne suis pas sûr que cela explique le problème, car les deux côtés terminent l'arrêt en douceur de la connexion et se retrouvent dans des états où ils ne devraient pas envoyer / recevoir de données.
Alexander
Dépend. combien de FIN vous voyez sur la prise? Cela pourrait également être un problème spécifique à la plate-forme: peut-être que Windows répond à chaque FIN avec un FIN et la connexion est fermée aux deux extrémités, mais d'autres systèmes d'exploitation peuvent ne pas se comporter de cette façon, et c'est là que le problème 2 se pose
Lorenzo Boccaccia
1
Non, ce n'est malheureusement pas le cas. isOutputShutdown et isInputShutdown est la première chose que tout le monde essaie face à cette "découverte", mais les deux méthodes renvoient false. Je viens de le tester sur Windows XP et Linux 2.6. Les valeurs de retour des 4 méthodes restent les mêmes même après une tentative de lecture
Alexander
1
Pour mémoire, ce n'est pas du semi-duplex. Le semi-duplex signifie qu'un seul côté peut envoyer à la fois; les deux côtés peuvent encore envoyer.
rbp
1
isInputShutdown et isOutputShutdown testent l' extrémité locale de la connexion - ce sont des tests pour savoir si vous avez appelé shutdownInput ou shutdownOutput sur ce Socket. Ils ne vous disent rien sur l'extrémité distante de la connexion.
Mike Dimmick
3

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

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 */  
}  
Joshua
la source
Vous pouvez faire la même chose avec getsocketop(... SOL_SOCKET, SO_ERROR, ...) .
David Schwartz
1
L'ensemble de descripteurs de fichier d'erreur n'indiquera pas une connexion fermée Veuillez lire le manuel de sélection: 'exceptfds - Cet ensemble est surveillé pour des "conditions exceptionnelles". En pratique, une seule de ces conditions excessives est courante: la disponibilité de données hors bande (OOB) pour la lecture à partir d'un socket TCP. FIN ne sont pas des données OOB.
Jan Wrobel
1
Vous pouvez utiliser la classe 'Selector' pour accéder à l'appel système 'select ()'. Il utilise cependant NIO.
Matthieu
1
Il n'y a rien d'exceptionnel à propos d'une connexion qui a été coupée par l'autre côté.
David Schwartz
1
De nombreuses plates - formes, y compris Java, font donner accès à l'appel système select ().
Marquis of Lorne le
2

Voici une solution de contournement boiteuse. Utilisez SSL;) et SSL établit 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 de poignée de main proprement dit).

Dean Hiller
la source
2
Comment est-il "notifié" de la fermeture du socket lors de l'utilisation de SSL en Java?
Dave B
2

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

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

WMR
la source
2
Les sockets REALbasic (sur Mac OS X et Linux) sont basés sur des sockets BSD, mais RB parvient à vous donner une belle erreur 102 lorsque la connexion est interrompue par l'autre extrémité. Donc je suis d'accord avec l'affiche originale, cela devrait être possible et c'est nul que Java (et Cocoa) ne le fournissent pas.
Joe Strout
1
@JoeStrout RB ne peut le faire que lorsque vous effectuez des E / S. Il n'y a pas d'API qui vous donnera l'état de la connexion sans effectuer d'E / S. Période. Ce n'est pas une carence Java. Cela est en fait dû à l'absence de «tonalité» dans TCP, qui est une caractéristique de conception délibérée.
Marquis de Lorne
select() vous indique si des données ou EOS sont disponibles pour être lus sans blocage. «Les signaux que vous pouvez essayer sans bloquer» n'a aucun sens. Si vous êtes en mode non bloquant, vous pouvez toujours essayer sans bloquer. select()est piloté par des données dans le tampon de réception du socket ou un FIN en attente ou un espace dans le tampon d'envoi du socket.
Marquis of Lorne
@EJP Et getsockopt(SO_ERROR)? En fait, même getpeernamevous dira si la prise est toujours connectée.
David Schwartz
0

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.

Rayon
la source
Je pense qu'un point de terminaison est autorisé à lancer un arrêt de connexion en douceur en envoyant un paquet avec FIN défini, sans écrire de charge utile.
Alexander
@Alexander Bien sûr que oui, mais ce n'est pas pertinent pour cette réponse, qui consiste à détecter moins de connexion.
Marquis of Lorne
-3

Quand il s'agit de traiter les sockets Java semi-ouverts, on peut vouloir jeter un œil à isInputShutdown () et isOutputShutdown () .

JimmyB
la source
Non. Cela ne vous dit que ce que vous avez appelé, pas ce que le pair a appelé.
Marquis of Lorne
Voulez-vous partager votre source pour cette déclaration?
JimmyB
3
Voulez-vous partager votre source pour le contraire? C'est votre déclaration. Si vous avez une preuve, ayez-la. J'affirme que vous avez tort. Faites l'expérience et prouvez-moi que j'ai tort.
Marquis de Lorne
Trois ans plus tard et aucune expérience. QED
Marquis of Lorne