Les bases du réseau — 2. Un support de communication

ℹ️ Information
Cet article fait partie de la série Les bases du réseau. Retrouvez le sommaire à cette adresse.

Maintenant que l’on a compris de quoi et composée une communication en vrai, voyons comment cela se décline en informatique. On va partir du cas le plus simple : une machine veut communiquer avec une autre machine. Souvent, on parlera du modèle « Client - Serveur » : un « client » (typiquement un PC) va faire une requête au serveur, et ce dernier lui répondra. La communication est donc initiée par le client. Ce n’est pas toujours le cas, mais c’est le plus répandu.

On a l’émetteur, le récepteur et le message, on a maintenant beoin d’un canal, et de règles pour rendre tout cela possible.

Produire un message : comment on fait ?

Le message

En fait ce ne sont pas les machines qui veulent communiquer, mais des applications qui tournent sur ces machines. Mon PC n’a rien à dire au serveur qui fait tourner mon blog. Par contre, mon navigateur web, qui tourne sur mon PC, a quelque chose à dire au service web qui tourne sur mon serveur. Il est important de ne pas faire la confusion.

Communcation simple à 2
Figure 1 — Communication simple à 2

Ici, on a une application ou « source » quelconque, qui souhaite émettre des données à son « homologue » : une application « destinataire ». Celle-ci va recevoir les données (et éventuellement répondre, auquel cas on inverse simplement les rôles). Typiquement, un message est une ressource (image, page web, vidéo, …), ou bien une requête d’accès à cette ressource.

Le protocole

Une fois que l’on a un message, il faut le rendre transférable sur un réseau : il faut pour cela l’organiser en séquence binaire. On parle parfois de sérialisation. On part de données qui sont dispersées dans la RAM dans un format qui dépend de l’application (un objet JSON par exemple), et on arrive à une suite d’octets organisés selon une certaine logique.

De quelles règles a-t-on besoin ? On l’a vu juste avant : d’un langage commun — en informatique on appelle cela un protocole. Un protocole est constitué de données (par exemple une image) qui auront été encodées, et le plus souvent de métadonnées les accompagnant qui permettent aux interlocuteurs de les comprendre, de les vérifier, ou de savoir quoi faire avec. Par exemple si on envoie une image via un protocole de transfert de fichier, ça peut être bien d’indiquer le type d’opération (demander un fichier ou envoyer un fichier), d’ajouter la taille de l’image, son encodage, ou bien un simple numéro qui fait correspondre une requête à une réponse. Ou encore, une instruction : on envoie l’image, et on demande de la copier dans un répertoire ; ou tout simplement : lister le répertoire en cours.

On peut aussi prendre comme exemple un message de chat : il contient le corps du message, mais aussi le pseudo de son auteur, la date d’envoi, etc., le tout suivant un certain format.

ℹ️ C'est quoi l'encodage ?
L’encodage est la façon de représenter une information en binaire : pour du texte on a l’ASCII historiquement, et plus souvent aujourd’hui l’UTF-8. C’est simplement un code qui dit : la lettre A s’écrit 01000001, la lettre B s’écrit 01000010, etc. Idem pour une image ou n’importe quel média, il existe plusieurs façons de les écrire en binaire selon leur encodage (png, jpeg, etc.).

Un protocole est donc la langue (vocabulaire, syntaxe, …) que l’on va utiliser pour échanger des messages, et les métadonnées autour de ces messages ; il doit être compris par les deux partis. Certains protocoles sont standards : tout le monde peut avoir accès à leurs spécifications et donc les implémenter — d’autres sont privés, dits « propriétaires ».

Le format

Souvent, on commence par écrire les métadonnées (en fait on appelle ça un en-tête ou header), puis les données. Il faut retenir que les données sont structurées selon un schéma précis connu à l’avance. On peut inventer n’importe quel protocole applicatif et n’importe quel encodage pour représenter n’importe quel type d’information, tant que le destinataire sait interpréter ce qu’envoie l’émetteur.

Pour illustrer cela, on peut imaginer un protocole de transfert de fichier fictif qui pourrait ressembler à cela :

----------------------------------
| Opération | Séquence | Données |
----------------------------------

J’ai représenté chaque champ :

  • Opération peut être : demander un fichier ou envoyer un fichier ; peut valoir 0 pour une demande et 1 pour un envoi.
  • Séquence sert à identifier le numéro de la demande : le client indique un numéro, le serveur le recopie dans la réponse, pour que le client sache faire le lien avec sa requête, s’il en fait plusieurs. C’est une technique fréquente.
  • Données est soit le nom du fichier demandé si l’opération est 0, soit le fichier en lui-même si opération = 1.

