Outils pour utilisateurs

Outils du site


cours:informatique:iot:programmer_internet_des_objets:210_representation_des_donnees

Cours “Programmer l'Internet des objets” proposé sur la plateforme FUN-MOOC par l'Institut Mines Télécom.

La représentation des données

Vidéo : Sérialisation des données

Transcription de la vidéo Qu'est ce que la Sérialisation des données ⚙️ MOOC PLIDO

On s’intéresse ici à la sérialisation, qui est un concept important mais souvent ignoré quand on veut transmettre des données d'un ordinateur à un autre.

Pour cela on va étudier la facon dont les variables sont stockées en mémoire. Avec le pseudo code number = 10 on affecte la valeur 10 à la variable 'number'. La valeur binaire de 10 sera stockée quelque part dans la mémoire de l'ordinateur et on la relira par exemple pour l'afficher print(number).

Même chose avec une chaîne de caractères, elle sera stockée quelque part en mémoire : chaque caractère occupant un octet.

Si on veut envoyer la donnée d'un ordinateur à l'autre par exemple en utilisant une pseudo commande send() l'envoie du texte ne posera pas de problème mais l'envoi de la variable numérique retournera une erreur… Pourquoi ?

Envoyer un texte consiste à l'émettre caractère par caractère et la destination reconstitue le message. Tous les ordinateurs utilisent la même représentation des caractères (du moins ceux de base), donc ca ne pose aucun problème de transfert.

Un nombre au contraire utilise la représentation locale de la machine : par exemple sur 64 bits ou 32 bits. Si on l'envoi bit à bit à l'autre ordinateur, il y a de grande chance pour que la valeur ne soit pas interprétée de la même manière.

On pourrait transformer la valeur numérique en une chaîne de caractères et l'envoyer : mais comment l'ordinateur en reception ferait la différence entre l'envoie de la chaîne “10” et l'envoie du nombre 10. Il faut que l'équipement distant puisse comprendre le type de variable que l'on envoie pour le stocker de la bonne manière.

Que se passera t-il si on veut envoyer deux nombres par exemple 10 et 11 ? Comment faire pour qu'ils ne soient pas pris en compte comme étant 1011 ?

Comme on peut constater ici, la sérialisation, c'est pas aussi simple que ça en à l'air.

Difficulté de l'envoie de donnée

Envoyer une donnée sur un réseau n’est pas aussi simple que l’on croit.

Il faut faire la différence entre le format utilisé pour stocker des données dans la mémoire de l'ordinateur et celui employé pour l'envoyer à une autre machine. En effet, chaque machine à sa propre représentation souvent liée aux capacités de leur processeur. Cela est surtout vrai pour les nombres. Ils peuvent être stockés sur un nombre de bits plus ou moins important ou peuvent être représentés en mémoire de manière optimisée pour accélérer leur traitement.

En revanche, la représentation des chaînes de caractères (non accentués) est relativement uniforme car elle se base sur le code ASCII qui est le même pour tous les ordinateurs. Un texte de base est facilement compréhensible par toutes les machines. Une solution serait donc de n'utiliser que des chaînes de caractères.

Par exemple, si l’on veut envoyer l'entier ayant pour valeur 123, il existe plusieurs représentations possibles :

  • Envoyer une chaîne de caractères ”123” contenant les chiffres du nombre ;
  • envoyer la valeur binaire 1111011.

On voit que juste pour transmettre une simple valeur stockée dans la mémoire d'un ordinateur, il existe plusieurs options et évidemment pour que cette valeur soit interprétée de la bonne façon, il faut que les deux extrémités se soient mises d'accord sur une représentation.

Quand on veut transmettre plusieurs valeurs, c'est-à-dire quand on a des données structurées, d'autres problèmes surviennent.

Par exemple : quelle est la taille des blocs que l’on va transmettre ? Comment indiquer la fin de la transmission ? Pour une chaîne de caractères, comment indiquer qu’elle se termine ? Autre exemple : si l'on veut transmettre “12” puis “3”, comment faire pour que l'autre extrémité ne comprenne pas “123” ?

