Master Expert Technologie de l'information EPITECH 2020.
Co-fondateur et CTO d'une startup dans l'Edtech 2019 - fin 2022. (+3 ans)
Formation PSPO-1 Agile Scrum 2022.
Co-fondateur et CTO d'une startup dans la Deeptech fin 2022 - aujourd'hui.
Qu'est-ce qu'une architecture N-Tiers ?
Création d'une architecture Client / Serveur d'un service IA.
Les modes de communications entre processus ?
QCM de fin de module et rendu des TPs et exercices
Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.
Après la théorie, nous verrons alors la pratique en réalisant des exercices.
Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.
En 1960, les États-Unis cherche à créer un réseau décentralisé pour résister à une attaque nucléaire.
Un système complexe de router utilisant les protocoles IP et TCP pour envoyer les paquets.
Des protocoles apparaissent au fil des ans : UDP, DNS, HTTP, HTTPS, TLS, SSL, VOIP...
Une construction à partir du « bas »
3 niveaux d’interconnexion
- Mécanismes réseau de base (TCP/IP)
- Gestion des noms et des adresses
- Des outils et des protocoles spécialisés
- Le langage HTML et CSS
- La technologie Adobe Flash
- Le langage Javascript puis Typescript
- De nombreux frameworks...
- Les Websockets
- QUIC - HTTP/3
- Etc...
Le modèle OSI (Open Systems Interconnection) développé par l'ISO (Organisation internationale de normalisation) est un cadre conceptuel standard qui décrit comment les différentes couches d'un système de communication réseau interagissent.
Le modèle possède 7 couches qui permettent :
Physique - Gère les aspects matériels de la transmission des données (câbles, signaux, connecteurs, etc.).
Exemples : Ethernet, USB, Wi-Fi.
Liaison de Données - Assure le transfert fiable des données entre deux nœuds adjacents et gère les adresses matérielles (MAC) et la détection/correction d’erreurs.
Exemples : Ethernet, PPP (Point-to-Point Protocol).
Réseau - Responsable de l'acheminement des paquets à travers plusieurs réseaux et gère les adresses IP et le routage.
Exemple : IP (Internet Protocol).
Transport - Garantit le transfert fiable et ordonné des données entre deux hôtes et gère la segmentation, le contrôle de flux et la correction d'erreurs.
Exemples : TCP, UDP.
Session - Gère les sessions de communication entre deux applications et s'occupe de l’établissement, du maintien et de la terminaison des connexions.
Exemple : NetBIOS.
Présentation - Assure la traduction, le chiffrement/déchiffrement et la compression des données et fait en sorte que les données soient lisibles par l'application.
Exemple : SSL/TLS, JPEG, ASCII.
Fournit des services réseau aux applications utilisateur et inclut les protocoles utilisés pour le courrier électronique, le web, etc.
La communication se fait de la plus basse à la plus haute, donc de la couche Physique jusqu'à la couche Application.
HTTP - HyperText Transport Protocol - Port 80
RPC - Remote Procedure Call - Port 1024-5000
SSH - Secure Shell - Port 22
FTP - File Transfer Protocol - Port 21
SMTP - Simple Mail Transfer Protocol - Port 25
Utilisent les sockets - interface de programmation permettant l'échange de données (via TCP ou UDP).
Le plus simple, service de type best-effort : les paquets peuvent être perdus ou arriver dans le désordre.
Rapide : pas de délai de connexion, pas d'état entre émetteur / récepteur.
Léger : petite en-tête donc économie de bande passante, sans contrôle de congestion.
Streaming audio ou video, mieux vaut perdre quelques frames que ralentir tout le flux.
Jeu video pour le temps réel des jeux en ligne comme League of Legends ou Call of Duty.
WebRTC pour le temps réel dans le Web pour des chats textuels ou video.
Applications qui envoient peu de données et qui ne nécessitent pas un service fiable : DNS, SNMP, BOOTP/DHCP.
Transport fiable en mode connecté : assure la délivrance des données en séquence entre deux adresses.
Contrôle la validité des données reçues, organise les reprises sur erreur ou sur temporisation.
Réalise le contrôle de flux : ecriture dans un tampon, et le contrôle de congestion.
HTTP car chaque donnée est essentielle, évitons de n'envoyer qu'une partie du mot de passe de l'user.
FTP car stocker un fichier à moitié ou avec une écriture dans le mauvais ordre ne sert à rien.
SMTP car on ne peut pas perdre la demande d'envoi d'un e-mail.
UDP - User Datagram Protocol
Rapide, léger, sans connexion.
Pas de garantie de livraison, d’ordre, ou de retransmission.
Idéal pour : streaming, VoIP, jeux en ligne (faible latence).
TCP - Transmission Control Protocol
Fiable, orienté connexion.
Garantit la livraison, l’ordre, et la retransmission des données perdues.
Idéal pour : navigation web, email, transferts de fichiers (intégrité critique).
En bref : UDP pour la vitesse, TCP pour la fiabilité.
Une architecture N-tiers est un modèle de conception de logiciels dans lequel une application est divisée en plusieurs couches (ou tiers) distinctes, chacune ayant un rôle spécifique. Cette séparation améliore la modularité, la maintenabilité, et la scalabilité de l’application.
Chaque tier est indépendant des autres et interagit avec eux via des interfaces définies.
Les tiers peuvent être déployés sur des serveurs distincts ou combinés sur un même serveur, selon les besoins.
Exemple concret : e-commerce
Tier présentation : Une boutique en ligne où les utilisateurs naviguent (front-end).
Tier logique applicative : Traitement des commandes, gestion du panier, calcul des frais de livraison.
Tier données : Base de données contenant les informations sur les utilisateurs, produits, et commandes.
Vous est-il déjà arrivé d’entrer dans un restaurant et de commander votre repas directement au cuisinier ?
Vous attendez qu’un serveur vienne prendre votre commande. Vous lui dites ce que vous voulez manger et celui-ci va dans la cuisine et le dit au cuisinier.
Vous n'utilisez pas nécessairement le bon jargon pour le cuisinier, et ce dernier ne sait peut-être pas à quelle table envoyer votre plat lorsqu'il est prêt.
Ici notre Client est le serveur et notre Serveur le cuisinier.
Le but est de permettre à notre utilisateur d'utiliser le Serveur facilement en utilisant notre Client.
Monolithe : unifiée et contient toutes les fonctionnalités dans un seul bloc. (Ruby on Rails)
Microservice : divisée en plusieurs services indépendants, chacun ayant une fonction spécifique.
Événementielle : les composants communiquent en émettant ou en réagissant à des événements via un message broker.
Serverless : le code est exécuté dans des environnements où l'infrastructure est entièrement gérée par un fournisseur.
Hexagonale : centrée sur la logique métier, elle définit des interfaces pour interagir avec l'extérieur via des adaptateurs.
Et d'autres...
Type | Modularité | Scalabilité | Complexité |
---|---|---|---|
N-tiers | Moyenne | Bonne | Moyenne |
Monolithique | Faible | Limitée | Faible |
Microservices | Élevée | Très bonne | Élevée++ |
Événementielle | Élevée | Très bonne | Élevée |
Serverless | Moyenne | Automatique | Élevée |
Hexagone | Très élevée | Bonne | Moyenne++ |
Centralisation des ressources et des données
Simplifie la gestion et la maintenance, facilite les sauvegardes et la sécurité des données et évite la duplication inutile des données.
Séparation des responsabilités
Le client se concentre sur l'interface utilisateur (UI) et l'expérience utilisateur. Le serveur gère les calculs, la logique métier et la gestion des données. Cela permet une spécialisation.
Les serveurs peuvent être mis à niveau pour gérer un nombre croissant de clients, les clients peuvent également être développés ou diversifiés (ordinateurs, mobiles, tablettes) sans affecter le serveur.
Un middleware est un logiciel intermédiaire qui agit comme un pont entre différentes applications, systèmes ou composants au sein d’une architecture informatique.
Exemples
Il existe énormément de bonnes pratiques et méthodologies pour réaliser une bonne conception d'application.
Nous allons voir ensemble le Domain Driven Design qui possèdent de nombreux avantages et permet de gérer proprement les N-tiers.
Première fois cité dans le livre de Eric Evans du même titre publié en 2003
Le DDD est une architecture pour la conception de logiciels
Place la logique métier au centre des préoccupations.
Isoler la logique métier de la logique applicative / infrastructure.
Abstraire les outils et technologies utilisés.
Un seul et même langage pour toute l'équipe, il est très facile de collaborer.
La couche domaine met en œuvre la logique commerciale centrale, indépendante des cas d'utilisation, du domaine/système.
La couche application met en œuvre les cas d'utilisation de l'application basés sur le domaine. Un cas d'utilisation peut être considéré comme une interaction de l'utilisateur avec l'interface utilisateur (UI).
La couche présentation contient les éléments de l'interface utilisateur (pages, composants) de l'application.
La couche infrastructure soutient les autres couches en mettant en œuvre les abstractions et les intégrations avec les bibliothèques et les systèmes tiers.
Nous allons créer une première application en Client / Serveur en NodeJS pour réaliser notre premier "Hello world". N'hésitez pas à télécharger NodeJS pour la suite du TP si vous ne l'avez pas encore.
Pour faciliter le rendu final des TPs et exercices, nous allons tout de suite créer un dépôt sur Github en publique et le cloner sur notre machine.
Ensuite, créez le dossier "TP-Sockets" et lancez la commande :
npm init
Acceptez toutes les options par défaut, nous utiliserons la librairie Net pour utiliser les sockets et faire communiquer notre client et notre serveur.
Créons un fichier server.js qui contiendra toute la logique permettant de renvoyer "Hello world".
Ensuite, ajoutons le code suivant :
const net = require("net");
// Port de la socket pour le serveur
const PORT = 5001;
// Création du serveur, la socket ouverte par le client est en paramètre.
const server = net.createServer((socket) => {
console.log("--- Client connecté.");
// Écouter les requêtes RPC du client
socket.on("data", (data) => {
const name = data.toString();
socket.write(`Hello world ${name}!`);
});
socket.on("end", () => {
console.log("--- Client déconnecté.");
});
});
// Démarre le serveur sur le port 5001
server.listen(PORT, () => {
console.log(`Serveur RPC en écoute sur le port ${PORT}`);
});
Créons un fichier client.js qui contiendra toute la logique permettant de recevoir "Hello world".
Ensuite, ajoutons le code suivant :
const net = require("net");
const PORT = 5001;
// Connexion au serveur
const client = net.createConnection(PORT, "localhost", () => {
console.log("--- Connecté au serveur.");
// Requête pour appeler le serveur avec votre nom.
client.write("Me");
});
// Réception des réponses du serveur RPC
client.on("data", (data) => {
const response = data.toString();
console.log(response);
client.end(); // Terminer la connexion après la réponse
});
client.on("end", () => {
console.log("--- Déconnecté du serveur.");
});
Maintenant que les fichiers sont prêts, lancez un terminal avec le serveur :
node server.js
Et un terminal avec le client quand le serveur écoute :
node client.js
Que remarquez-vous ?
Appelez-moi pour que l'on puisse valider ensemble le TP.
Nous allons utiliser un autre moyen qu'une simple string pour communiquer avec le serveur. Ajoutez la logique nécessaire pour que le serveur puisse maintenant réagir à un type de requête en utilisant du JSON :
{
"request": "echo",
"params": {
"text": "This is a test."
}
}
N'hésitez pas à utiliser JSON.parse
et JSON.stringify
pour générer ou parser les messages envoyés par les sockets.
Le serveur doit donc vérifier le type de request, si c'est égal à "echo", répondre le "text" envoyé dans "params" au client.
Appelez-moi pour que l'on puisse valider ensemble.
Créez un serveur dans un dossier "MyIRC" capable d'accepter des connexions multiples via TCP, ceci simule un serveur IRC, nous utiliserons donc le port par défaut qui est 6667 pour ce genre de service. Vous devez gérez les cas suivants : quand un utilisateur se connecte pour la première fois, demandez son pseudo et informer les autres utilisateurs qu'il vient de rejoindre le chat et quand il se déconnecte. Lorsqu'un utilisateur envoie un message, le transmettre à tous les autres utilisateurs connectés.
Utilisez telnet comme client :
telnet localhost
6667
(Utilisez brew install telnet
pour Macos ou Enable-WindowsOptionalFeature -Online -FeatureName TelnetClient
pour Powershell)
Ajoutez la commande /list permettant d'afficher la liste de tous les pseudos des clients actuellement connectés au chat.
Appelez-moi pour que l'on puisse valider ensemble.
Ajoutez la commande /whisper permettant de n'envoyer un message qu'à un utilisateur en particulier, le message doit s'afficher différement des autres.
Par exemple :
Appelez-moi pour que l'on puisse valider ensemble.
/list
# 1. Valentin
# 2. Amélie
/whisper Valentin "Comment ça va ?"
# Affiche chez Valentin :
# [Whisper][Amélie] Comment ça va ?
Ajoutez la logique des canaux de discussions, ajoutez la commande /channels list pour lister les canaux, /channels create pour en créer un, /channels join pour le rejoindre.
Un utilisateur doit être de base dans le canal "global" où tous les utilisateur sont par défaut.
Quand un utilisateur rejoins un canal et envoie un message, seuls ceux dans ce canal sont capables de le voir.
Appelez-moi pour que l'on puisse valider ensemble.
Par mémoire partagée
Par messages
Par signaux
Que cela soit en local, entre processus s'exécutant sur le même hôte, ou à distance, entre processus s'exécutant sur des hôtes distants. Sauf pour les signaux, seulement en local.
Les processus se partagent une zone de mémoire commune dans laquelle ils peuvent lire et/ou écrire
L'accès à une mémoire partagée n'est possible que par deux actions : Read et Write.
Avantages :
- Communications transparentes.
- Limitation des copies mémoire.
Inconvénients :
- Problème si deux écritures simultanées (ordre d’ordonnancement, atomicité des opérations).
- Les processus P1 et P2 doivent se synchroniser pour accéder au tampon partagé (verrou, sémaphore, …).
En local
Les deux processus s'exécutent sur la même machine donc peuvent se partager une partie de leur espace d'adressage.
Exemple : les threads s'exécutent dans le contexte d'un même processus.
A distance
La mémoire partagée est physiquement répartie, le gestionnaire de mémoire virtuelle permet de regrouper les différents morceaux selon un seul espace d'adressage.
Attention : problème de cohérence mémoire...
Communications locales de type mémoire partagée.
Le canal de communication est unidirectionnel (pas de problème de synchronisation), communications entre 2 processus uniquement : l'un écrit dans le tube, l'autre lit.
Exemple : cat package.json | wc -l
Dans cette situation, cat peut seulement écrire et wc peut seulement lire.
Les processus n'ont pas accès à des "variables" communes et ne communiquent qu'en s'envoyant des messages.
Ils utilisent deux primitives : send() et recv().
Des zones de mémoire locales à chaque processus permettent l'envoi et la réception des messages et l'émetteur/récepteur doit pouvoir désigner le récepteur/émetteur distant. (adresse)
Ces opérations peuvent être bloquantes ou non-bloquantes.
Une opération bloquante bloque le processus jusqu'à ce qu'elle se termine.
Cela permet de simplifier les communications entre processus et éviter de l'asynchrone.
Une opération non-bloquante permet au processus de continuer son execution en attendant que des données soient émises ou reçues.
Attente active - appels réguliers à la primitive jusqu'à complétion.
Attente passive - le système informe le processus par un moyen quelconque de la complétion de l'opération (signaux par exemple)
Mécanisme de communications locales interprocessus (ou depuis le noyau vers un processus) permettant de notifier un événement.
Le déclenchement d'un signal interromps le logicielle quand l'événement se produit.
Pour utiliser les signaux, le processus doit :
- Indiquer les signaux qu'il souhaite capter (provoquant son interruption)
- Mettre en place un handler (fonction particulière) qui sera exécuté quand l'événement se produira.
Exemple : CTRL+C dans un terminal envoie le signal SIGINT qui permet au terminal d'arrêter la commande en cours.
Deux processus communiquent en émettant et recevant des données via les sockets, pour faire du passage de messages, il est nécessaire de désigner l'autre extrémité de la communication.
Deux types de désignations :
Explicite - Designation du ou des processus destinataire(s)/émetteurs.
Implicite - recevoir un message de n'importe qui, émettre un message à n'importe qui (diffusion) ou une phase d'établissement de connexion désigne les deux entités communicantes.
Une socket est une connexion entre deux processus et est représenté par un fichier virtuel avec les opérations d'ouverture, fermeture, écriture, lecture, …
En bas niveau, on peut donc les utiliser comme un descripteur de fichier.
Elles sont des portes d'entrées / sorties vers le réseau (la couche transport).
Il y a trois types de sockets :
Stream socket - TCP
Datagram socket - UDP
Raw socket - IP
Une socket est identifiée par une adresse de transport qui permet d'identifier les processus de l'application concernée.
Format de l'adresse :
<Adresse IP>:<Numéro de port>
La première identifie l'hôte dans le réseau, l'autre l'application lancée au sein de l'hôte.
Pour les ports, on évite d'utiliser ceux en dessous de 1024 qui sont souvent réservés par les "well-knowns ports".
Règles pour la connexion
Un serveur qui offre le même service en mode connecté et non connecté.
Exemple : DAYTIME port 13 sur UDP et sur TCP qui permet de lire la date et l'heure sur le serveu
Le serveur écoute sur 2 sockets distinctes pour rendre le même service.
Certains systèmes ferment tout accès à UDP pour des raisons de sécurité (pare-feu).
Non duplication des ressources associées au service (corps du serveur).
Un serveur qui répond à plusieurs services (une socket par service).
Pourquoi un serveur multi-services ?
Problème lié à la multiplication des serveurs : le nombre de processus nécessaires et les ressources consommées qui y sont associées.
Avantages
L'invocation d'un service Internet standard (FTP, TELNET, RLOGIN, SSH, …) nécessite la présence côté serveur d'un processus serveur qui tourne en permanence et qui est en attente des requêtes clientes.
A priori, il faudrait un démon par service.
Problème : multiplication des services == multiplication du nombre de démons. Sous UNIX, un super-démon : inetd.
# /etc/inetd.conf
# Internet services syntax :
# <service_name> <socket_type> <proto> <flags> <user> <server_pathname> <args>
# wait : pour un service donné, un seul serveur peut exister à un instant donné
# donc le serveur traite l'ensemble des requêtes à ce service
# stream --> nowait : un serveur par connexion
ftp stream tcp nowait root /etc/ftpd ftpd -l
tftp dgram udp wait root /etc/tftpd tftpd
shell stream tcp nowait root /etc/rshd rshd
Analogie avec un appel de fonction
La fonction ou procédure ne rend la main au programme appelant qu'une fois le traitement (calcul) terminé.
RPC - Remote Procedure Call
Permettre à un processus de faire exécuter une fonction par un autre processus se trouvant sur une machine distante, cela se traduit par l'envoi d'un message contenant l'identification de la fonction et les paramètres, une fois le traitement terminé, un message retourne le résultat de la fonction à l'appelant.
Rapide et utilise des ressources distantes côté client comme l'appel d'une fonction classique.
L'application n'a pas à manipuler directement les sockets ni gérer la communication entre les deux.
L'implémentation des RPC est indépendante de l'OS.
La procédure distante n'a pas accès aux variables du client, aux périphériques d'E/S (affichage d'une erreur !)
Un appel de procédure est synchrone, l'instruction suivante doit attendre que l'appel soit terminé.
Impossible de passer des références ou pointeurs via la procédure.
Définition de service très simple utilisant Protocol Buffers pour optimiser les communications.
Facilement déployable en env de Dev ou Production avec des millions de RPCs par seconde.
Streaming bidirectionnel et authentification entièrement intégrée avec le transport basé sur HTTP/2
Mécanisme extensible de Google, indépendant du langage / la plateforme, pour la sérialisation des données.
Pensez à XML, mais en plus petit, plus rapide et plus simple. Ultra performant car la donnée est sérialisée.
Génère automatiquement une librairie pour envoyer et recevoir vos données.
syntax = "proto3";
package todo;
// Définition du service
service TodoService {
rpc AddTask (Task) returns (AddTaskResponse);
rpc GetTasks (Empty) returns (TaskList);
}
// Messages utilisés par le service
message Task {
string id = 1;
string description = 2;
}
message AddTaskResponse {
string message = 1;
}
message TaskList {
repeated Task tasks = 1;
}
message Empty {}
Créez le dossier "TP-Signaux" et ajoutez le fichier "index.js" à l'intérieur. Ce fichier va contenir la logique pour détecter un signal.
// Fonction générique pour gérer les signaux
async function handleSignal(signal) {
console.log(`Signal ${signal} reçu.`);
}
// Ecoute du signal SIGINT.
process.on("SIGINT", () => handleSignal("SIGINT"));
// Simulation d'une application qui reste active
console.log("Application en cours d'exécution.");
console.log(
"Appuyez sur CTRL+C pour envoyer un signal."
);
// Execute la fonction toutes les 5 secondes.
setInterval(() => {
console.log("Le processus est toujours actif...");
}, 5000);
Lancez le script et essayez maintenant de réaliser un CTRL+C. Appelez-moi pour que l'on puisse valider ensemble.
Nous voulons que notre solution puisses nettoyer avant de s'arrêter après un CTRL+C, il faut donc écrire dans le terminal "Nettoyage en cours..." et arrêtez le processus au bout de 5 secondes.
Ajoutez la logique grâce à setTimeout et process.exit(0) pour arrêter le processus correctement au bout d'un certains temps.
Appelez-moi pour que l'on puisse valider ensemble.
Nous voulons gérer aussi le signal SIGTERM qui est lancé par la commande kill -15 <PID> sur Linux, ce signal est envoyé quand un processus est tué par le système.
Ajoutez le même comportement pour SIGTERM.
Appelez-moi pour que l'on puisse valider ensemble.
ATTENTION cet exercice ne fonctionne pas sur Windows, vous pouvez passer à la suite.
Par soucis de sécurité, nous voulons que notre solution ne puisses pas être arrêter dans des phases importantes de son fonctionnement.
Réaliser un interval où toutes les 5 secondes, la solution passe d'un état où elle peut être arrêté via un signal à un état où il n'est pas possible de l'arrêter.
Ecrivez dans le terminal que l'arrêt est impossible pour le moment quand l'utilisateur tente de l'arrêter.
Appelez-moi pour que l'on puisse valider ensemble.
Dupliquer le dossier "MyIRC" et nommez-le "MyIRC-Signal".
Ajoutez la logique de gestion des signaux à votre MyIRC, quand l'utilisateur tente de stopper le serveur via un signal, envoyez à tous les clients connectés que le serveur va fermer dans 5 secondes et arrêter le serveur au bout des 5 secondes.
Appelez-moi pour que l'on puisse valider ensemble.
Nous allons créer un service gRPC avec Node.js permettant de gérer une liste de tâches (to-do list).
Créez le dossier "TP-RPC" et initialisez le projet nodejs :
Nous allons maintenant ajouter la logique de Protocol Buffers.
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
Définissez le fichier Protocol Buffers et nommez-le "todo.proto" :
syntax = "proto3";
package todo;
// Définition du service
service TodoService {
rpc AddTask (Task) returns (AddTaskResponse);
rpc GetTasks (Empty) returns (TaskList);
}
// Messages utilisés par le service
message Task {
string id = 1;
string description = 2;
}
message AddTaskResponse {
string message = 1;
}
message TaskList {
repeated Task tasks = 1;
}
message Empty {}
Créez le fichier "server.js" suivant :
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const PROTO_PATH = './todo.proto';
// Chargement du fichier .proto
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const todoProto = grpc.loadPackageDefinition(packageDefinition).todo;
// Liste des tâches en mémoire
const tasks = [];
// Implémentation des méthodes du service
const addTask = (call, callback) => {
const task = call.request;
tasks.push(task);
callback(null, { message: 'Task added successfully!' });
};
const getTasks = (call, callback) => {
callback(null, { tasks });
};
// Démarrage du serveur
const server = new grpc.Server();
server.addService(todoProto.TodoService.service, { addTask, getTasks });
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('Server running on http://0.0.0.0:50051');
server.start();
});
Créez le fichier "client.js" suivant :
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const PROTO_PATH = './todo.proto';
// Chargement du fichier .proto
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const todoProto = grpc.loadPackageDefinition(packageDefinition).todo;
// Création du client
const client = new todoProto.TodoService('localhost:50051', grpc.credentials.createInsecure());
// Ajouter une tâche
client.AddTask({ id: '1', description: 'Learn gRPC' }, (err, response) => {
if (err) console.error(err);
else console.log(response.message);
// Récupérer les tâches
client.GetTasks({}, (err, response) => {
if (err) console.error(err);
else console.log('Tasks:', response.tasks);
});
});
Lancez votre serveur puis lancez votre client.
Que remarquez-vous ?
Appelez-moi pour que l'on puisse valider ensemble.
Intégrez une API tierce dans votre serveur gRPC.
Méthode :
Pour cela nous allons utiliser la librairie Axios pour réaliser une requête HTTP auprès de l'API.
Connectez votre service gRPC à une base de données MongoDB.
Cas d’usage :
Stockez et récupérez des informations sur une liste de produits.
Créez des méthodes pour ajouter, modifier et supprimer des produits.
Utilisez Docker pour lancer facilement une base de donnée en local :
docker run --name mongodb -p 27017:27017 -d mongo
Utiliser la libraire NodeJS MongoDB pour utiliser la base de donnée.
Appelez-moi pour que l'on puisse valider ensemble.
Créer un service gRPC qui utilise TLS pour dialoguer en sécurité avec son client. Pour cela, nous allons générer les certificats SSL nous-même à la racine du dossier de l'exercice :
Vous obtiendrez alors les fichiers server.crt et server.key dans le dossier certs. Nous utiliserons alors la méthode createSsl au lieu de createInsecure où serverCert et serverKey sont le contenu des fichiers dans certs.
Appelez-moi pour que l'on puisse valider ensemble.
mkdir certs
cd certs
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"
openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 365
grpc.ServerCredentials.createSsl(null, [{
cert_chain: serverCert,
private_key: serverKey,
}], true);
Ajoutons dans notre exercice 2 avec la logique de gestion de produit notre gestion d'erreur.
Nous allons donc gérer le cas où nous réalisons un getProduct pour récupérer un produit avec un id mais où l'id n'existe pas.
Renvoyez une erreur INVALID_ARGUMENT au client et intégrez la gestion d'erreur côté client pour afficher un message d'erreur explicite.
Appelez-moi pour que l'on puisse valider ensemble.
Créer un service gRPC qui permet à un client de streamer une vidéo en fragments depuis un serveur.
Configuration du projet Node.js avec gRPC.
Définir un fichier .proto
décrivant le service de streaming vidéo.
Implémenter un serveur gRPC capable d'envoyer des fragments vidéo en continu.
Créer un client gRPC pour recevoir et stocker dans un fichier les fragments.
Pour cela nous allons utiliser le concept de Stream en NodeJS, inspirez vous du tutoriel de gRPC pour réaliser la logique de stream avec gRPC.
Appelez-moi pour que l'on puisse valider ensemble.
Créer un service gRPC qui permet aux clients d'écouter un stream de notifications pour réagir dès qu'un client crée une nouvelle notification.
Cela ressemble au fonctionnement de notre MyIRC que nous avions réalisé via les Sockets où un client doit pouvoir envoyer un message et le reste des clients vont le recevoir.
N'hésitez pas à vous baser sur ce que vous avez fait lors du Stream de la vidéo.
Appelez-moi pour que l'on puisse valider ensemble.
Maintenant que notre service de notification est prêt, ajoutons une interface graphique à celui-ci pour découvrir comment utiliser gRPC directement dans une application web.
Pour cela nous allons utiliser la librairie grpc-web pour nous connecter à notre service depuis le frontend pour afficher les notifications.
Suivez la documentation de la librairie pour générer les services.
Choix des technologies libre mais je vous conseille de passer par Vite pour générer le projet de base.
Pour utiliser notre service gRPC, nous allons utiliser Envoy via Docker qui permet de réaliser un Proxy, configuration sur la slide suivante.
// envoy.yaml
static_resources:
listeners:
- address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
config:
codec_type: AUTO
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: example_service
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
clusters:
- name: example_service
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: example_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 50051 }
Appelez-moi pour que l'on puisse valider ensemble.
Application permettant à un utilisateur de se connecter (prendre partiellement le contrôle) sur un ordinateur distant (à partir d'un terminal local).
Pour cela, l'utilisateur doit disposer d'un accès autorisé à la machine. On peut executer alors des commandes saisies localement au clavier sur une machine distante.
Par exemple, je peux me connecter depuis Windows à un serveur sous Linux et executer des commandes Linux.
telnet - le standard (existe sur de nombreuses plate-formes).
rlogin - uniquement entre machines unix.
ssh - sécurisé (authentification + cryptage), peut transporter le DISPLAY.
Aujourd'hui, on utilise essentiellement SSH pour la connexion à des serveurs distants ou pour la connexion avec son gestionnaire de dépôt Git.
Un des premiers standard de l'Internet : RFC 854,855 (1983).
Connexion TCP sur le port 23 côté serveur avec une authentification sur le shell distant (mot de passe en clair).
Quand un caractère est tapé au clavier, il est envoyé au serveur qui renvoie un "écho" du caractère ce qui provoque son affichage dans le terminal local.
Prise en compte de l'hétérogénéité : mécanisme de négociation d'options à la connexion (codage des caractères ASCII sur 7 ou 8 bits ?)
Exemple : telnet d'une machine Windows vers une machine Unix, tous les caractères ASCII n'ont pas la même signification
// Par le nom de la machine distante (DNS+port 23)
telnet google.com
// Par l'adresse IP de la machine distante (port 23)
telnet 168.192.0.5
// Accès à un autre service (connexion sur un autre port)
telnet 168.192.0.5 80
// Exemple
telnet google.com 80
Trying 142.250.75.238...
Connected to google.com.
Escape character is '^]'.
$
HTTP/1.0 400 Bad Request
Content-Type: text/html; charset=UTF-8
Referrer-Policy: no-referrer
Content-Length: 1555
Date: Thu, 09 Jan 2025 10:50:38 GMT
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 400 (Bad Request)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>400.</b> <ins>That’s an error.</ins>
<p>Your client has issued a malformed or illegal request. <ins>That’s all we know.</ins>
Connection closed by foreign host.
telnet
telnet> ?
Commands may be abbreviated. Commands are:
close close current connection
logout forcibly logout remote user and close the connection
display display operating parameters
mode try to enter line or character mode ('mode ?' for more)
telnet connect to a site
open connect to a site
quit exit telnet
send transmit special characters ('send ?' for more)
set set operating parameters ('set ?' for more)
unset unset operating parameters ('unset ?' for more)
status print status information
toggle toggle operating parameters ('toggle ?' for more)
slc change state of special charaters ('slc ?' for more)
auth turn on (off) authentication ('auth ?' for more)
z suspend telnet
! invoke a subshell
environ change environment variables ('environ ?' for more)
? print help information
telnet>
Un terminal "virtuel réseau" qui permet de supporter des environnements hétérogènes réalise la conversion des caractères spéciaux ou des séquences particulières en un format NVT.
Le format NVT
Tous les caractères sont codés sur 8 bits, les 128 caractères ASCII sont transmis tels quels.
NVT redéfinit la signification de certains caractères de commande ASCII : CR (13d) = retour au début de la ligne, LF (10d) = déplacement d'une ligne vers le bas.
Dans le format NVT, RETURN ou ENTER se traduit par CR-LF
SSH pour Secure SHell est un protocol permettant l'ouverture d'un shell à distance de manière sécurisée.
Points forts
Pour la connexion, sur le port 22 en TCP côté serveur.
Exemple : Vous pouvez lier une clé SSH à votre compte Github pour pouvoir gérer beaucoup plus facilement les accès à votre dépôt.
La qualité de la sécurité dépend :
Symétrique - même clé privée secrète partagée utilisée pour le chiffrement et le déchiffrement, l'émetteur et le récepteur doivent se mettre d'accord sur la clé à utiliser.
Exemple : AES, DES
Asymétrique - utilisation d'une clé publique pour le chiffrement et d'une clé privée. Pour qu'un émetteur envoie un message chiffré, il suffit qu'il connaisse la clé publique du destinataire
Comment être sûr que la clé publique est bien celle du destinataire escompté ? On utilise alors des certificats : association d'une clé publique et d'un nom de destinataire signée par un tiers de confiance.
Exemple : RSA, SSL, TLS
Algorithme asymétrique pour l'authentification : généralement RSA basé sur l'arithmétique modulo.
Algorithme symétrique pour les communications : utilisation de RSA pour échanger la clé de l'algorithme symétrique pour un chiffrement et déchiffrement moins coûteux.
# Se connecter à un serveur
ssh -l user hostname
ssh user@hostname
# Éxecute une commande directement à distance
ssh -l user hostname cmd
ssh user@hostname cmd
# Copie de fichiers à distance
scp file1 file2 user@hostname:
# Copie un dossier
scp -r dir user@hostname:/tmp
Nous allons créer un serveur utilisant SSH grâce à Docker.
Pour cela, nous utiliserons l'image linuxserver/openssh-server.
Nous allons d'abord créer votre paire de clé via l'image :
docker run --rm -it --entrypoint /keygen.sh linuxserver/openssh-server
Choississez RSA et 4096 comme options.
Vous allez voir deux clés dans le terminal, une privée et une publique. Nous allons les stocker dans le dossier dédié et nous allons créer un sous-dossier "test" soit dans "~/.ssh/test/" avec pour nom "id_rsa" et "id_rsa.pub" pour la privée et la publique.
Sur Windows, vos clés seront dans le dossier C:\Users\TonNom\.ssh\test\
Maintenant que nous avons généré notre paire de clé, nous allons lancer le serveur en lui donnant accès à notre clé publique.
Lancez la commande suivante :
docker run -d --name openssh-server -e USER_NAME=user -e PUBLIC_KEY="$(cat ~/.ssh/test/id_rsa.pub)" -p 2222:2222 linuxserver/openssh-server
Attention, ici nous utiliserons le port 2222 pour le port SSH.
Le serveur est maintenant prêt pour la suite du TP.
Avant de nous connecter, nous devons ajouter les bonnes permissions à notre clé privée, lancez-la commande :
chmod 600 ~/.ssh/test/id_rsa
Ou pour Windows :
icacls C:\Users\VotreUtilisateur\.ssh\test\id_rsa /inheritance:r /grant VotreUtilisateur:F
Maintenant que le serveur est lancé, nous allons nous connecter :
ssh -i ~/.ssh/test/id_rsa user@localhost -p 2222
Vérifiez que vous êtes bien sur le serveur en réalisant des commandes de bases.
Appelez-moi pour que l'on puisse valider ensemble.
Comme vu ensemble durant le cours, il est possible d'exécuter une commande ou même d'envoyer un fichier sur le serveur.
Appelez-moi pour que l'on puisse valider ensemble.
Copie intégrale d'un fichier d'un système de fichiers vers un autre en environnement hétérogène.
L'hétérogénéité concernant les fichiers est dépendante d'un système à l'autre :
Plusieurs protocoles
Type client/serveur
le client interagit avec l'utilisateur, le système de fichiers local et les protocoles réseau.
le serveur interagit avec les protocoles réseau et le système de fichiers du serveur.
Ne pas confondre avec les protocoles d'accès aux fichiers distants comme NFS (RPC), SMB (Microsoft)
Standard TCP/IP pour le transfert de fichiers : connexion TCP sur le port 21 côté serveur.
Contrôle d'accès au serveur distant en utilisant un login et un mot de passe, attention le mot de passe circule en clair.
Particularité de FTP par rapport à TELNET, etc :
RETR <filename>
Déclenche la transmission par le serveur du fichier <filename> sur le canal de données.
STOR <filename>
Déclenche la réception d'un fichier qui sera enregistré sur le disque sous le nom <filename>. Si un fichier existe déjà, il sera remplacé.
APPE <filename>
Déclenche la réception d'un fichier qui sera enregistré sur le disque sous le nom <filename>. Si un fichier existe déjà, les données seront ajoutées au fichier.
ABOR - Abandon d'un transfert en cours.
PWD - Impression du répertoire actuel.
LIST - Liste tous les fichiers et dossiers du répertoire courant.
NLIST - Même que LIST en version courte.
CWD <name> - Change de dossier.
MKD <name> - Création d'un nouveau dossier.
RMD <name> - Suppression d'un dossier.
DELE <filename> - Suppression d'un fichier.
RNFR / RNTO - Renomme un fichier.
STAT - Status courant de la session FTP.
HELP - Affiche l'aide sur les opérations possibles.
Status | Signification |
---|---|
1xx | Réponse positive préliminaire (une autre réponse suivra) |
2xx | Réponse positive finale (une autre requête est possible) |
3xx | Réponse positive intermédiaire (une autre requête doit suivre) |
4xx | Réponse négative temporaire (la même requête peut réussir plus tard) |
5xx | Réponse négative définitive (la requête n’est pas acceptée) |
TFTP (Trivial File Transfer Protocol) : Version simplifiée de FTP, sans authentification ni cryptage. Il fonctionne sur un seul port (69) et est souvent utilisé pour des transferts simples sur des réseaux locaux.
SFTP (SSH File Transfer Protocol) : Variante sécurisée de FTP qui fonctionne sur SSH (port 22). Il chiffre les données et les identifiants, assurant ainsi la confidentialité et l'intégrité des transferts.
Différences avec le transfert de fichiers
NFS (Network File System) : Protocole de partage de fichiers principalement utilisé dans les environnements Unix/Linux. Il permet de monter des systèmes de fichiers distants et d'accéder aux fichiers comme s'ils étaient locaux, avec une gestion fine des permissions.
SMB (Server Message Block) : Protocole de partage de fichiers principalement utilisé dans les environnements Windows. Il permet l'accès, la gestion et le partage de fichiers et imprimantes entre ordinateurs sur un réseau local.
Nous allons créer un serveur utilisant FTP grâce à Docker.
Pour cela, nous utiliserons l'image delfer/alpine-ftp-server.
Nous allons d'abord télécharger un client FTP comme FileZila.
Ensuite, lancez la commande Docker :
docker run -d -p 21:21 -p 21000-21010:21000-21010 -e USERS="user|password" -e PASV_ADDRESS=127.0.0.1 delfer/alpine-ftp-server
Cela va lancer le serveur FTP.
Lancez votre client FileZila et ajouter les informations suivantes pour vous connecter en haut du logiciel :
Hôte : localhost
Nom d'utilisateur : user
Mot de passe : password
Et appuyez sur Connexion rapide.
Lisez bien les logs juste en dessous et retrouvez les commandes réalisés par votre client FTP avec le serveur FTP.
Testez d'ajouter des fichiers ou des dossiers.
Appelez-moi pour que l'on puisse valider ensemble.
NIS - Network Information System
Introduit par SUN en 1985 (Yellow Pages (yp) à l'origine), n'est pas un standard de l'Internet mais est largement utilisé.
C'est une base de données distribuée qui permet le partage d'informations système (/etc/passwd, /etc/hosts, …).
Objectif
Exemple : il suffit de créer un nouvel utilisateur sur le serveur NIS pour que chaque machine client NIS ait accès aux informations de login de cet utilisateur.
Comment relier les adresses IP utilisées pour acheminer les paquets aux noms utilisés par les utilisateurs ou les applications ?
Pour répondre à ce problème, on a créé DNS - Domain Name System, un annuaire distribué des adresses de l'Internet :
Permet la traduction d'adresses mais aussi d'autres services :
Une requête DNS peut impliquer plusieurs serveurs de noms répartis dans le monde entier.
Tolérance aux pannes : si le serveur DNS tombe, tout l'Internet aussi !
La réponse soit la plus proche possible du demandeur
Trop de correspondances à gérer, nombre de requêtes au serveur trop importante.
Serveurs de noms locaux à qui s'adressent les requêtes locales ; en charge de la résolution.
Serveurs de noms racine qui sont censés savoir comment s'approcher de la réponse.
Serveurs de noms de source autorisée qui contiennent les correspondances "officielles".
Un domaine est un sous-arbre entier de l’espace de nommage.
Domaine complet
Domaine .fr
Domaine epsi.fr
Hôte ou noeud : ssh.epsi.fr
Une zone DNS est une portion d’un espace de noms de domaine gérée par une entité spécifique. Elle contient les enregistrements DNS (comme A, MX, CNAME, TXT) nécessaires pour traduire les noms de domaine en adresses IP et diriger le trafic réseau correctement.
Exemple :
La zone DNS de example.com peut inclure :
www.example.com
à une adresse IP.mail.example.com
).Chaque zone est gérée par un serveur DNS autoritaire, qui stocke et fournit ces enregistrements aux requêtes DNS.
Les enregistrements DNS sont des entrées dans une zone DNS qui définissent comment un domaine est résolu et dirigé sur Internet. Chaque type d’enregistrement a un rôle spécifique.
Clé | Type | Valeur |
---|---|---|
ssh.epsi.fr. | CNAME | backoffice.epsi.fr. |
epsi.fr. | NS | ns.ovh.com. |
epsi.fr | NS | ns2.ovh.com |
epsi.fr. | A | 140.77.1.32 |
epsi.fr. | MX | 20 mx.ovh.com. |
epsi.fr. | MX | 100 mx2.ovh.com. |
backoffice.epsi.fr. | A | 140.77.1.67 |
Permet de centraliser la réception des messages sur une machine qui a un système plus robuste :
Les MX permettent ensuite de répartir la charge sur différents serveurs de mail et de disposer de serveurs de secours, par exemple, en cas de saturation, le serveur SMTP peut aiguiller les messages via un autre serveur SMTP interne.
Ex : 20 mx.ovh.com. permet d'être plus prioritaire que 100 mx2.ovh.com.
Un annuaire fédérateur LDAP - Lightweight Directory Access Protocol est un système de gestion et d'organisation d'informations, principalement utilisé pour centraliser des données telles que les utilisateurs, groupes, permissions et autres ressources dans un réseau.
Dans une entreprise, un annuaire LDAP fédérateur centralise les informations d'authentification (utilisateurs, groupes, permissions) provenant de plusieurs systèmes (messagerie, fichiers, intranet). Ainsi, un utilisateur n'a besoin que d'un seul identifiant et mot de passe pour accéder à tous les services, simplifiant l'administration et améliorant la sécurité.
L'administration de réseaux consiste à gérer, configurer, surveiller et maintenir les équipements d'un réseau informatique (comme les routeurs, commutateurs, serveurs) afin d'assurer leur bon fonctionnement et leur sécurité.
SNMP - Simple Network Management Protocol est un protocole utilisé pour surveiller et gérer les équipements réseau. Il permet de récupérer des informations sur leur état (comme la charge CPU, l'utilisation de la bande passante) et d'effectuer des actions de configuration à distance.
Un administrateur réseau utilise SNMP pour surveiller la bande passante de son routeur. Grâce à des outils de gestion SNMP (comme Zabbix ou Nagios), il peut détecter une utilisation excessive de la bande passante, recevoir des alertes, et intervenir rapidement pour optimiser les performances du réseau.
Les protocoles de courrier électronique permettent l’envoi, la réception et la gestion des e-mails entre les serveurs et les clients de messagerie.
Principaux types de protocoles :
Nous verrons ça ensemble durant le module Services Web.
Nous voulons réaliser un script capable de synthétiser une grande quantité de texte en quelques phrases.
Créez un nouveau dossier "AI" puis à l'intérieur un dossier "TP".
Dans le dossier "TP" lancez la commande :
npm init
Puis nous allons ajouter le paquet dotenv pour utiliser des fichiers .env pour sauvegarder nos secrets :
npm i dotenv
Créez un fichier ".env" vide dans le dossier "TP".
Ensuite, ajoutez le code de la slide suivante dans un fichier "index.js".
require("dotenv").config();
async function queryModelWithFetch(inputText) {
const MODEL_NAME = "facebook/bart-large-cnn";
try {
const response = await fetch(
`https://api-inference.huggingface.co/models/${MODEL_NAME}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HF_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: inputText,
}),
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error("Error:", error);
throw error;
}
}
async function main() {
try {
// Using Fetch
const response = await queryModelWithFetch(
"The tower is 324 metres (1,063 ft) tall, about the same height as an 81-storey building, and the tallest structure in Paris. Its base is square, measuring 125 metres (410 ft) on each side. During its construction, the Eiffel Tower surpassed the Washington Monument to become the tallest man-made structure in the world, a title it held for 41 years until the Chrysler Building in New York City was finished in 1930. It was the first structure to reach a height of 300 metres. Due to the addition of a broadcasting aerial at the top of the tower in 1957, it is now taller than the Chrysler Building by 5.2 metres (17 ft). Excluding transmitters, the Eiffel Tower is the second tallest free-standing structure in France after the Millau Viaduct."
);
console.log("Response:", response);
} catch (error) {
console.error("Main Error:", error);
}
}
main();
Créez ensuite un compte sur Hugging Face pour pouvoir ensuite créer un token qui nous permettra de réaliser des requêtes sur certaines modèles. Lien vers la création d'un token.
Vous ne devez cocher que la case "Make calls to the serverless Inference providers". Une fois créé, ajouter la variable d'environnement "HF_API_TOKEN" avec la valeur du token.
Ensuite, ajoutez dans votre package.json, le script "start" qui aura pour valeur "node index.js".
Lancez la commande npm start.
Appelez-moi pour que l'on puisse vérifier ensemble.
Changer le modèle utilisé par notre script NodeJS par samLowe/roberta-base-go_emotions et relancer le script.
Que remarquez-vous ?
Maintenant, écrivez un message représentant une certaine émotion, relancez le script.
Que remarquez-vous ?
Appelez-moi pour que l'on puisse vérifier ensemble.
Changer le modèle utilisé par notre script NodeJS par deepseek-ai/DeepSeek-R1-Distill-Qwen-32B et l'input par "Tell me a joke please.", relancez le script.
Que remarquez-vous ?
Essayez de l'utiliser comme vous feriez avec ChatGPT.
Que remarquez-vous ?
Appelez-moi pour que l'on puisse vérifier ensemble.
Je vais noter votre investissement durant nos séances.
Pour rendre votre travail, créez un dépôt sur Github ou Gitlab et ajoutez-moi en droit de lecture pour ValentinMontagne (Github) ou vm-marvelab (Gitlab) avec tous vos exercices et TPs.
Ajoutez dans un README votre nom / prénom pour que je puisse vous noter correctement.
N'oubliez pas le .gitignore pour éviter d'ajouter les node_modules.