Pour préciser un peu, le premier champ représente l’en-tête, et donnée le message en tant que tel. On appelle cela la charge utile.

Le champ opération aura toujours la même longueur, un octet suffit (un seul bit en fait), et on peut se dire que Séquence prendra un octet également (256 possibilités) ; mais on a ici une longueur variable pour données, ça peut être utile de la préciser. On peut donc rajouter un champ d’en-tête de taille fixe, disons 4 octets. Pourquoi ? Parce qu’avec 4 octets le nombre maximal que l’on peut représenter est 4 294 967 295. On peut donc imaginer que la limite de taille de fichier de notre protocole est 4,94 Go.

Notre protocole ressemble maintenant à ceci :

  1 o.     1 o.     4 octets                         Variable
------------------------------------------------------------------
| Opé.   | Séq.   | Taille                         | Données ... |
------------------------------------------------------------------
💡 1 octet ?

Pourquoi utiliser un octet entier pour représenter une variable qui tient sur 1 bit ?

Très bonne remarque, c’est vrai, on gâche 7 bits par message, qui vaudront toujours 0. Si on cherche à optimiser, on pourrait profiter de cet octet pour écrire la taille. Par exemple le bit de poids fort pourrait valoir Opération, et les 7 bits de poids faible représenteraient un bout de la taille, le reste étant écrit sur les autres octets réservés à cela. Mais franchement ça pose plusieurs problèmes :

  • C’est un peu plus pénible à implémenter et débugger.
  • C’est peu lisible.
  • Et si on avait besoin de ces bits ?

En effet, on pourrait imaginer un troisième type d’opération, dans ce cas on aurait besoin d’un bit supplémentaire… Et si on a besoin d’indiquer un autre champ sur un bit ? Ca arrive assez souvent, on appelle ça des flags. On pourrait avoir un flag qui vaut 1 lorsque l’on veut dire que le fichier existe déjà par exemple. Il est donc judicieux d’avoir un peu de réserve : pas besoin de redéfinir tout le protocole, on utilise des bits qui sont déjà présents. Cela permet aussi, dans une certaine mesure de la rétrocompatibilité : si une machine implémente la première version de notre protocole, elle ignorera ces bits là et ne regardera que celui qu’elle sait interpréter.

Voici donc à quoi pourrait ressembler un échange :

Requête :
 Opé Séq  T.   Données
--------------------------------------------------
| 0 | 0 | 33 | /home/emile/fichier_important.txt |
--------------------------------------------------

Réponse :
 Opé Séq  T.   Données
-----------------------------------------------------------------------------
| 1 | 0 | 60 | aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj13TmYwUVQ5cjk3SQ== |
-----------------------------------------------------------------------------

Ici, les champs sont représentés en décimal, ASCII et base64 pour plus de lisibilité. En réalité, tout est en binaire comme toujours.

Récap'

On a vu ici ce qu’est un protocole, et grossièrement la façon dont on le formate généralement : en-tête, et données. Le protocole est l’élément atomique des communications réseau, il sert à standardiser la façon dont on veut communiquer dans le cadre d’une application donnée. On parle donc de protocole applicatif. Il existe des protocoles applicatifs de transfert de fichier, de vidéo, de page web, … et il existe des protocoles non applicatifs, dont on verra l’utilité plus tard.

Le canal

Il est de la responsabilité de l’application de construire une séquence binaire que le destinataire comprendra : c’est le protocole. Il est de la responsabilité du canal de transporter cette séquence jusqu’au destinataire. Il va falloir passer à un moment donné par un élément physique, dit autrement faire sortir le message de la machine émettrice. Comment passe-t-on d’une suite binaire « virtuelle » à un signal physique ?

Sur la Figure 1, le canal est matérialisé par le trait rouge. Il faut trouver un moyen de transmettre un 1 d’une certaine manière et un 0 d’une autre manière, sans ambiguité.

Codage et transmission

