Jusque là, nous avons transmis des données d'un ordinateur à un autre sans faire attention aux types de données que nous transmettions. Mais cela pose de sérieux problèmes de portabilité.
En effet, après avoir sûrement lu le cours de M@téo21 sur
l'allocation dynamique vous avez appris que, par exemple, la taille d'une variable de type
int peut varier d'un ordinateur à un autre.
Donc, si nous envoyons une variable de type
int à partir d'un ordinateur où le type
int fait 8 octets sur un autre ordinateur où le type
int fait 2 octets cela posera irrémédiablement problème : Pendant que l'ordinateur source envoi une seule variable de type
int codée sur 8 octets, l'ordinateur de destination se verra recevoir 4 variables de type
int 
!
Ce problème pourrait être réglé en utilisant un type de données qui garde le même nombre d'octets d'un ordinateur à autre...
Mais voilà qu'il y à un second problème : l'ordre des octets d'une variable codée sur plusieurs octets n'est pas toujours le même non plus

.
Il y a plusieurs façon de représenter un groupe d'octets en mémoire, on peut commencer par l'octet de poids fort ou par l'octet de poids faible par exemple, cela s'appelle
l'Endianness (ou boutisme en français). Si un groupe d'octet commence par l'octet de poids fort on dit que sont orientation est big-endian, s'il commence par l'octet de poids faible on dit que sont orientation est little-endian. La façon d'organiser un groupe d'octet en mémoire dépend de l'architecture de la machine.
Pour résumer...
Si votre application communique avec une autre application qui se trouve sur le même ordinateur, il n'y aura donc pas de problème car la taille des types de données ne change pas d'une application à une autre, de même pour l'ordre des octets d'une variable en mémoire.
A contrario, pour réaliser une communication portable entre deux ordinateurs, il faut absolument transmettre les octets de nos variables un à un selon un ordre donné qui est le même pour les deux applications distantes. Des fonctions existent pour réaliser ces manipulations, nous allons donc commencer par les étudier puis nous allons les utiliser dans le cadre d'un exemple pour bien comprendre leur fonctionnement.
Fonctions de conversion
Deux groupes de fonctions de conversion de l'ordre des octets existe. Le premier groupe de fonctions a pour but de convertir un entier qui à l'endianness de votre ordinateur en un entier qui à l'endianness du réseau qui est toujours en big-endian (octet de poids fort en première position). Le second groupe de fonctions a pour but de faire la même opération mais dans le sens opposé.
Code : C | unsigned long htonl(unsigned long hostlong);
unsigned short htons(unsigned short hostshort);
unsigned long ntohl(unsigned long netlong);
unsigned short ntohs(unsigned short netshort);
|
La fonction htonl convertit l'entier de 4 octets hostlong depuis l'ordre des octets de l'hôte vers celui du réseau.
La fonction htons convertit l'entier de 2 octets hostshort depuis l'ordre des octets de l'hôte vers celui du réseau.
La fonction ntohl convertit l'entier de 4 octets netlong depuis l'ordre des octets du réseau vers celui de l'hôte.
La fonction ntohs convertit l'entier de 2 octets netshort depuis l'ordre des octets du réseau vers celui de l'hôte.
Un exemple de fonctionnement des fonctions htonl et ntohl
Supposons que vous vouliez transmettre un entier codée sur 4 octets à un autre ordinateur de manière portable. Il va falloir décomposer l'entier en 4 parties et envoyer chaque octet un à un.
De même pour la réception sauf qu'il va juste falloir faire l'opération inverse.
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | void send4(int sock, unsigned long data)
{
// Tableau d'octet qui sera ensuite envoyé
char dataSend[4];
// On décompose l'entier 'data' de 4 octets en 4 parties de 1 octet
dataSend[0] = (data >> 24) & 0xFF; // On sélectionne l'octet de poids fort de 'data' que l'on met dans la première case du tableau d'octet 'dataSend'
dataSend[1] = (data >> 16) & 0xFF; // De même avec l'octet qui suit
dataSend[2] = (data >> 8) & 0xFF; // De même avec l'octet qui suit
dataSend[3] = (data >> 0) & 0xFF; // On sélectionne l'octet de poids faible de 'data' que l'on met dans la dernière case du tableau d'octet 'dataSend'
// On envoi les 4 octets dans un ordre qui ne change jamais quelque soit la machine
send(sock, dataSend, 4, 0);
}
void read4(int sock, unsigned long* data)
{
char dataRecv[4];
// On reçoit une suite de 4 octets, le premier octet reçu est toujours l'octet de poids fort
recv(sock, dataRecv, 4, 0);
// On rassemble les 4 octets séparé en une seul variable de 4 octets
unsigned long temp = 0;
temp |= dataRecv[0] << 24;
temp |= dataRecv[1] << 16;
temp |= dataRecv[2] << 8;
temp |= dataRecv[3] << 0;
// On fini par copier le résultat dans 'data'
*data = temp;
}
|
Ces deux fonctions sont un peu lourdes donc je vous conseil d'utiliser plutôt les fonctions de conversion spécifiées plus haut. Voici les même fonctions send4 et read4 implémentées en utilisant les fonctions htonl et ntohl :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | void send4(int sock, unsigned long data)
{
// On convertit data en entier big-endian
long dataSend = htonl(data);
// On envoie l'entier convertit
send(sock, (char*)&dataSend, 4, 0);
}
void read4(int sock, unsigned long* data)
{
long dataRecv;
// On récupère l'entier en big-endian
recv(sock, (char*)&dataRecv, 4, 0);
// On convertit l'entier récupéré en little-endian si l'ordinateur
// stock les entiers en mémoire en little-endian, sinon s'il les
// stock en big-endian l'entier est convertit en big-endian
*data = ntohl(dataRecv);
}
|
Notez que si vous voulez envoyer un entier de 2 octets le fonctionnement est exactement le même

.
Pour ce qui est de la transmission de structures, il faudra envoyer chaque éléments qui la compose un à un pour que le code reste portable.