Etape 11 – Communication réseau

1 Introduction

Le but de cette étape est de terminer le projet en écrivant les programmes principaux correspondant au client et au serveur, communiquant entre eux via le réseau.

1.1 Client et serveur

Comme cela a été expliqué dans l'introduction au projet, la mise en œuvre du jeu XBlast est séparée en deux programmes distincts :

  • le serveur, qui fait évoluer l'état du jeu en fonction des règles et des événements reçus des clients, et le transmet périodiquement aux clients,
  • le client, qui affiche à l'écran l'état reçu du serveur et détecte la pression des touches correspondant aux différentes actions, qu'il transmet au serveur.

Dans une partie comportant n joueurs, un total de 1 + n programmes s'exécutent : un serveur, et n clients. Chaque joueur humain dispose d'un ordinateur sur lequel il exécute une copie du client, tandis que le serveur s'exécute soit sur un des ordinateurs des joueurs, soit sur un ordinateur séparé.

1.2 Protocole client/serveur

La communication entre le serveur et les clients est découpée en deux phases distinctes :

  1. dans un premier temps, les clients envoient périodiquement au serveur un message l'informant de leur intention de se joindre à la partie ; le serveur accepte chaque nouveau client jusqu'à ce qu'il y en ait le nombre requis — généralement 4,
  2. dans un deuxième temps, la partie commence, et à partir de cet instant, le serveur envoie périodiquement le nouvel état à chaque client, et chaque client lui transmet les actions de son joueur.

Ces deux phases de communication et les différents messages échangés par un serveur s et deux clients c1 et c2 au début d'une partie sont illustrés dans la figure 1. Le temps s'y écoule de gauche à droite, et les messages échangés sont représentés par des flèches allant de l'expéditeur du message à son destinataire. Les types de messages sont identifiés par les couleurs des flèches et les étiquettes attachées, qui ont la signification suivante :

  • J désigne un message envoyé par un client au serveur pour lui communiquer son intention de se joindre à la partie,
  • p,si désigne un message envoyé par le serveur à un client pour lui communiquer son identité p et l'état de la partie au coup d'horloge i,
  • N (resp. S) désigne un message envoyé par un client au serveur l'informant que le joueur désire se diriger vers le nord (resp. sud),
  • B désigne un message envoyé par un client au serveur l'informant que le joueur désire déposer une bombe.

Sorry, your browser does not support SVG.

Figure 1 : Messages échangés entre un serveur et deux clients

Tous les messages transmis par les clients au serveur ne contiennent qu'un octet, qui spécifie l'action que le client désire effectuer. Cet octet n'est rien d'autre que l'index de l'action en question dans l'énumération PlayerAction définie lors de l'étape précédente (0 pour JOIN_GAME, noté J ci-dessus, 1 pour MOVE_N, etc.).

Les messages transmis par le serveur sont constitués de :

  • un octet donnant l'identité du joueur auquel le message est destiné (0 pour le premier joueur, 1 pour le second, etc.),
  • une séquence d'octets de taille variable représentant l'état du jeu sérialisé de la manière décrite à l'étape 8.

1.3 Communication via Internet

La communication entre le serveur et les clients se fait via Internet. Les différents appareils connectés à Internet — généralement appelée hôtes (host) — communiquent entre eux au moyen d'un grand nombre de protocoles distincts, dont la description complète sort du cadre de ce cours. Néanmoins, une rapide introduction est nécessaire et les personnes intéressées par plus de détails peuvent suivre les liens vers Wikipedia inclus ci-dessous, et/ou lire le livre Java Network Programming, très complet et disponible en ligne.

Les protocoles d'Internet sont organisés en différentes couches, correspondant à différents niveaux d'abstraction. Les protocoles d'une couche donnée sont toujours basés sur ceux de la couche juste en-dessous. Trois de ces couches et plusieurs protocoles importants de chacune d'entre elles sont illustrées sur la figure 2.

Sorry, your browser does not support SVG.

Figure 2 : Organisation en couches de quelques protocoles d'Internet

Comme nous le verrons plus loin, le protocole du jeu, désigné par XBlast dans cette figure, est basé sur le protocole UDP de la couche inférieure. Beaucoup de protocoles connus — p.ex. HTTP qui est le protocole du Web, ou SMTP et IMAP qui sont liés au courrier électronique — sont quant à eux basés sur un autre protocole de la couche inférieure, TCP.