Pour transmettre le message, on va se baser sur une propriété physique du canal. Le cas le plus simple est son état électrique : on pourrait dire qu’une tension de 0V sur un fil de cuivre équivaut à la valeur binaire 0, et une tension de 5V à 1. L’émetteur n’aura qu’à fermer un circuit pour communiquer un 1, et à l’ouvrir pour un 0, à intervalle de temps régulier. On va juste avoir un problème : si on n’a qu’un fil, on risque de très vite s’embrouiller si les 2 machines parlent en même temps (ça existe cela dit, voir I²C). Alors on peut prendre 2 fils ! Du point de vue de chaque machine, l’un sert à émettre les données (on parle de RX) et l’autre à recevoir (on parle de TX). Evidemment, le RX de l’un arrive sur le TX de l’autre. Souvent on a besoin de boucler le circuit. On ne va donc plus parler de fil mais de paire : 2 fils servent à la communication dans un sens, 2 fils pour l’autre. Pour réduire les interférences, on les torsade (voir ici). Une autre solution est d’utiliser un fil de masse commun, et on a 3 fils au lieu de 4. Vu la faible tension, c’est bien de mettre l’émetteur et le destinataire à équipotentiel pour comparer ce qui est comparable. N’allons pas plus loin là dedans, ce n’est pas un cours d’électronique et je ne suis pas électronicien :)

// image

ℹ️ Quelques exemples

Le fameux câble dit Ethernet contient 4 paires torsadées indépendantes. Suivant la norme Ethernet utilisée, on utilise 2 paires ou plus pour les données. Dans certains cas, les 2 paires en rab peuvent faire passer du courant électrique par exemple (voir PoE).

A l’inverse, le câble utilisé pour les communications série RS-232 utilise un neutre commun et d’autres fils qui servent à d’autres trucs. Dans l’article suivant, nous en implémenterons un très simple avec 3 fils.

Il va aussi falloir se mettre d’accord sur une vitesse de transmission : si le récepteur s’attend à reçevoir 1000 bits par seconde, mais qu’on lui en envoie 1500, ça ne va pas aller. On parle souvent en baud. Un baud est un nombre de “mot” par seconde, un mot n’étant pas forcément un bit. En effet, on peut, en une seule unité de temps, transmettre plusieurs bits en jouant sur plusieurs paramètres physique à la fois et à plusieurs niveaux d’intensité — on appelle cela un mot. On peut définir, par exemple, que -10V = 00, -5V = 01, 5V = 10 et 10V = 11. Dans les communications sans-fil on va produire une onde électromagnétique sinusoïdale et faire varier sa phase et son amplitude. Avec 2 paramètres on peut déjà coder 2 bits d’un coup. Mais si permet plusieurs niveaux pour chaque paramètre, on va pouvoir aller beaucoup plus loin, à condition d’avoir un canal de bonne qualité, car plus on rajoute de niveaux plus on est sensible aux interférences. Pour creuser : voir modulation de signal.

💡 Pour aller plus loin

Souvent, on va complexifier encore un peu la transmission pour être en mesure de détecter voir corriger les erreurs de transmissions : voir le codage Hamming.

Aussi, avec notre méthode 0V = 0 et 5V = 1, si l’on doit transférer plein de 0, la ligne va être à 0V pendant longtemps, et il va être difficile de garder une synchronisation. On peut résoudre cela avec une horloge transmise sur une paire dédiée (comme I²C) ! Ou encore, la transmettre avec le signal, comme avec le codage Manchester, utilisé dans certaines versions d’Ethernet.

Enfin, dans certains cas on préfère envoyer en premier le bit de poids faible, dans d’autres, le bit de poids fort. Voir endianness.

Pour résumer, le canal va avoir la responsabilité de prendre un flux binaire et de l’émettre en jouant sur la modification de l’état physique du support. Si besoin, le flux binaire est modifié (détection/correction d’erreur, composition de plusieurs bits en mots, …).

Le framing

Parfois, on veut transmettre juste un octet voire un seul bit. Parfois, un fichier de 15Go. Si on transmet 2 messages de suite, comment savoir quand s’arrête le premier, et quand commence le second ? En cas d’erreur de transmission lors de l’envoi d’un fichier de 15Go, va-t-on devoir renvoyer les 15Go ? Si j’ai plusieurs applications qui utilisent mon canal et que l’une transfère un gros fichier, dois-je attendre que ce transfert soit terminé avant que l’autre puisse envoyer des données, même minuscules ? Si mon canal n’intègre pas d’horloge (voir plus bas), comment s’assurer que le récepteur ne se décale pas dans son écoute au bout d’un moment ?

Les réponses à ces questions — et bien d’autres — sont adressées par le framing (ou tramage en français ?). L’idée est simplement de découper la données en petits bouts, appelés trames, facile à transmettre indépendamment par le canal. Ainsi on peut mutualiser un canal pour de multiples transmissions parallèles (une trame de l’une, une trame de l’autre). On peut aussi, en cas d’erreur de transmission, renvoyer uniquement la trame en erreur, pas tout le message…

Il y a plusieurs façon de définir ce qu’est une trame : ça peut être un découpage temporel, une quantité de bits fixe, ou bien une quantité de bits arbitraire encadrée par des données additionnelles signalant le début et la fin de la trame…

