# Jeu de bataille navale en réseau ## Introduction **Contexte**: ce rapport est le travail réalisé dans le cadre d'un sujet écrit noté à rendre dans le cadre du [cours de Réseaux sur le thème TCP/IP](http://dept-info.labri.fr/~thibault/enseignements.html), au sein de la [licence pro ADSILLH ](http://dept-info.labri.fr/adsillh/)de l’université de Bordeaux: la réalisation d'un jeu de bataille naval en réseau (2 clients-1 serveur). Voici les liens vers [le sujet](http://dept-info.labri.fr/~thibault/Reseau/sujet.pdf), le code source de départ (Copyright: [Samuel Thibault](http://dept-info.labri.fr/~thibault))et les [modalités de rendu](http://dept-info.labri.fr/~thibault/projet). **Informations sur l'auteur**: il s'agit du premier programme de plus de 10 lignes que j'ai écrit dans ma vie, si bien qu'au vu de mon niveau débutant en programmation, j'ai tente de fournir un niveau de détails assez élevé par rapport, pour tenter d’éviter trop de sauts explicatifs de futurs lecteurs également débutants en programmation et intéressés par le thème des Réseaux. Ce rapport comprend aussi bien les recherches techniques que j'ai pu effectuer en termes de programmation en Python, que sur les notions de réseau (TCP/IP), ou sur les attitudes adaptées et les sentiments vécus face à un premier projet de cette envergure pour ma part.J'envoie mes remerciements a ceux qui ont répondu a mes questions au cours de cet exercice (ils sont cites au fur et a mesure du rapport): Simon Crespeau (aide théorique et pratique en direct), Samuel Thibault (enseignement), Florian Lassenay (conseils), les auteurs des blogs et sites dont les URLs sont indiques tout au long de ce rapport (ressources théorique et pratiques). **Temps total passé** (écriture du code et du rapport): 29H. **Pre-requis**: notions intermédiaires en Bash. **Langages utilises**: Bash, Python 3. **Configuration de ma machine**: Debian 10. **Contact**: habib.belaribi@aquilenet.fr **Rapport sous licence**: CC by SA, **disponible à ce lien** (une fois les licences du code source du main[point]py et du game[point]py confirmées). --- ## Table des matières [TOC] --- ## 1. Première lectures du sujet et du code source Au terme de cette première lecture, j'en conclus que: - je ne suis pas sur de comment fonctionne une liste et ses méthodes dans Python3 (.append, .remove, ...) - https://www.w3schools.com/python/python_lists.asp - je ne sais pas comment fonctionne **class** - https://www.w3schools.com/python/python_classes.asp - je ne comprends pas a quoi correspond le **_ _init_ _** - https://www.w3schools.com/python/python_classes.asp - j'ai un doute sur le fonctionnent **def**, qu'est-ce que **return**? - https://www.learnpython.org/en/Functions - je ne sais pas ce que sont **ord** et **chr** (ont l'air de typage)? - il n'y a pas de **try...except** dans ce code comme vu dans de précédents cours, faut-il en rajouter ? - regarder comment passer précisément un argument a l'appel d'un programme avec Python? - https://www.pythonforbeginners.com/system/python-sys-argv ## 2) Recherche plus approfondie des éléments en jeu dans une communication TCP serveur-clients ### 2-a) "Spoken human P2P" ("le pair-à-pair entre humains via la parole") J'ai d'abord demande a Florian et Simon quelles étaient les notions et supports en jeu pour savoir par ou commencer raisonnablement. - Florian m'a conseille de voir le TP 5 en entier pour commencer, ce que je fais: - sujet et corrections du sujet: - echo-tcp - echo-udp - echo-tcp-select - setsockopt? - l.remove? - echo-tcp-thread - chat - chat-thread - Je retiens que Simon m'indique que pour une connexion TCP, il faut bien faire la distinction entre le premier File Descriptor d'une connexion au serveur et le deuxième File Descriptor de lecture/écriture ouvert pour cette connexion. ### 2-b) "Written human P2P" ("le pair-à-pair entre humains via l’écriture") Je me pose ensuite les questions suivantes a la relecture du sujet, et identifient pour chacune d'elle des ressources écrites imprimées et en ligne susceptibles d'y répondre. je choisis de lister tous les liens ici car je considère le temps de recherche en ligne comme suffisamment chronophage pour qu'il mérite d’être partagé: * *QUELLE EST la différence entre une communication TCP de 1 client a 1 serveur, et de 2 clients au même serveur, en termes de fonctions invoquées?* * **cours de réseau - TP 5: Chat** * http://dept-info.labri.fr/~thibault/enseignements.html * **créer et confirmer une connexion client vers serveur en écoute** : * https://www.tutorialspoint.com/python/python_networking.htm * https://www.youtube.com/watch?v=Lbfe3-v7yE0 * envoi et réception de données entre 1 client et 1 serveur (notion de buffer en +) : https://www.w3schools.in/python-tutorial/network-programming/#Defining_Socket * https://www.geeksforgeeks.org/socket-programming-python/ * send() versus sendall(): https://realpython.com/python-sockets/ * matérialiser la **différence entre port, socket, socket d’écoute, sockets de communication** * https://medium.com/fantageek/understanding-socket-and-port-in-tcp-2213dc2e9b0c * comprendre **select.select()**, et saisir le lien entre socket, port, connexions, et file descriptors ouverts lors d'une connexion en TCP: * https://pymotw.com/2/select/ * https://www.youtube.com/watch?v=2ukHDGLr9SI * comprendre le **threading** pour des connexion multi-clients : * https://www.geeksforgeeks.org/socket-programming-multi-threading-python/ * https://realpython.com/python-sockets/#multi-connection-client-and-server * autres ressources rencontrées qui m'ont semble intéressantes pour les notions de réseau en général, mais pas directement utiles pour répondre a ce sujet: * http://xahlee.info/linux/tcp_ip_tutorial.html * *COMMENT chaque client peut-il récupérer sur sa machine des données qui se répondent entre elles, c'est-à-dire suivre une même histoire qui se déroule chacun sur leur machine (de manière relativement synchrone, pour éviter qu'une partie ne dure plusieurs heures, voire jours: on imagine que les deux joueurs restent face à leur écran, concentrés sur la mémé partie au même moment)? Et sous quelle forme (comment ces données s'affichent-elles de manière lisibles pour la personne qui est face à son écran côté client)?* * " accept() : blocks and waits for an incoming connection. When a client connects, it returns a new socket object representing the connection and a tuple holding the address of the client. The tuple will contain (host, port) for IPv4 connections or (host, port, flowinfo, scopeid) for IPv6. See Socket Address Families in the reference section for details on the tuple values.One thing that's imperative to understand is that we now have a new socket object from accept(). This is important since it's the socket that you'll use to communicate with the client. It's distinct from the listening socket that the server is using to accept new connections" * https://realpython.com/python-sockets/ * *QUE SIGNIFIE "établir un protocole textuel ascii"? Quel autre type de protocole pourrait-on avoir?* * https://stackoverflow.com/questions/5909873/how-can-i-pretty-print-ascii-tables-with-python * https://stackoverflow.com/questions/11741574/how-to-print-utf-8-encoded-text-to-the-console-in-python-3 * https://codereview.stackexchange.com/questions/122970/python-simple-battleship-game * https://realpython.com/python-encodings-guide/ * https://www.devdungeon.com/content/create-ascii-art-text-banners-python * http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html * *QUE VEUT DIRE choisir "comment chaque client fait son affichage, indépendamment du protocole"? Que le même protocole doit potentiellement fonctionner pour différentes façon d'afficher les coups joues, la grille et les résultats, sur différents clients?* ## 3) Test du jeu seul face a la machine 1. Je me place dans le dossier décompressé où se trouvent les 2 fichiers main.py et game.py `cd ~/chemin_du_dossier_ou_se_trouvent_main_et_game` 2. Je rends les 2 fichiers exécutables pour pouvoir les lancer `chmod +x game.py main.py` --> Je vérifie que c'est bien le cas avec`ls -l *.py`. C'est OK. 3. Je lance la commande `./main.py` (programme qui importe lui-même toutes les fonctions dont il a besoin pour le bon fonctionnement du jeu depuis le fichier game[point]py), et je commence une partie avec la machine. Tout fonctionne. 4. Par mégarde, je tape + d'un caractère en renseignant une colonne, et j'obtiens une erreur qui me fait quitter la partie en cours. Je m’aperçois alors que je dois en recommencer une depuis le début en relançant le script. Et me dis que je devrai rester vigilant de traiter le cas ou l'un des 2 joueurs se déconnecte en cours de partie, provoquant a priori la fin de la partie pour son adversaire avec le code actuel. A voir. ## 4) Relecture détaille du code source et actions: ### 4-a) Je relis le code source... ...et commente chaque étape de la fonction principale `def main()` du main.py, que j'identifie comme correspondant a l'algorythme de déroulement d'une partie. ### 4-b) Sous forme d'un schéma... ...je retranscris le déroulé de ces étapes d'échange d'informations entre les 2 clients qui jouent ensemble en passant par le même serveur J'y ajoute également les input/output générés par le code source a chaque étape d'une partie, pour mieux visualiser les échanges attendus entre un client et un serveur. [photo du brouillon des annotations sur le main.py, page 2] ### 4-c) Début de l’écriture du code NB: Les paragraphes, A), B1), B2), etc, sont nommés de la même manière que dans les commentaires des 2 programmes *client_mybattleship.py* et *serveur_mybattleship.py* que j'ai écrit dans le code indique sur le lien git en introduction de ce rapport. #### 4-c) - A)serveur et B1)client: lancement du programme du serveur sans argument, et sur les clients, lancement du programme avec le nom du serveur en paramètre - Problème rencontre lors de l'usage de sys.argv[1] : je découvre qu'il faut convertir l'argument en str() pour pouvoir utiliser une variable qui l'appelle. - source qui m'a servi : https://www.pythonforbeginners.com/argv/more-fun-with-sys-argv) - Problème de format de données renvoyées par le serveur : rajout de bytes() dans le c.send() dans un premier temps, puis abandon au profit de .decode (voir 4-c)FIN) - source qui m'a servi: https://www.youtube.com/watch?v=Lbfe3-v7yE0 - J'ai tente d'utiliser une variable `host_ip = socket.gethostbyname('host_name') ` dans l’idée d'inclure une résolution du hostname au sein du programme pour que chaque client puisse taper n'importe quel hostname en argument même s'il n'en connaît pas l'adresse IP, mais sans succès. Je n'ai fait que recevoir une erreur en retour : `socket.gaierror: [Errno -5] No address associated with hostname` . Je n'ai pas réussi a la résoudre au bout d'1 heure environ et j'ai choisi d'abandonner le .gethostname() pour le moment. Il faut écrire l'IP complet pour le moment, que l'on passe en argument lors du lancement du programme cote client. - Sur le programme du serveur, j'ai voulu visualiser que le socket d’écoute et le socket de communication (envoi/réception de messages avec des clients) étaient bien différentes en ajoutant la variable 's' dans un print:`print("Socket successfully created:",s)`, ce qui me renvoyait le message suivant `Socket successfully created: <socket.socket fd=3, family=AddressFamily.AF_INET6, type=SocketKind.SOCK_STREAM, proto=0, laddr=('::', 0, 0, 0)>`, ainsi qu'en rajoutant la variable correspondant a la connexion du socket de communication dans un print `print("Got connection from", addr,c)`, ce qui me renvoyait le message `Got connection from ('::ffff:127.0.0.1', 33824, 0, 0) <socket.socket fd=4, family=AddressFamily.AF_INET6, type=SocketKind.SOCK_STREAM, proto=0, laddr=('::ffff:127.0.0.1', 1234, 0, 0), raddr=('::ffff:127.0.0.1', 33824, 0, 0)` . J'ai ainsi pu observer que les file descriptors (fd=3 et fd=4 etaient bien differents, et que le **fd** du socket d'ecoute ne possede pas de champ **raddr** correspondant au parametre d'identification du client connecte sur le second socket de connexion). J'ai alors le sentiment de mieux materialiser le fonctionnement des sockets. #### 4-c) - B2)serveur: creation et gestion des sockets de connexion des 2 clients - Ne comprenant pas en detail comment fonctionne la fonction **select.select()**, je decide d'utiliser d'abord les fonctions de **threading**, en faisant un copier-coller du cours echo-chat-thread, mais ne comprend pas exactement comment cela fonctionne au niveau des blocages et deblocages de fils de processus. J'obtiens des erreurs que je ne saisis pas bien cote client, du type : ``` Exception in thread Thread-1: Traceback (most recent call last): File "/usr/lib/python3.7/threading.py", line 917, in _bootstrap_inner self.run() File "/usr/lib/python3.7/threading.py", line 865, in run self._target(*self._args, **self._kwargs) File "./serveur_mybattleship.py", line 32, in f new_connection, address = s.accept() ConnectionResetError: [Errno 104] Connection reset by peer ``` Après avoir demande à Simon ce qu'il en pensait, celui-ci m'a invité à plutôt utiliser la fonction select.select() et m'explique clairement comment elle marche. J'en comprends qu'elle permet d'identifier facilement quelles sont les sockets en cours d'usages, de les lister, et donc de choisir sur laquelle on souhaite recevoir ou envoyer des données. Je vais donc facilement pouvoir assigner 2 sockets à chacun des joueurs, en admettant qu'ils n'effectuent pas une déconnexion-reconnexion en cours de partie, auquel cas, la partie serait perdue et devrait alors être réinitialisée par les 2 clients (faute de code plus élaboré). J’écris alors une première version du code du paragraphe B1) sur le programme du serveur. #### 4-c) - C1)client: envoi des réponses de confirmation par chaque client - Après avoir écrit le paragraphe C1 cote client, je rencontre un bug que je ne comprends pas et découvre l'outil `pdb.set_trace()` sur les conseils de Simon, qui est une fonction que l'on insère dans un endroit du programme ou l'on souhaite observer ce qui bug de manière plus fine, pour tenter de debugger. Dans mon cas, le bug obtenu est le suivant: alors que le client envoie (ligne 34, c.send()) le contenu du prompt (oui/non), le serveur reçoit bien la réponse en octets (symbole 'b' pour bytes devant la réponse, mais aussi une réponse vide juste a la suite qui le fait rentrer dans la boucle `if` suivante et effectuer un `new_connection.close()`. Finalement, en tapant *"python issue c.recv two messages including 1 empty from input on client side"* dans le moteur de recherche, je tombe sur un lien (source: https://docs.python.org/2/howto/sockets.html) qui me donne la réponse "*When a recv returns 0 bytes, it means the other side has closed (or is in the process of closing) the connection"* . Je comprends alors que c'est tout simplement parce que mon programme se termine cote client que le serveur reçoit une information vide d'octets. Je choisis alors une solution temporaire pour pouvoir continuer de coder cote serveur, et ajoute un `time.sleep(5)` en fin de programme cote client. Que j'abandonne très vite n'en voyant pas l’utilité. #### 4-c) - C2)serveur: confirmation des joueurs, lancement et déroulement d'une partie - Je me penche sur l'initialisation d'une partie et déroulé dans un premier temps les lignes de code de manière assez fluide: - d'abord, je récupère les premières lignes de la fonction main() du programme main[point]py, et les renvoie a chaque joueur en assignant a chacun un socket de communication distinct; - ensuite, j’écris comment gérer la réception et le renvoi des réponses d'un joueur à l'autre: - à ce moment-la je me questionne: est-il possible qu'un joueur envoie plusieurs fois de suite un message, ce qui parasiterait l’enchaînement logique des coups? Est-il utile de considérer les fonction de threading ici, afin de bloquer chaque enchaînement de couple "réception du message d'un joueur-renvoi du message a l'autre joueur"? - je réalise alors que je ne suis pas sur de l'endroit ou l'ensemble du déroulement d'une partie doit être positionne dans le code? C'est le string=new_connection.recv() et le l.remove(new_connection) qui me font hésiter : ou doivent-ils apparaître dans l'ordre d’enchaînement des instructions par rapport au code du déroulement d'une partie? - je fais alors des aller-retour entre le paragraphe B2)serveur et C2)serveur, pour finalement saisir que la partie doit se dérouler dans B2) au sein du 2eme cas de figure, qui est celui de la réception de données par les sockets de communication. Au sein de ce dernier, on doit distinguer 2 sous-cas de figure: celui ou un des clients connecte s'est déconnecté (pour en informer son adversaire et mettre fin a la partie), et celui ou les deux clients sont bien connectes au sein duquel on va pouvoir exécuter le code principal de la bataille naval a proprement parler. #### 4-c) - D1)client: lancement et déroulement d'une partie Ici, pas de découvertes ou de blocage particulier à cette étape. #### 4-c) - E)F)client: clôture d'une partie ici, pas de découvertes ou de blocage particulier à cette étape. #### 4-c) - Debuggage: lancement du code sur 3 terminaux bash pour simuler 2 clients qui se connectent a un serveur - La première surprise est grande. Lorsque je lance les programmes, les 3 terminaux m'affichent la même chose, a savoir directement la grille d'une partie a jouer, avant meme les confirmations de connexion entre client et serveur. *Sentiment de panique*. Je me dis qu'il y a un principe fondamental ordination ou d'import des fichiers python... que je n'ai pas saisi. - En testant le jeu sur chaque terminal, je comprends que je joue contre la machine sur les 3 terminaux. Comme si mes 2 programmes allaient chercher directement la fonction main() - Je décide de me concentrer sur la ligne `from mainbattleship import *` et tente instinctivement la chose suivante : dans le mainbattleship.py (copie du fichier main.py), je supprime les lignes de code relatives a la création de la fonction `def main()` puisque cela est écrit de la même manière dans le code de mes deux nouveaux programmes. - Je relance alors le programme sur 3 terminaux. Le début du programme marche a nouveau cote serveur, puisque j'obtiens : ``` habsinn@habsinn:~/Bureau/TOUT/Cours_fac/bataille$ ./serveur_mybattleship2.py Socket crée sans problème socket relie au port 1234 Le socket est en ecoute de nouvelles connexions clients Un nouveau client vient de se connecter: ('::ffff:127.0.0.1', 59550, 0, 0) Un nouveau client vient de se connecter: ('::ffff:127.0.0.1', 59552, 0, 0) ``` - Je peux alors me lancer dans ce quoi je crois être du debuggage, en lisant les messages d'erreur rencontres au fur et a mesure: oubli d'arguments, erreurs de typographies dans le nom des variables,... - Je rencontre alors un nouveau problème: je m’aperçois que le programme n'avance plus après l'envoi des 2 réponses 'oui' par chacun des 2 clients - Je tente de réutiliser l'outil `pdb.set_trace()`. En le positionnant sur différentes lignes par supposition, je finis par voir que cela ne semble pas fonctionner au niveau des lignes 44 et 62 de mon programme. - Je décide de m’arrêter ici pour l'envoi du rapport au vu de la date de rendu. ### 4-d) Prochaines étapes identifiées - Reprendre et poursuivre le debuggage a partir des lignes 44 (sur le fichier client_mybattleship.py) et 62 (sur le fichier serveur_mybattleship.py). ## 5. ReadME Voir fichier README.md contenu dans le tar. ## 6: Mise en forme telle que demandée Voir [modalités de remise du sujet](http://dept-info.labri.fr/~thibault/projet).