Pour que la transmission se fasse correctement, il faut que l’émetteur et le récepteur adoptent les mêmes conventions. Quand il s’agit d’un ensemble de données, il faut être capable de les séparer. Avec les tableurs, une première méthode est possible avec la notation CSV (pour Comma Separated Values). Comme son nom l’indique, les valeurs sont séparées par des virgules. Les valeurs sont représentées par des chaînes de caractères. Les textes sont différenciés des valeurs numériques, par l’utilisation de guillemets. Ainsi, 123 sera interprété comme un nombre et ”123” comme un texte.

Si cette représentation est adaptée aux tableurs, elle est relativement pauvre car elle ne permet de représenter que des valeurs sur des lignes et des colonnes. Pour les usages du Web, il a fallu trouver un format plus souple permettant de représenter des structures de données complexes. Évidemment, comme rien n'est simple, il en existe plusieurs et les applications échangeant des données devront utiliser le même.

On voit que l'envoi de la chaîne de caractères ne suffit pas, il faut la formater pour que le récepteur puisse trouver le type de la donnée transmise, qu'un nombre ne soit pas interprété comme une chaîne de caractères, qu'une chaîne de caractères reste une chaîne de caractères même si elle ne contient que des chiffres.

QUIZ 2.1

1-Combien faut-il au minimum d'octets pour représenter en binaire la valeur 123 ?

1

2-Combien faut-il de caractères pour transmettre cette valeur ?

3

CBOR

JSON et CBOR sont tous les deux des modes de codage de la donnée.

JSON introduit une notation très flexible permettant de représenter toutes les structures de données. Le choix de l'ASCII rend ce format universel et n'importe quel ordinateur pourra le comprendre. Mais l'utilisation de l'ASCII ne permet pas de transmettre de manière optimale l'information sur un réseau. Quand les réseaux ont un débit raisonnable, cela ne pose pas de problème. Quand on en vient à l'internet des objets, il faut prendre en compte la capacité de traitement limité des équipements et la faible taille des messages échangés.

Ainsi, en ASCII, la valeur 123 est codée sur 3 octets (un octet par caractère) tandis qu'en binaire elle n'occuperait qu'un seul octet : 0111 1011.

CBOR (Concise Binaire Object Representation), défini dans le RFC 7049, permet de représenter les structures de JSON mais suivant une représentation binaire. CBOR est complètement compatible avec JSON, mais il est également possible de représenter d'autres types d'information très utiles dans l'internet des objets.

La taille de l'information est réduite et le traitement simplifié. Il faut savoir un peu jongler avec la représentation binaire mais cela reste basique.

CBOR définit 8 types majeurs qui sont représentés par les 3 premiers bits d'une structure CBOR (bits de poids fort du premier octet). Ces types majeurs sont donc définis par des valeurs comprises entre 0 et 7 (000 à 111 en binaire).

Valeur dec Valeur binaire Type majeur CBOR
0 000 Entier positif
1 001 Entier négatif
2 010 chaine d'octets
3 011 Chaîne de texte
4 100 Tableau
5 101 Liste d'objet
6 110 Tag/étiquette sémantique CBOR
7 111 true, false, null et nombre flottant

Les cinq bits suivants du premier octet contiennent soit une valeur soit une longueur indiquant combien d'octets supplémentaires sont nécessaires pour coder la valeur. CBOR offre ainsi des optimisations qui permettent de réduire la longueur totale de la structure des données.

JSON ne fait pas de différence entre les nombres, entiers, décimaux, positifs ou négatifs. CBOR réintroduit une distinction pour optimiser la représentation.

Le premier type majeur correspond aux entiers positifs. Il est codé par 3 bits à 0; les 5 bits suivants finissent l'octet et, suivant leur valeur, vont avoir une signification différente :

  • de 0 à 23, il s'agit de la valeur de l'entier à coder ;
  • 24 indique que l'entier est codé sur 1 octet qui sera codé dans l'octet suivant ;
  • 25 indique que l'entier est codé sur 2 octets qui seront codés dans les deux octets suivants ;
  • 26 indique que l'entier est codé sur 4 octets qui seront codés dans les quatre octets suivants ;
  • 27 indique que l'entier est codé sur 8 octets qui seront codés dans les huit octets suivants.

Expérimenter CBOR avec Pyhton