Il faut bien comprendre que notre message de tout à l’heure va peut être devoir être coupé en 2 (ou plus) avant d’être envoyé sur le canal. Ainsi la requête par exemple, qui fait 39 octets de long, sera peut-être découpée en un premier bloc de 15 octets (l’en-tête et le début du message), un second de 15 octets (le milieu du message) et un dernier de 9 octets (la fin). Comment on reconstitue les deux bouts à réception ? On verra ça plus tard.

Ces deux bouts se verront joints d’autres informations utiles pour le canal : ici il s’agit simplement du numéro de trame, on verra plus tard quelles informations on peut mettre ici. Il faut bien observer que l’en-tête applicative n’apparait qu’une fois : les trames sont découpées en ignorant totalement la structure de ce protocole. Du point de vue de l’algorithme qui découpe le message en trame, ce n’est qu’une suite binaire.

  N°     PDU
----------------------------------------------
|   |  ------------------------------------- |
| 1 |  | 1 | 0 | 60 | aHR0cHM6Ly93d3cueWi  | |
|   |  ------------------------------------- |
----------------------------------------------

-------------------------------------------
|   |  ---------------------------------- |
| 2 |  | 91dHViZS5jb20vd2F0Y2g/dj13TmYw | |
|   |  ---------------------------------- |
-------------------------------------------

-------------------------
|   |  ---------------- |
| 3 |  | UVQ5cjk3SQ== | |
|   |  ---------------- |
-------------------------

Voilà qui nous rappelle quelque chose : en fait on va ajouter un nouvel en-tête, donc on va avoir affaire à un nouveau protocole dont la charge utile sera le message du protocole applicatif tout entier (en-tête et charge utile). C’est ça, l’encapsulation. Un protocole est constitué d’un en-tête et d’une charge utile, appelée PDU, elle-même constituée d’un autre protocole. A la fin, le PDU est juste le message que l’on veut transmettre. On peut empiler de la sorte plusieurs protocoles : Empiler les protocoles

Figure 2 — Empiler les protocoles

💡 Pas systématique
Toutes les technologies de transmission n’implémentent pas cela : la radio FM par exemple n’encadre pas ses données dans des trames, le signal est continu (c’est un signal analogique). Mais en informatique, c’est très courant d’avoir recourt à cela, que ce soit en Ethernet (on voit ça bientôt t’inquiète), en USB, ou autre. En radio numérique aussi, on a des trames.

Vers une généralisation

Les couches

Il est temps de parler des couches réseau. A deux reprises, on a rajouté des données à notre message initial : une fois dans un contexte applicatif, une fois dans le contexte de la transmission sur un canal. On peut voir cela comme une imbrication : on prend notre donnée, on la formalise selon un protocole, et on place le tout dans un autre protocole (ici pour faire des trames), en découpant si besoin. Cette imbrication s’appelle l’encapsulation.

Dans le premier cas, cette “augmentation” a été nécessaire pour que le message soit compris par le destinataire. Dans le second, pour qu’il arrive à destination en empruntant un canal physique. Chaque encapsulation répond donc à une fonction. C’est vraiment très important. A chaque fonction réseau correspond une encapsulation et donc l’ajout d’une couche. Cela se traduit par l’ajout de données au message initial.

//schéma

On a ici deux couches :

  • La couche applicative, qui contient les données utiles (mises en forme).
  • La couche physique, qui permet la transmission sur un canal.
💡 Précision
En réalité le framing n’appartient pas à la couche physique mais à une autre couche que l’on verra plus tard. Le rôle de la couche physique est uniquement d’envoyer la donnée binaire sur le canal, mais il me paraissait important de parler du framing et ça va nous servir pour le TP. La couche dont je parle est responsable d’autres fonctions, pour ne pas tout compliquer j’ai un peu triché.

La charge utile d’une couche N est la couche N+1 (après l’encapsulation c’est à dire après l’ajout d’informations utiles à la fonction de cette couche N+1). L’implémentation technologique de chaque couche s’appelle un protocole. Un protocole peut implémenter plusieurs couches à la fois, c’est à dire plusieurs fonctions réseau à la fois. Certaines couches peuvent être divisées en sous-couche, si l’implémentation de la couche est complexe et nécessite beaucoup de mécanismes (spoiler : c’est le cas d’Ethernet).

Dans le prochain article théorique, on va un peu améliorer notre canal pour permettre la communication à plusieurs. Mais d’ici là je vous propose un petit TP pour mettre en pratique dans un cas très simple un protocole applicatif au dessus d’un canal physique.