1.3.1 IP

A la couche la plus basse qui nous intéresse ici se trouve le protocole de base d'Internet, nommé Internet Protocol et généralement abrégé IP. Ce protocole permet à deux hôtes connectés à Internet de s'échanger des messages, appelés paquets (packets), d'une taille maximale fixe. Aucune garantie n'est offerte quant à cette transmission, et il est donc possible que les paquets soient corrompus, perdus ou arrivent dans le désordre.

Bien entendu, l'échange de tels messages suppose l'existence d'un mécanisme permettant de désigner individuellement chacun des hôtes connecté au réseau, au même titre que l'expédition de lettres et paquets physiques suppose l'existance d'un mécanisme permettant de désigner une boîte aux lettres, l'adresse postale. Dans le cas d'Internet, ce rôle est joué par ce que l'on nomme l'adresse IP (IP address).

A l'origine, une adresse IP était un entier de 32 bits, généralement représenté par les quatre octets le composant, en base 10, séparés par un point. Par exemple, 128.178.131.224 est une telle adresse IP, en l'occurrence celle du serveur Web du cours. Une adresse IP sur 32 bits est appelée adresse IPv4. Certaines d'entre elles ont une signification particulière et prédeterminée. Il en va par exemple ainsi de l'adresse 127.0.0.1, qui désigne toujours l'hôte local (local host), c-à-d l'hôte sur laquelle on l'utilise.

Ces dernières années, au vu de la rapide augmentation du nombre d'hôtes connectés à Internet, il a été nécessaire d'augmenter la taille des adresses, 32 bits ne suffisant plus. Un nouveau format d'adresses, un entier de 128 bits, a donc été introduit et est appelé adresse IPv6. Ces adresses sont généralement représentées par huit groupes représentant chacun 16 bits, en base 16 et séparés par un deux-points. Par exemple, 2002:80B2:DE47:0:0:0:0:0 est une telle adresse.

Etant donné qu'il n'est pas facile pour les êtres humains de se souvenir d'adresses IP (v4 et encore moins v6) numériques, Internet est doté d'un service de noms nommé DNS (Domain Name System) et permettant d'utiliser des noms alphanumériques au lieu d'adresses numériques. Par exemple, le nom cs108.epfl.ch correspond à l'adresse 128.178.131.224 mentionnée plus haut, et le nom localhost correspond à l'adresse de l'hôte local, 127.0.0.1.

Le protocole IP n'est presque jamais utilisé directement par des applications, mais sert de base à d'autres protocoles de plus haut niveau. Les deux principaux protocoles construits directement sur IP sont UDP et TCP. La différence principale entre UDP et TCP est que UDP ne garantit pas que les données envoyées par l'expéditeur soient effectivement reçues par le destinataire, tandis que TCP offre cette garantie.

1.3.2 UDP

UDP (User Datagram Protocol) n'ajoute que très peu de choses au protocole IP, sur lequel il est basé. En particulier, tout comme IP, il ne donne aucune garantie quant à la transmission des paquets, qui peuvent être corrompus, perdus ou réordonnés lors du transfert.

Malgré cette absence de garanties, UDP sert de base à la plupart des protocoles pour lesquels la perte de données n'est pas fatale mais les performances sont importantes. C'est le cas par exemple des protocoles utilisés pour la téléphonie (p.ex. celui de Skype), la vidéoconférence et la plupart des jeux. Dans toutes ces applications, la perte de quelques données n'est pas très importante — l'humain auxquelles elles sont destinées étant généralement capable de compenser leur absence — mais il est par contre important que le délai entre l'envoi et la réception des données soit aussi petit que possible. Dans de telles situations, UDP est un meilleur choix que TCP, car lorsque le réseau est congestionné, TCP peut retarder considérablement la transmission de certaines données afin de garantir qu'elles soient toutes transmises.