Pour mieux appréhender l'encodage CBOR, on peut le manipuler de façon interactive via Python. Ici on crée un environnement virtuel et on installe le module cbor2

# Creation d'un environnement virtuel python pour l'expérimentation
python3 -m venv py_test_cbor
# active l'envr
cd py_test_cbor && source bin/activate
 
# installation du module
pip3 install cbor2

Depuis l'environnement virtuel on peut lancer l'interpréteur Python3 puis taper le code ci-dessous:

# charger le module CBOR
import cbor2 as cbor
 
# valeur entiere à encoder
v = 1
 
# i permet de faire évoluer l'ordre de grandeur de v
for i in range(0,19):
     # c stocke l'encodage cbor de la valeur v 
     c = cbor.dumps(v)
     # utilise une f-string pour afficher l'ordre de grandeur i, la valeur décimale v et l'encodage CBOR de v en hexadécimal 
     print(f"{i:3} {v:30} {c.hex()}") 
     v *= 10

L’exécution du code ci-dessus produit la sortie suivante:

                                                                                            
  0                              1 01
  1                             10 0a
  2                            100 1864
  3                           1000 1903e8
  4                          10000 192710
  5                         100000 1a000186a0
  6                        1000000 1a000f4240
  7                       10000000 1a00989680
  8                      100000000 1a05f5e100
  9                     1000000000 1a3b9aca00
 10                    10000000000 1b00000002540be400
 11                   100000000000 1b000000174876e800
 12                  1000000000000 1b000000e8d4a51000
 13                 10000000000000 1b000009184e72a000
 14                100000000000000 1b00005af3107a4000
 15               1000000000000000 1b00038d7ea4c68000
 16              10000000000000000 1b002386f26fc10000
 17             100000000000000000 1b016345785d8a0000
 18            1000000000000000000 1b0de0b6b3a7640000

Sur la première ligne on lit bien que la valeur décimale 1 est codée sur un octet de valeur hexadécimale 0x01

Sur la deuxième ligne on lit que la valeur décimale 10 est stockée sur 1 octet de valeur hexadécimale 0x0a. Avec une f-string on peut obtenir facilement la représentation binaire:

# valeur binaire de 0x0a
f"{int('0a', 16):08b}"                                                                      
'00001010'

Les 3 bits de poids forts sont 000 (structure majeure CBOR entier positif), les 5 bits restants permettent de coder la valeur 01010 = 0xa = 10

A partir de la troisième ligne on constate que la valeur décimale 100 est codée sur 2 octets 0x1864. Toujours via une f-string on peut facilement changer la base de ce premier octet:

# hexa -> binaire
f"{int('18', 16):08b}"                                                                      
'00011000'
 
# hexa -> décimal
f"{int('18', 16)}"                                                                          
'24'

Les 3 bits de poids fort sont bien à 0, on code une structure CBOR de type entier positif, les 5 bits de poids faible codent 24 en décimal indiquant que la valeur entière est codée sur le prochain octet. Le prochain octet vaut 0x64 = 100 en décimal.

# hexa -> décimal
f"{int('64', 16)}"                                                                          
'100'

Sur la quatrième ligne la logique est la même le premier octet code la valeur héxadécimale 0x19 = 25 indiquant un entier codé sur les deux octets suivants

code CBOR 0x1903e8
# premier octet 0x19
f"{int('19', 16):08b}"                                                                      
'00011001'
 
f"{int('19', 16)}"                                                                          
'25'
 
# 2 octets suivants hexa -> decimal
f"{int('03e8', 16)}"                                                                        
'1000'

On peut noter qu'il n'y a pas de surcoût pour coder un entier de 0 à 23. Ainsi, la valeur 15 sera codée 0x0F (000-0 1111) tandis que, pour toutes les autres valeurs supérieures, le surcoût ne sera que d'un octet. 100 sera codé 000-1 1000 (11000 correspond à 24) suivi de la valeur 100 (0110 0100).

Les valeurs entre 100 000 et 1 000 000 000 nécessitent 5 octets et les suivantes 9 octets.

La taille de la représentation s'adapte à la valeur. Ainsi, il n'est pas nécessaire de définir une taille fixe pour coder une donnée.

On peut aussi noter que comme le type majeur est sur 3 bits, ce type peut être reconnu car il commence par la valeur “0” ou “1”.

