Labo 1 - Architecture Logicielle de la donnée

Structure générale

Respectueusement des conventions de Go, la structure du projet se divise en un package cmd ne faisant qu’utiliser les packages définis dans internal.

Architecture en couches

Dans un soucis de séparation des préoccupations, ainsi que pour faciliter l’extension dans le futur, nous choisissons une architecture en couches, dont il en existe pour l’instant deux : Transport et Server.

Transport

La couche transport occulte la complexité du réseau derrière une abstraction qui prend la forme d’une interface NetworkInterface, définie dans networkInterface.go. Cela permettra un changement de protocole de communication sans effets pour les couches utilisatrices du réseau.

Abstraction

L’interface NetworkInterface définit tout objet capable d’envoyer et recevoir des octets de la part de processus identifiés par une adresse IP. L’utilisation d’octets permet à cette couche d’être indépendante de son contexte d’utilisation.

La NetworkInterface utilise un modèle de souscription pour la réception de messages. Elle définit pour cela une interface MessageHandler décrivant tout objet offrant une méthode HandleNetworkMessage(*Message) (wasHanlded bool) qui retourne un booléen ssi le message reçu a été traité. La NetworkInterface est alors responsable de partager chaque message reçu à tous les souscrits à travers cette méthode, jusqu’à ce que l’un d’eux affirme l’avoir traité. Elle peut donc supposer que tout message n’appartient qu’à un seul souscrit.

Les méthodes d’une NetworkInterface sont les suivantes :

Nous n’offrons pour l’instant qu’une seul implémentation de cette interface, UDP, que nous décrivons dans la suite de cette section.

État interne

L’état interne d’une instance de UDP, c’est à dire toute donnée dont dépend le comportement de l’instance et qui peut changer au cours de l’exécution, se réduit aux valeurs suivantes.

Il est important de déterminer ces états puisque, par leur variabilité à travers le temps, il est nécessaire d’en prévenir tout accès concurrent, ce qui est garanti par le choix des goroutines.

Goroutines principales

Nous analysons ici les contraintes techniques auxquelles nous faisons face, et les goroutines permettant de les satisfaire.

Les événements auxquels cette couche doit répondre sont les suivants, associés aux états auxquels elles doivent avoir accès

Étant donné qu’aucune paire de ces événements n’a besoin de pouvoir être exécutée en parallèle, nous optons pour la solution simple de regrouper leur gestion en une seule goroutine, handleState. Ainsi, tous les événements seront traités séquentiellement, évitant donc tout risque d’accès concurrent aux variables d’état. Afin d’éviter toute erreur lors de l’implémentation, ces variables d’état sont locales à la goroutine, et non des attributs de la struct UDP.

La gestion de la clôture de l’interface réseau se fait à l’aide d’une unique channel, closeChan, qui sera clôturée au moment d’une demande de clôture. Elle pourra ainsi être surveillée par toutes les goroutines pour détecter leur nécessité de s’interrompre.

Une seconde goroutine, listenIncomingMessages, est responsable d’écouter les messages reçus sur UDP, et les transmettre à handleState pour envoi aux souscrits. Celle-ci génère une petite goroutine écoutant simplement closeChan et clôturant la connexion UDP d’écoute, permettant de notifier la goroutine principale en faisant échouer l’écoute.

Enfin, afin d’éviter de recréer une connexion au même voisin à chaque envoi, la goroutine handleState crée une goroutine pour chaque voisin auquel une demande d’envoi a été faite. Cette dernière est responsable de la connexion avec ce voisin, et une channel créée et maintenue par handleState lui est fournie. Cette channel sera utilisée par handleState pour informer la goroutine du message à envoyer, puis clôturée pour indiquer la fin de programme.

Pour le détail des goroutines et leurs moyens de communication…

Il existe donc trois goroutines principales communiquant par channels.

Serveur

Le serveur est responsable uniquement de l’écoute d’entrée sur stdin, et l’affichage des messages reçus. Aucun état modifiable majeur n’existe dans cette couche.

Le serveur est une struct offrant deux méthodes.

Goroutines principales

Il existe une seule goroutine centrale au serveur. Celle-ci est responsable d’écouter et envoyer sur le réseau les entrées de l’utilisateur.ice. Elle crée également une goroutine secondaire responsable uniquement de traduire l’appel bloquant à la lecture IO en une transmission sur channel, afin de permettre l’utilisation du select de Go.

La gestion des réceptions se fait par une souscription au NetworkInterface lors de la construction de la structure. Aucune goroutine n’est donc nécessaire ici.

Structures additionnelles

Quelques structures supplémentaires permettent de séparer les préoccupations.

Modules complémentaires

Quelques autres modules d’aide sont fournis.