UDP, tout comme TCP, augmente la notion d'adresse fournie par IP en lui ajoutant un numéro de port, un entier 16 bits non signé — compris donc entre 0 et 65'535. Ce numéro de port permet à plusieurs applications de se partager une même adresse IP pour peu qu'elles utilisent un numéro de port différent, un peu comme différentes personnes peuvent se partager une même adresse de bâtiment pour peu qu'une information additionnelle (p.ex. un numéro d'appartement) permette de les distinguer.

Les ports dont la valeur est inférieure à 1'024 sont réservés à des protocoles précis, et ne doivent pas être utilisées par d'autres applications. Par exemple, le port 25 du protocole TCP est réservé au protocole SMTP, qui sert à l'envoi de courriels.

1.3.3 Protocole XBlast

Le protocole du jeu XBlast se base sur UDP. Etant donné qu'un message du serveur vers le client contient toujours la totalité de l'état du jeu, la perte de l'un d'entre eux n'est pas dramatique et se traduit par une saccade de l'affichage, nettement préférable à un ralentissement de la transmission, qui se traduit par un délai rendant rapidement le jeu injouable.

Cela dit, en pratique, lorsqu'une partie a lieu entre plusieurs joueurs qui sont physiquement proches les uns des autres, et donc probablement sur le même réseau local, il est très rare que des paquets UDP se perdent ou même arrivent dans le désordre.

2 Mise en œuvre Java

La mise en œuvre du client et du serveur implique l'utilisation de quelques classes de la bibliothèque Java qui n'ont pas été vue au cours et avec lesquelles il est nécessaire de se familiariser. Une brève introduction à ces classes est donnée ci-dessous, mais il est très important de bien lire leur documentation avant d'essayer de les utiliser.

2.1 Classes Buffer et ByteBuffer

La classe ByteBuffer du paquetage java.nio représente une mémoire tampon (ou simplement tampon) d'octets, c-à-d un tableau d'octets de taille fixe. Par rapport à un tableau Java, un tel tampon est doté d'attributs supplémentaires permettant un accès séquentiel à son contenu. Des instances de cette classe sont utilisées par la classe Channel et ses sous-classes — décrites plus bas — pour représenter les paquets transmis à travers le réseau.

Les principaux attributs que ByteBuffer possède en plus d'un tableau Java normal sont :

  • une position (position), qui donne la position du prochain élément à lire ou à écrire dans le tampon,
  • une limite (limit), qui donne la position du premier élément qui ne doit pas être lu ou écrit.

Ces deux attributs, bien décrits dans la documentation de la classe Buffer, permettent à une instance de ByteBuffer de se comporter un peu comme un flot, aux éléments duquel on accède de manière séquentielle. Etant donné que certaines méthodes de ByteBuffer permettent également d'accéder à ces éléments de manière aléatoire — c-à-d au moyen d'un index — un tel tampon peut être vu comme une combinaison entre un tableau et un flot.

Les méthodes de la classe ByteBuffer qui sont utiles à cette étape et dont le fonctionnement doit donc être bien compris sont :

  • allocate, qui permet la création d'un tampon d'une capacité (taille) donnée,
  • put (variante relative), qui permet l'écriture d'un octet à la position courante du tampon, qui est mise à jour en fonction,
  • put (variante absolue), qui permet l'écriture d'un octet à une position donnée,
  • flip, qui met la limite du tampon à la position actuelle, et réinitialise cette dernière à 0 ; cela permet de préparer pour la lecture un tampon dans lequel des données ont été écrites,
  • rewind, qui « rembobine » le tampon, en mettant à 0 sa position,
  • remaining, qui retourne le nombre d'octets restants dans le tampon, c-à-d la différence entre la limite et la position,
  • hasRemaining, qui retourne vrai si et seulement si il reste au moins un octet dans le tampon,
  • clear, qui met à 0 la position du tampon, et sa limite à sa capacité.

L'extrait de code ci-dessous montre comment créer un tampon d'une capacité de 100 éléments, y insérer les octets de 0 à 9 aux dix premières positions au moyen d'écritures relatives, remplacer le premier élément par 50 au moyen d'une écriture absolue, puis finalement afficher ces éléments.

ByteBuffer b = ByteBuffer.allocate(100);
for (int i = 0; i < 10; ++i)
  b.put((byte)i);               // écriture relative
b.put(0, (byte)50);             // écriture absolue
b.flip();
while (b.hasRemaining())
  System.out.println(b.get());  // 50, 1, 2, …, 9

2.2 Classes SocketAddress et InetSocketAddress

La classe InetSocketAddress du paquetage java.net, immuable, représente une paire (adresse IP, numéro de port). Pour cette étape, seuls deux constructeurs de la classe InetSocketAddress sont nécessaires :

  • le premier construisant une adresse en fonction d'un nom d'hôte et d'un numéro de port,
  • le second construisant une adresse en fonction d'un numéro de port uniquement.

Les adresses obtenues au moyen du second constructeur ne possédant qu'un numéro de port, elles ne sont utilisables que comme argument à la méthode bind d'un canal, décrite ci-dessous. En d'autres termes, elles ne sont utilisables que dans le code du serveur, qui désire recevoir des données, et pas dans le code du client, qui désire envoyer des données et doit donc forcément spécifier l'hôte auquel les envoyer.

L'utilisation de ces deux constructeurs est illustrée dans les extraits de code de la section ci-après.

2.3 Classes Channel et DatagramChannel

La classe DatagramChannel du paquetage java.nio.channels représente un canal de communication UDP. Le concept de canal est propre à la nouvelle version de la bibliothèque d'entrées/sorties Java (paquetage java.nio), et sert d'équivalent aux flots d'entrée/sortie de l'ancienne version. Toutefois, l'interface des canaux est très différente de celle des flots.

Les canaux de type DatagramChannel sont toujours bidirectionnels, et permettent donc à la fois la réception et l'envoi de données. Un canal peut être configuré pour être soit bloquant (son état par défaut), soit non bloquant. Lorsqu'il est bloquant, ses méthodes d'envoi (send) et de réception (receive) bloquent en attendant que les données soient effectivement envoyées ou reçues.

Les méthodes de la classe DatagramChannel qui sont utiles à cette étape et dont le fonctionnement doit donc être bien compris sont :

  • open, qui permet de créer un canal UDP,
  • close, qui permet de fermer le canal et libérer les resources système qui lui sont associées,
  • bind, qui permet de lier le canal à un port UDP,
  • configureBlocking, qui permet de rendre le canal bloquant ou non,
  • send, qui permet d'envoyer un paquet UDP sur le canal,
  • receive, qui permet de recevoir un paquet UDP du canal.

L'extrait de programme suivant montre comment utiliser un tel canal pour envoyer un paquet UDP constitué d'un seul octet valant 4 sur le port UDP 2016 de l'hôte local, c-à-d l'ordinateur sur lequel s'exécute le programme.

DatagramChannel channel =
  DatagramChannel.open(StandardProtocolFamily.INET);
SocketAddress address =
  new InetSocketAddress("localhost", 2016);

ByteBuffer buffer = ByteBuffer.allocate(1);
buffer.put((byte)4);
buffer.flip();

channel.send(buffer, address);

L'extrait de programme suivant montre quant à lui comment utiliser un tel canal pour attendre l'arrivée d'un paquet UDP sur le port 2016 et afficher son contenu à l'écran.

DatagramChannel channel =
  DatagramChannel.open(StandardProtocolFamily.INET);
channel.bind(new InetSocketAddress(2016));

ByteBuffer buffer = ByteBuffer.allocate(1);
SocketAddress senderAddress = channel.receive(buffer);

System.out.printf("Reçu l'octet %d de %s%n",
                  buffer.get(0),
                  senderAddress);

En plaçant ces deux extraits chacun dans un programme séparé et en lançant d'abord le second, puis le premier, le second devrait afficher à l'écran un message ressemblant à ceci, mais avec un numéro de port différent de 65347 :

Reçu l'octet 4 de /127.0.0.1:65347

Ces deux extraits de programme représentent respectivement un embryon de client et un embryon de serveur, et constituent donc un bon point de départ pour vous.

2.4 Serveur

La classe Main du paquetage ch.epfl.xblast.server est la classe principale du serveur. Elle n'offre rien d'autre qu'une méthode main acceptant les arguments du serveur sous la forme d'un tableau de chaînes de caractères, comme d'habitude.

Le serveur prend un seul argument, optionnel, qui donne le nombre de clients qui doivent se connecter avant que la partie ne démarre. Si cet argument n'est pas spécifié, il vaut 4 par défaut.

A noter que cet argument ne détermine pas le nombre de joueurs présents sur le plateau, qui vaut toujours 4, mais uniquement le nombre de clients qui doivent se connecter avant que la partie ne démarre. Lorsque ce nombre de clients est inférieur à 4, les joueurs auquel aucun client ne correspond sont simplement inertes. Ce comportement est utile pour le test, puisqu'en spécifiant qu'un seul client est nécessaire au démarrage de la partie, il est possible de lancer à la fois le client et le serveur sur le même ordinateur puis de contrôler le premier joueur via le clavier.

Si vous ne savez pas ou plus comment passer des arguments à un programme exécuté depuis Eclipse, vous pouvez consulter notre petit guide à ce sujet.

Le serveur attend les messages de la part des clients sur le port UDP 2016, comme l'embryon ci-dessus.

2.4.1 Phases

Le comportement du serveur est très différent dans chacune des deux phases du protocole décrites plus haut.

Dans la première phase, il boucle en attendant que le nombre requis de clients (distincts) lui ait envoyé un message exprimant leur intention de se joindre à la partie. Ce faisant, il construit une table associant les adresses des clients — retournées par la méthode receive de DatagramChannel — aux identités des joueurs correspondants.

Dans la deuxième phase, et tant que la partie n'est pas terminé, il boucle en envoyant l'état actuel à chacun des clients, dormant jusqu'à l'heure du prochain coup d'horloge (voir ci-après), récoltant les messages reçus des clients durant ce temps afin d'en déterminer les événements, puis calculant le nouvel état.

A la fin de la partie, l'identité du vainqueur est affichée à l'écran, s'il y en a un.

2.4.2 Gestion du temps

La méthode nanoTime de la classe System permet d'obtenir une notion de temps actuel à une résolution de la nanoseconde. Cette notion de temps actuel n'a pas de lien avec l'heure courante, et est locale à l'ordinateur sur lequel le programme s'exécute. Elle est néanmoins très utile pour le serveur, car si celui-ci mémorise le temps du début de la partie, il peut facilement déterminer le temps de chacun de ses coups d'horloge.

Ainsi, si \(T_s\) est le temps du début de la partie retourné par nanoTime, le temps \(t_n\) correspondant au coup d'horloge \(n\) est simplement donné par \(t_n = T_s + n\,\delta\) où \(\delta\) est la durée, en nanosecondes, d'un coup d'hologe — la constante Ticks.TICK_NANOSECOND_DURATION.

La méthode sleep de la classe Thread permet de « faire dormir » le programme pour une durée déterminée, spécifiée avec une résolution de la nanoseconde. Une fois que le serveur a envoyé l'état actuel à la totalité des clients, il doit calculer le temps restant jusqu'au prochain coup d'horloge puis, si celui-ci est positif, utiliser la méthode sleep pour attendre son arrivée. A ce moment, il peut calculer le prochain état en fonction des événements reçus entre temps, l'envoyer aux clients, et ainsi de suite.

Notez qu'il est possible — et même probable en début de partie — que le temps restant jusqu'au prochain coup d'horloge soit négatif, auquel cas il faut calculer le prochain état sans attendre.

Attention à ne pas faire systématiquement dormir le serveur durant 50 ms entre chaque coup d'horloge, car cela est incorrect ! En effet, le temps nécessaire au serveur pour calculer le prochain état doit être pris en compte, faute de quoi la durée d'un coup d'horloge est supérieure à ce qu'il devrait être, et peut varier en fonction du travail que le serveur effectue.

2.4.3 Réception des messages des clients

Juste avant de calculer le prochain état du jeu, le serveur doit consulter la totalité des messages reçus des clients durant son « sommeil ». Cela peut se faire facilement à condition que le canal de réception soit mis en mode non bloquant : dans ce cas, la méthode receive retourne immédiatement null si aucun message n'est disponible. Le serveur peut donc simplement exécuter une boucle tant et aussi longtemps que cette méthode ne retourne pas null, et construire les événements correspondant aux messages reçus, afin d'en tenir compte pour le calcul du nouvel état.

2.5 Client

La classe Main du paquetage ch.epfl.xblast.client est la classe principale du client. Elle n'offre rien d'autre qu'une méthode main acceptant les arguments du client sous la forme d'un tableau de chaînes de caractères, comme d'habitude.

Le client prend un seul argument, optionnel, qui donne le nom de l'hôte sur lequel se trouve le serveur. Si cet argument n'est pas spécifié, il vaut localhost par défaut.

A noter que cet argument peut simplement être passé au constructeur de la classe InetSocketAddress et que cette dernière accepte à la fois les chaînes représentant un nom d'hôte — p.ex. localhost — que celles représentant une adresse IPv4 — p.ex. 127.0.0.1.

2.5.1 Phases

Tout comme le serveur, le client a un comportement très différent dans chacune des deux phases du protocole décrit plus haut.

Dans la première phase, il boucle en envoyant à intervalles réguliers (p.ex. toutes les secondes) un message au serveur, exprimant son intention de se joindre à la partie. Il fait cela tant et aussi longtemps qu'il n'a pas reçu le premier état du jeu de la part du serveur.

Une fois le premier état du jeu reçu, le client le désérialise, l'affiche à l'écran, puis attend (de manière bloquante) le prochain état de la part du serveur et recommence.

2.5.2 Fils d'exécution

Comme toute application Swing, le client comporte deux fils d'exécution (threads) qui s'exécutent de manière concurrente : le fil principal (main thread), qui exécute la méthode main, et le fil Swing (event dispatch thread), qui gère l'interface graphique. Il est très important de comprendre les responsabilités de chacun de ces deux fils pour écrire un client correct.

Le fil principal doit commencer par faire en sorte que l'interface graphique soit construite par le fil Swing. Comme d'habitude, cela peut se faire en appelant la méthode invokeAndWait de SwingUtilities, en lui passant une lambda ne faisant rien d'autre qu'appeler la méthode créant l'interface utilisateur (souvent nommée createUI).

Cela fait, le fil principal se charge de la plupart de la communication avec le serveur. Comme cela a été expliqué plus haut, dans un premier temps il lui communique son intention de se joindre à la partie, et dès qu'il reçoit pour la première fois un état de la part du serveur, il le transmet au fil Swing, puis attend — de manière bloquante — le prochain état, et ainsi de suite.

Le fil Swing, de son côté, affiche l'état du jeu lorsque le fil principal le lui demande via un appel à la méthode setGameState du composant XBlastComponent. De plus, il gère les événements clavier et envoie au serveur les actions correspondantes.

2.6 Tests

Pour tester votre client et votre serveur sur une seule machine, vous pouvez lancer tout d'abord le serveur, en lui passant 1 en argument afin qu'il n'attende la connexion que d'un client, puis le client. Pour passer 1 en argument au serveur, suivez les indications de notre guide Eclipse sur les configurations d'exécution.

En plus de ces tests locaux, il est fortement conseillé d'essayer d'utiliser votre client avec le serveur d'un autre groupe, et inversément. Cela vous permettra de vous assurer que vous avez bien respecté le protocole et le format de sérialisation de l'état spécifiés dans les étapes précédentes.

Pour jouer une partie avec plusieurs joueurs utilisant des ordinateurs séparés, il faut tout d'abord déterminer l'adresse IP de l'ordinateur sur laquelle le serveur sera exécuté. Il existe de très nombreux moyens de l'obtenir, le plus simple étant probablement d'utiliser un site comme ipinfo. En cliquant sur ce lien, vous verrez s'afficher l'adresse IP de votre ordinateur, que vous pouvez communiquer oralement aux autres joueurs, pour peu bien entendu que ce soit l'ordinateur exécutant le serveur.

Pour faire ces tests, assurez-vous d'être connecté au réseau de l'EPFL, soit par Wifi, soit par le réseau câblé. Pour différentes raisons — en particulier l'utilisation systématique de NAT — il est très peu probable que vous soyez en mesure d'échanger des données entre un serveur et un client situés sur des machines accédant à Internet via un fournisseur d'accès Internet commercial (Swisscom, Cablecom, etc.). Si vous désirez néanmoins effectuer des tests depuis l'extérieur de l'EPFL, connectez-vous dans un premier temps au VPN de l'EPFL.

3 Résumé

Pour cette étape, vous devez :

  • écrire les classes Main du client (paquetage ch.epfl.xblast.client) et du serveur (paquetage ch.epfl.xblast.server) en fonction des spécifications données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies,
  • rendre votre projet soit dans le cadre du rendu final anticipé (si vous désirez tenter d'obtenir un bonus), soit dans le cadre du rendu final normal (sinon) ; les instructions concernant ces rendus seront publiées ultérieurement.

Attention : votre projet terminé sera testé « manuellement » par des correcteurs, et il est donc capital que les classes principales du client et du serveur soient nommées conformément aux instructions ci-dessus, et que les deux programmes principaux acceptent les arguments optionnels décrits, avec les valeurs par défaut données. Les groupes rendant un projet ne respectant par l'une ou l'autre de ces règles perdront la totalité des points attribués au test de leur projet.

Faites très attention à ce point, il y a malheureusement chaque année quelques groupes qui perdent de nombreux points suite au non respect de telles règles.