Type Entier Négatif

Le type majeur entier négatif est à peu près similaire à l'entier positif. Le type majeur est 001 et le codage de la valeur se fait sur la valeur absolue du nombre à laquelle on retranche 1. Cela évite deux codes différents pour les valeurs 0 et -0.

Ainsi, pour coder -15 en CBOR, on va coder la valeur 14 avec le type majeur 001, ce qui donne en binaire 001-0 1110. Ainsi, -24 peut également être codé sur 1 octet tandis que +24 sera codé sur 2 octets.

import cbor2 as cbor
v = -15                                                                                     
c = cbor.dumps(v)                                                                           
 
f"v={v} v(CBOR base 2)={int(c.hex(), 16):08b} v(CBOR base 16)={c.hex()}"                    
'v=-15 v(CBOR base 2)=00101110 v(CBOR base 16)=2e'

Type Séquence binaire ou Chaîne de caractères

Les séquences binaires et les chaînes de caractères ont le même comportement. Le type majeur est respectivement 010 et 011. Il est suivi par la longueur de la séquence ou de la chaîne. Le même type de codage que pour les entiers est utilisé :

  • si la longueur est inférieure à 23, elle est codée dans la suite du premier octet. On trouve ensuite le nombre d'octets ou de caractères correspondant à cette longueur;
  • si la longueur peut être codée dans 1 octet (donc inférieure à 255), la suite du premier octet contient 24 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.
  • si la longueur peut être codée dans 2 octets (donc inférieure à 65535), la suite du premier octet contient 25 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.
  • si la longueur peut être codée dans 4 octets, la suite du premier octet contient 26 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.
  • si la longueur peut être codée dans 8 octets, la suite du premier octet contient 27 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.

Ce codage est aussi assez optimal. Il est rare d'envoyer plus de 23 caractères.

import cbor2 as cbor
 
for i in range(1,10): 
     c = cbor.dumps("LoRaWAN"*i) 
     print(f"{i:3} {c.hex()}")

On obtient la sortie suivante:

                                                                                              
  1 674c6f526157414e
  2 6e4c6f526157414e4c6f526157414e
  3 754c6f526157414e4c6f526157414e4c6f526157414e
  4 781c4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  5 78234c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  6 782a4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  7 78314c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  8 78384c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  9 783f4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e

Ici on décortique le premier l'octet du code CBOR sur la première ligne

# hex -> base 2
f"{int('0x67',16):08b}"                                                                      
'01100111'
# 011 -> structure majeure chaîne de texte ; 00111 -> 7 caractères
 
# affichage des 7 caractères
bs = b"\x4c\x6f\x52\x61\x57\x41\x4e"                                                         
print(bs)                                                                                           
b'LoRaWAN'

Jusqu'à 3 répétitions de la chaîne de caractères “LoRaWAN”, le codage de la longueur est optimal (codé sur 2 octets).

# ligne 2
f"{int('0x6e',16):08b}"                                                                      
'01101110'
# 011 -> structure majeure chaîne de texte ; 01110 -> 14 caractères
 
#ligne 3
f"{int('0x75',16):08b}"                                                                      
'01110101'
# 011 -> structure majeure chaîne de texte ; 10101 -> 21 caractères
 
#ligne 4
f"{int('0x78',16):08b}"                                                                      
'01111000'
# 011 -> structure majeure chaîne de texte ; 11000 -> 24 -> longueur codée dans l'octet suivant 0x1c
f"{int('0x1c',16)}"                                                                         
'28'

Type tableau

Le type tableau va regrouper un ensemble d'éléments. Chacun de ces éléments étant une structure CBOR, la seule information nécessaire pour connaître le début et la fin d'un tableau est son nombre d'éléments. Le type majeur est 100. Il existe deux méthodes pour coder la longueur d'un tableau :

  • si celle-ci est connue au moment du codage, il suffit de l'indiquer avec un codage identique à celui utilisé pour indiquer la longueur d'une chaîne de caractères ;
  • si celle-ci n'est pas connue au moment du codage, il existe un code spécial pour indiquer la fin du tableau. Nous en reparlerons par la suite.

Quelques exemples de codages de tableau CBOR:

  • [1,2,3,4] devient 8401020304. On peut deviner la structure du message CBOR : 0x84 indique un tableau de 4 éléments (attention le décodage n'est pas toujours aussi simple). Les 4 éléments sont des entiers inférieurs à 23 ;
  • [1,[2, 3], 4] devient 830182020304. Il s'agit d'un tableau de 3 éléments dont le deuxième est un tableau de deux éléments ;
  • [1000, +20, -10, +100, -30, -50, 12] devient 871903e814291864381d38310c.

Le site web cbor.me permet de faire automatiquement le codage dans un sens ou dans l'autre. La colonne de gauche représente la donnée en JSON et celle de droite en CBOR (dite “représentation canonique” facilitant la lecture).

On peut calculer le degré de compression de CBOR. Ainsi, dans le premier exemple, le tableau JSON [1,2,3,4] faisait 9 caractères tandis que la représentation CBOR n'en faisait que 5. Pour le dernier exemple, les 13 octets de la représentation CBOR sont transformés en 34 caractères (donc 34 octets) en JSON.

Type objets

Le type majeur objets ( également désigné 'mapped', 'liste de paires' ou 'dictionnaire') est indiqué par la valeur 101. Il fonctionne de la même manière que les tableaux en comptant le nombre d'éléments. Mais cette fois-ci, la valeur code une paire, c'est-à-dire deux objets CBOR.

Il existe des différences entre JSON, CBOR et la représentation des variables en Python:

  • Les codages hexadécimaux et binaires de Python sont convertis en décimal lors du codage en JSON ;
  • JSON n'autorise que des clés en ASCII pour indexer les paires. Si on code en CBOR depuis Python on peut choisir d'utiliser des clés numériques mais on perd alors la compatibilité vers JSON ;
  • CBOR permet de définir des clés identiques ce qui n'est pas possible dans un dictionnaire en Python.

Type étiquette

CBOR enrichit le typage des données ; ce qui permet de manipuler plus facilement des données. Par exemple, une chaîne de caractères peut représenter une date, une URI, voire une URI codée en base 64. Le type 110 peut être suivi d'une valeur dont une liste exhaustive est donnée ici.

import cbor2 as cbor
from datetime import date, timezone                                                          
 
# instanciation d'un objet date python
d = date.today()                                                                             
 
print(d)                                                                                     
2021-12-05
 
type(d)                                                                                     
datetime.date
 
# Encode l'objet date en CBOR
c1 = cbor.dumps(d, timezone=timezone.utc, date_as_datetime=True)                             
 
# affiche la valeur hexadécimale 
print(c1.hex())                                                                              
c074323032312d31322d30355430303a30303a30305a
 
# conversion de l'encodage CBOR en variable Python 
d1 = cbor.loads(c1)                                                                          
 
# affiche la valeur 
print(d1)                                                                                    
2021-12-05 00:00:00+00:00
 
# Affiche le type de la valeur
type(d1)                                                                             
datetime.datetime

Ci-dessous la représentation canonique de la valeur encodée en CBOR:

C0                                      # tag(0)
   74                                   # text(20)
      323032312D31322D30355430303A30303A30305A # "2021-12-05T00:00:00Z"

Notons que le tag 0 implique un format normalisé pour la date ; d'où l'ajout des heures, minutes et secondes, alors qu'elles n'ont pas été spécifiées initialement. On peut également remarquer que loads() retourne un type date et non une chaîne de caractères.

Flottants et valeurs particulières

Le dernier type majeur (111) permet de coder les nombres flottants en utilisant la représentation définie par l'IEEE 754. Suivant la taille de la représentation, la suite de l'octet contient les valeurs 25 (demi précision sur 16 bits), 26 (simple précision sur 32 bits) ou 27 (double précision sur 64 bits).

Ce type permet également de coder les valeurs définies par JSON : True (valeur 20), False (valeur 21) ou None (valeur 22).

Finalement, ce type peut indiquer la fin d'un tableau ou d'une liste de paires quand la taille n'est pas connue au début du codage.

◁ Précédent | ⌂ Sommaire | Suivant ▷

cours/informatique/iot/programmer_internet_des_objets/210_representation_des_donnees.txt · Dernière modification : 2023/05/27 10:12 de yoann