Pour la conception de programmes en C, voir le chapitre sur le langage C.
Tous les exemples de programmes C ont été compilés avec gcc et make et testés sous linux.
Structure pour la connexion ipv4
Structure pour la connexion ipv6
La structure sockaddr_in6 contient les champs :
La structure addrinfo, élément d'une liste chaînée, qui contient principalement les champs :
Les formats des données entières peuvent être différentes entre le réseau et l'ordinateur, afin d'éviter toute erreur, il convient d'utiliser les fonctions adaptées pour le numéro de port et l'adresse ip, voici quelques exemples de fonctions :
Ces informations sont accessibles à l'aide de la fonction getaddrinfo qui, en plus de fournir les informations des interfaces réseaux, permet de fournir l'adresse ip à partir du nom FQDN (Fully qualified domain name) en utilisant la résolution DNS si cela est possible. Ces informations sont stockées dans la liste chaînée d'éléments de type addrinfo.
Client
Serveur
Toutes ces fonctions peuvent être groupées dans un fichier netcommon.c et définies dans le fichier netcommon.h, dans lequel, il ne faut pas oublier d'inclure les fichiers de prototypes et constantes : rpa/inet.h et netdb.h
Il faudra déclarer les constantes
#define NON_ERREUR 0
#define ERREUR -1
#define SOCK_ERR -1
#define PORT_DEFAUT 1234
#define IP_DEFAUT "127.0.0.1"
#define NOM_DEFAUT "localhost"
#define TAILLE_BUFFER 1024
#define TAILLE_NOM_IP 100
Fonction de configuration de la structure de données
int createsock(char *nom,unsigned int port,struct sockaddr_in *hote) {
struct addrinfo *hoteinfo, hotecfg;
int lg = 0 ;
hote->sin_family = AF_INET ;
hote->sin_port=htons(port) ;
int ipAddr = inet_addr(nom);
if (ipAddr != INADDR_NONE) {
lg = sizeof(struct sockaddr_in) ;
hote->sin_addr.s_addr = ipAddr ;
}
else {
bzero(&hotecfg, sizeof(struct addrinfo));
hotecfg.ai_family = AF_INET ;
int err = getaddrinfo(nom,NULL,&hotecfg,&hoteinfo);
if (err == 0) {
lg = hoteinfo->ai_addrlen;
hote->sin_addr.s_addr = ((struct sockaddr_in *)(hoteinfo->ai_addr))->sin_addr.s_addr ;
}
else {
perror("erreur ");
}
freeaddrinfo(hoteinfo);
}
return lg;
}
Cette fonction initialise les champs d'une structure de type sockaddr_in. L'adresse IP est soit transmise sous la forme d'une chaîne de caractère, soit obtenue à partir du nom DNS FQDN (Fully qualified domain name).
Afin de détecter si le nom correspond à une adresse IP ou bien un nom, on utilise la fonction inet_addr. Si la fonction retourne une erreur, on considère qu'il s'agit d'un nom et on retrouve l'adresse IP avec la fonction getaddrinfo.
Cette fonction retourne la taille de la structure de données ou bien 0 en cas d'erreur.
La fonction htons permet de convertir le type entier de l'ordinateur au type entier du réseau quelque soit le format utilisé par l'ordinateur.
La fonction bzero permet d'initialiser à 0 une zone mémoire.
Initialisation mode client
int InitClientSocket(unsigned int numport,struct sockaddr_in *p_socket_adresse) {
int clientsocket;
int err;
clientsocket = socket(PF_INET, SOCK_STREAM, 0);
if (clientsocket == SOCK_ERR) {
perror("Erreur socket ");
return clientsocket;
}
err=connect(clientsocket,(struct sockaddr *)p_socket_adresse,sizeof(struct sockaddr_in));
if (err == SOCK_ERR) {
perror("Erreur connect ");
return err;
}
return clientsocket;
}
La fonction socket initialise la connexion et retourne un identifiant qui est utilisé par les fonctions qui accèdent au réseau.
La fonction connect établit une connexion avec le serveur en utilisant la structure de donnée sockaddr_in.
Cette fonction d'initialisation retourne l'identifiant de connexion ou un code d'erreur (-1).
Cet identifiant sera ensuite utilisé par les fonctions de lecture et d'écriture.
Initialisation mode serveur
int InitServerSocket(unsigned int numport, struct sockaddr_in *p_socket_adresse) {
int serveursocket;
int err=0;
serveursocket = socket(PF_INET, SOCK_STREAM, 0);
if (serveursocket == SOCK_ERR) {
return serveursocket;
}
int p=1;
err=setsockopt (serveursocket, SOL_SOCKET, SO_REUSEADDR, &p, sizeof(int));
if (err == SOCK_ERR) {
perror("Erreur socket ");
return err;
}
err=bind(serveursocket, (struct sockaddr *)p_socket_adresse, sizeof(struct sockaddr_in ));
if (err == SOCK_ERR) {
perror("Erreur bind ");
return err;
}
err=listen(serveursocket, 10);
if (err == SOCK_ERR) {
perror("Erreur listen ");
return err;
}
return serveursocket;
}
La fonction setsockopt permet de modifier le paramétrage de la connexion par défaut. Ici elle est utilisée pour permettre de réutiliser la connexion, immédiatement après une erreur, ce qui n'est pas le cas par défaut où il faut attendre un certain temps avant de refaire un essai.
La fonction bind assigne la connexion à la carte réseau. Pour un serveur, il n'y a pas de fonction connect.
La fonction listen définit la taille de la file d'attente pour les connexions.
Cette fonction d'initialisation retourne un identifiant pour la fonction d'attente de connexion ou une erreur (-1) en cas de problème.
Programme client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
int main(int nbargs,char *args[]) {
int clientsocket;
struct sockaddr_in sock_adresse;
int port = PORT_DEFAUT ;
char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
int lg ;
int taille;
char buffer[TAILLE_BUFFER];
printf("connexion à %s port %d\n",nom,port);
lg = createsock(nom,port,&sock_adresse);
if (lg == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
clientsocket=InitClientSocket(port,&sock_adresse);
if (clientsocket == SOCK_ERR) {
perror("Erreur connect");
return EXIT_FAILURE;
}
printf("connecte\n");
if (nbargs == 2) {
strcpy(buffer,args[1]);
}
else {
strcpy(buffer,"bonjour");
}
taille = strlen(buffer);
printf("envoi : %s\n",buffer);
send(clientsocket,buffer,taille+1,0);
taille = recv(clientsocket,buffer,TAILLE_BUFFER,0);
printf("reçu : %s\n",buffer);
strcpy(buffer,"merci pour tout");
taille = strlen(buffer);
printf("envoi : %s\n",buffer);
send(clientsocket,buffer,taille+1,0);
close(clientsocket);
return EXIT_SUCCESS;
}
Le programme de test envoie un texte qui peut être fourni en ligne de commande ou "bonjour" par défaut.
Après avoir initialiser la connexion avec le nom ou l'adresse IP du serveur et le numéro de port. Le programme envoie le texte, attend la réponse puis envoie le message "merci pour tout".
Le programme envoie la chaîne avec le caractère 0 de fin de chaîne. Ce qui explique la valeur taille+1 dans la fonction send.
L'affichage de la réponse fonctionne si, comme ce programme, le serveur a envoyé le 0 de fin de chaîne.
Il ne faut pas oublier la fonction close qui termine la connexion.
Exemple d'affichage
connexion à localhost port 1234 connecte envoi : bonjour reçu : bonjour OK envoi : merci pour tout
Programme serveur
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
int main(int nbargs,char *args[]) {
int serveursocket, client;
struct sockaddr_in socket_adresse;
socklen_t long_adr;
int port = PORT_DEFAUT;
char buffer[TAILLE_BUFFER];
int taille;
long_adr = createsock("0.0.0.0",port,&socket_adresse);
if (long_adr == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
serveursocket = InitServerSocket(port,&socket_adresse);
if (serveursocket == -1) {
perror("Erreur socket ");
return EXIT_FAILURE;
}
printf("Ecoute port %d\n",port);
client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
printf("client connecte, ...\n");
taille = recv(client,buffer,TAILLE_BUFFER,0);
printf("reçu : %s\n",buffer);
strcat(buffer," OK");
taille = strlen(buffer);
printf("envoi : %s\n",buffer);
send(client,buffer,taille+1,0);
taille = recv(client,buffer,TAILLE_BUFFER,0);
printf("reçu : %s\n",buffer);
close(client);
close(serveursocket);
return EXIT_SUCCESS;
}
La fonction accept est bloquante et attend la connexion du programme client. Cette fonction retourne un identifiant utilisé par les fonctions de lecture et écriture.
Ce programme attend le message du programme client, l'affiche, ajoute OK à la fin du message puis envoie le message complet. Il attend ensuite un dernier message du programme client qu'il affiche.
Il ne faut pas oublier la fonction close qui met fin à l'échange de données.
Il ne faut pas oublier la fonction close qui termine la connexion.
Exemple d'affichage
Ecoute port 1234 client connecte, ... reçu : bonjour envoi : bonjour OK reçu : merci pour tout
Toutes ces fonctions peuvent être groupées dans un fichier netcommon6.c et définies dans le fichier netcommon6.h, dans lequel, il ne faut pas oublier d'inclure les fichiers de prototypes et constantes : rpa/inet.h et netdb.h
Il faudra déclarer les constantes
#define NON_ERREUR 0 #define ERREUR -1 #define SOCK_ERR -1 #define PORT_DEFAUT 1234 #define IP_DEFAUT "::1" #define NOM_DEFAUT "localhost" #define TAILLE_BUFFER 1024
Fonction de configuration de la structure de données
int createsock(char *nom,unsigned int port,struct sockaddr_in6 *hote) {
struct addrinfo hotecfg, *hoteinfo;
unsigned char *p = hote->sin6_addr.s6_addr;
int lg = 0 ;
hote->sin6_family=AF_INET6;
hote->sin6_port=htons(port);
int r = inet_pton(AF_INET6,nom, p);
if (r == 1) {
lg = sizeof(struct sockaddr_in) ;
inet_pton(AF_INET6,nom, p);
}
else {
bzero(&hotecfg,sizeof (hotecfg));
hotecfg.ai_family = AF_INET6;
int err = getaddrinfo(nom,NULL,&hotecfg,&hoteinfo);
if (err == 0) {
lg = hoteinfo->ai_addrlen;
bcopy(((struct sockaddr_in6 *)(hoteinfo->ai_addr))->sin6_addr.s6_addr,hote->sin6_addr.s6_addr,lg);
}
else {
fprintf(stderr,"ERREUR DNS\n");
}
freeaddrinfo(hoteinfo);
}
return lg;
}
Cette fonction initialise les champs d'une structure de type sockaddr_in6. L'adresse IP est soit transmise sous la forme d'une chaîne de caractère, soit obtenue à partir du nom DNS (FQDN (Fully qualified domain name)).
Afin de détecter si le nom correspond à une adresse IP ou bien un nom, on utilise la fonction inet_pton. Si la fonction retourne une erreur, on considère qu'il s'agit d'un nom et on retrouve l'adresse IP avec la fonction getaddrinfo.
Cette fonction retourne la taille de la structure de données ou bien 0 en cas d'erreur.
La fonction htons permet de convertir le type entier de l'ordinateur au type entier du réseau quelque soit le format utilisé par l'ordinateur.
La fonction bzero permet d'initialiser à 0 une zone mémoire.
Initialisation mode client
int InitClientSocket6(unsigned int numport,struct sockaddr_in6 *p_socket_adresse) {
int clientsocket;
int err;
clientsocket = socket(PF_INET, SOCK_STREAM, 0);
if (clientsocket == SOCK_ERR) {
perror("Erreur socket ");
return clientsocket;
}
err=connect(clientsocket,(struct sockaddr *)p_socket_adresse,sizeof(struct sockaddr_in6));
if (err == SOCK_ERR) {
perror("Erreur connect ");
return err;
}
return clientsocket;
}
En dehors de la structure de donnée sockaddr_in6, cette fonction est identique à celle utilisée avec ipv4.
Initialisation mode serveur
int InitServerSocket6(unsigned int numport, struct sockaddr_in6 *p_socket_adresse) {
int serveursocket;
int err=0;
serveursocket = socket(PF_INET, SOCK_STREAM, 0);
if (serveursocket == SOCK_ERR) {
perror("Erreur socket ");
return serveursocket;
}
int p=1;
err=setsockopt (serveursocket, SOL_SOCKET, SO_REUSEADDR, &p, sizeof(int));
if (err == SOCK_ERR) {
perror("Erreur socket opt");
return err;
}
err=bind(serveursocket, (struct sockaddr *)p_socket_adresse, sizeof(struct sockaddr_in6));
if (err == SOCK_ERR) {
perror("Erreur bind ");
return err;
}
err=listen(serveursocket, 10);
if (err == SOCK_ERR) {
perror("Erreur listen ");
return err;
}
return serveursocket;
}
En dehors de la structure de donnée sockaddr_in6, cette fonction est identique à celle utilisée avec ipv4.
Programme client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "net6common.h"
int main(int nbargs,char *args[]) {
int clientsocket;
struct sockaddr_in6 socket_adresse;
int port = PORT_DEFAUT ;
char nom[100] = NOM_DEFAUT;
char buffer[TAILLE_BUFFER];
int lg;
int taille ;
printf("connexion à %s port %d\n",nom,port);
lg = createsock(nom,port,&socket_adresse);
if (lg == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
clientsocket=InitClientSocket6(port,&socket_adresse);
if (clientsocket == SOCK_ERR) {
perror("Erreur connexion");
return EXIT_FAILURE;
}
printf("connecte\n");
if (nbargs == 2) {
strcpy(buffer,args[1]);
}
else {
strcpy(buffer,"bonjour");
}
taille = strlen(buffer);
printf("envoi : %s\n",buffer);
send(clientsocket,buffer,taille+1,0);
taille = recv(clientsocket,buffer,TAILLE_BUFFER,0);
printf("reçu : %s\n",buffer);
strcpy(buffer,"merci pour tout");
taille = strlen(buffer);
printf("envoi : %s\n",buffer);
send(clientsocket,buffer,taille+1,0);
close(clientsocket);
return EXIT_SUCCESS;
}
En dehors de la structure de donnée sockaddr_in6, ce programme est identique à celle utilisée avec ipv4.
Programme serveur
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "net6common.h"
int main(int nbargs,char *args[]) {
int serveursocket, client;
struct sockaddr_in6 socket_adresse;
socklen_t long_adr;
int port = PORT_DEFAUT;
char buffer[TAILLE_BUFFER];
int taille;
printf("connexion %d\n",port);
long_adr = createsock("::0",port,&socket_adresse);
if (long_adr == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
serveursocket = InitServerSocket6(port,&socket_adresse);
if (serveursocket == -1) {
perror("Erreur init socket ");
return EXIT_FAILURE;
}
printf("Ecoute port %d\n",port);
client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
printf("client connecte\n");
taille = recv(client,buffer,TAILLE_BUFFER,0);
printf("reçu : %s\n",buffer);
strcat(buffer," OK");
taille = strlen(buffer);
printf("envoi : %s\n",buffer);
send(client,buffer,taille+1,0);
taille = recv(client,buffer,TAILLE_BUFFER,0);
printf("reçu : %s\n",buffer);
close(client);
close(serveursocket);
return EXIT_SUCCESS;
}
En dehors de la structure de donnée sockaddr_in6, ce programme est identique à celle utilisée avec ipv4.
Client
Serveur
Initialisation mode client
int InitClientSocketUDP(unsigned int numport,struct sockaddr_in *p_socket_adresse) {
int clientsocket;
clientsocket = socket(PF_INET, SOCK_DGRAM, 0);
if (clientsocket == SOCK_ERR) {
perror("Erreur socket ");
return clientsocket;
}
return clientsocket;
}
Par rapport au mode connecté, on utilise la valeur SOCK_DGRAM, et on n'utilise plus la fonction de connexion connect.
Initialisation mode serveur
int InitServerSocketUDP(unsigned int numport, struct sockaddr_in *p_socket_adresse) {
int serveursocket;
int err=0;
serveursocket = socket(PF_INET, SOCK_DGRAM, 0);
if (serveursocket == SOCK_ERR) {
return serveursocket;
}
int p=1;
err=setsockopt (serveursocket, SOL_SOCKET, SO_REUSEADDR, &p, sizeof(int));
if (err == SOCK_ERR) {
perror("Erreur socket ");
return err;
}
err=bind(serveursocket, (struct sockaddr *)p_socket_adresse, sizeof(struct sockaddr_in ));
if (err == SOCK_ERR) {
perror("Erreur bind ");
return err;
}
return serveursocket;
}
Par rapport au mode connecté, on utilise la valeur SOCK_DGRAM, et on n'utilise plus la fonction d'écoute listen.
Programme client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
int main(int nbargs,char *args[]) {
int clientsocket;
struct sockaddr_in sock_adresse;
int port = PORT_DEFAUT ;
char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
int lg ;
int taille;
char buffer[TAILLE_BUFFER];
printf("connexion à %s:%d\n",nom,port);
lg = createsock(nom,port,&sock_adresse);
if (lg == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
clientsocket=InitClientSocketUDP(port,&sock_adresse);
if (clientsocket == SOCK_ERR) {
perror("Erreur connect");
return EXIT_FAILURE;
}
if (nbargs == 2) {
strcpy(buffer,args[1]);
}
else {
strcpy(buffer,"bonjour");
}
taille = strlen(buffer);
printf("envoi : %s\n",buffer);
int envoye = sendto(clientsocket,buffer,taille+1,0,(struct sockaddr *)&sock_adresse, lg);
printf("envoye %d\n",envoye);
close(clientsocket);
return EXIT_SUCCESS;
}
Cette fois on utilise la fonction sendto qui utilise la structure de donnée de la connexion en plus.
Programme serveur
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
int main(int nbargs,char *args[]) {
int serveursocket;
struct sockaddr_in socket_adresse;
socklen_t long_adr;
int port = PORT_DEFAUT;
char buffer[TAILLE_BUFFER];
int taille;
long_adr = createsock("0.0.0.0",port,&socket_adresse);
if (long_adr == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
serveursocket = InitServerSocketUDP(port,&socket_adresse);
if (serveursocket == -1) {
perror("Erreur socket ");
return EXIT_FAILURE;
}
printf("Ecoute port %d\n",port);
taille = recvfrom(serveursocket, buffer, TAILLE_BUFFER, 0, (struct sockaddr *)&socket_adresse, &long_adr);
printf("reçu %d : %s\n",taille,buffer);
close(serveursocket);
return EXIT_SUCCESS;
}
Il n'y a plus de fonction bloquante accept
Cette fois on utilise la fonction recvfrom qui utilise la structure de donnée de la connexion en plus. La structure socket_adresse contient les informations su client (ip et port) qui peuvent être utilisées dans la suite de l'échange par sendto
La somme de contrôle ou cheksum est la somme de tous les octets modulo 256. Si on a une suite de n octets, le calcul s'écrit :
Code C
uint8_t calculsomme(unsigned char *donnees,int taille) {
uint32_t somme = donnees[0];
for(int i=1;i<taille;i+=1) {
somme += donnees[i];
}
return (somme % 256);
}
L'octet est converti en entier 32 bits, puis l'addition est réalisée. On réalise donc des additions sur 32 bits. Le modulo est calculé sur 32 bits, puis converti sur 8 bits. Ici on applique la définition.
uint8_t calculsomme(unsigned char *donnees,int taille) {
uint8_t somme = donnees[0];
for(int i=1;i<taille;i+=1) {
somme += donnees[i];
}
return somme;
}
Tout les calculs sont fait sur 8 bits. On a donc des additions modulo 256.
Ces deux fonctions donnent le même résultat avec un calcul différent, quelle est la propriété mathématique utilisée ?
Distributivité du modulo par rapport à l'addition, également utilisée avec la preuve par neuf :
void printhex(unsigned char *buffer,int taille) {
for(int i=0;i<taille;i+=1) {
printf("%02x ",buffer[i]);
}
printf("\n");
}
La fonction de calcul de la somme de contrôle calculsomme et la fonction d'affichage d'un tableau d'octets en hexadécimal printhexsont inclus dans le fichier common.c et déclaré dans le fichier common.h
Programme client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
int main(int nbargs,char *args[]) {
struct sockaddr_in sock_adresse;
int clientsocket;
int port = PORT_DEFAUT ;
char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
int lg ;
int taille;
printf("connexion à %s port %d\n",nom,port);
lg = createsock(nom,port,&sock_adresse);
if (lg == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
clientsocket=InitClientSocket(port,&sock_adresse);
if (clientsocket == SOCK_ERR) {
perror("Erreur connect");
return EXIT_FAILURE;
}
printf("connecte\n");
if (nbargs == 2) {
strcpy((char *)message,args[1]);
}
else {
strcpy((char *)message,"bonjour");
}
taille = strlen((char *)message);
uint8_t somme = calculsomme(message,++taille);
printf("somme = %02x\n",somme);
bcopy(message,trame,taille);
trame[taille++] = somme;
printhex(trame,taille);
send(clientsocket,trame,taille,0);
close(clientsocket);
return EXIT_SUCCESS;
}
La somme de contrôle est calculée sur la chaîne de caractères en incluant le 0 de fin de chaîne. Puis elle ajoutée à la fin de la chaîne pour former la trame à transmettre.
Exemple d'affichage du résultat
connexion à localhost port 1234 connecte somme = ff 62 6f 6e 6a 6f 75 72 00 ff
Programme serveur
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
int main(int nbargs,char *args[]) {
int serveursocket, client;
struct sockaddr_in socket_adresse;
socklen_t long_adr;
int port = PORT_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
int taille;
long_adr = createsock("0.0.0.0",port,&socket_adresse);
if (long_adr == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
serveursocket = InitServerSocket(port,&socket_adresse);
if (serveursocket == -1) {
perror("Erreur socket ");
return EXIT_FAILURE;
}
printf("Ecoute port %d\n",port);
client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
printf("client connecte, ...\n");
taille = recv(client,trame,TAILLE_BUFFER,0);
printhex(trame,taille);
bcopy(trame,message,--taille);
uint8_t sommerecu = trame[taille];
bcopy(trame,message,taille);
uint8_t sommecalcule = calculsomme(message,taille);
if (sommerecu == sommecalcule) {
printf("crc %02x OK\n",sommerecu);
}
else {
printf("recu %02x, calcule %02x\n",sommerecu,sommecalcule);
}
close(client);
close(serveursocket);
return EXIT_SUCCESS;
}
On extrait le message de la trame, puis on calcule la somme de contrôle du message. On compare cette somme calculée avec le dernier octet de la trame qui contient la somme de contrôle transmise. Si ces deux sommes sont identiques, on peut dire que la transmission est sans erreur dans les limites de la redondance de cette somme de contrôle.
Exemple d'affichage du résultat
Ecoute port 1234 client connecte, ... 62 6f 6e 6a 6f 75 72 00 ff crc ff OK
Le traitement de suites binaires en faisant référence au corps de Galois à deux éléments nommé GF2 a déjà été présenté dans le chapitre logique séquentielle.
Le corps de Galois est un corps à éléments finis avec deux éléments {0,1} où l'opération d'addition est réduite à la fonction logique OU exclusif et la multiplication réduite à la fonction logique ET (fonctions décrites dans le chapitre algèbre de Boole). Cette représentation fait que les valeurs 1 et -1 sont représentés par 1.
On associe des polynômes aux suites binaires en partant soit de la puissance 0 ou bien de la puissance maximale, ainsi la suite binaire {1, 0, 1, 0, 0, 1} est représentée par le polynôme :
Les propriétés des opérations binaires donnent les résultats des opérations sur les polynômes en algèbre modulo 2 :
Représentation polynomiale
x7 | +x5 | +x3 | +x2 | +1 | x4 | +x | +1 | |||||
x7 | +x4 | +x3 | x3 | +x | +1 | |||||||
x5 | +x4 | |||||||||||
x5 | +x2 | +x | ||||||||||
x4 | +x | |||||||||||
x4 | +x | +1 | ||||||||||
0 |
Représentation binaire
1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | |||
0 | 0 | 1 | 1 | 0 | ||||||||
1 | 1 | 0 | 0 | 0 | ||||||||
1 | 0 | 0 | 1 | 1 | ||||||||
0 | 1 | 0 | 0 | 1 | ||||||||
1 | 0 | 0 | 1 | 0 | ||||||||
1 | 0 | 0 | 1 | 1 | ||||||||
0 | 0 | 0 | 0 | 0 | ||||||||
0 |
Représentation polynomiale
x7 | +x5 | +x3 | +x2 | +x | x4 | +x | +1 | |||||
x7 | +x4 | +x3 | x3 | +x | +1 | |||||||
x5 | +x4 | |||||||||||
x5 | +x2 | +x | ||||||||||
x4 | ||||||||||||
x4 | +x | +1 | ||||||||||
x | +1 |
Représentation binaire
1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | |||
0 | 0 | 1 | 1 | 0 | ||||||||
1 | 1 | 0 | 0 | 0 | ||||||||
1 | 0 | 0 | 1 | 1 | ||||||||
0 | 1 | 0 | 0 | 0 | ||||||||
1 | 0 | 0 | 0 | 0 | ||||||||
1 | 0 | 0 | 1 | 1 | ||||||||
0 | 0 | 0 | 1 | 1 | ||||||||
1 | 1 |
Le CRC ou Code de redondance Cyclique est le reste de la division de la suite binaire par un polynôme choisi dans un ensemble de polynômes prédéfinis et normalisés, dont voici un extrait pour les crc 8 bits :
Désignation | Polynôme | Valeur normale | Valeur inversée | Valeur réciproque | Valeur réciproque inversée |
---|---|---|---|---|---|
CRC-8 | 0xD5 | 0xAB | 0x57 | 0xEA | |
CRC-8-AUTOSAR | 0x2F | 0xF4 | 0xE9 | 0x97 | |
CRC-8-Bluetooth | 0xA7 | 0xE5 | 0xCB | 0xD3 | |
CRC-8-CCITT | 0x07 | 0xE0 | 0xC1 | 0x83 | |
CRC-8-Dallas | 0x31 | 0x8C | 0x19 | 0x98 | |
CRC-8-DARC | 0x39 | 0x9C | 0x39 | 0x9C | |
CRC-8-GSM-B | 0x49 | 0x92 | 0x25 | 0xA4 | |
CRC-8-SAE | 0x1D | 0xB8 | 0x71 | 0x8E | |
CRC-8-WCDMA | 0x9B | 0xD9 | 0xB3 | 0xCD |
On propose de calculer le CRC-8-SAE (0x1D) de l'octet de donnée 0xaa. Pour effectuer la division on multiplie le polynôme qui représente la donnée par x8 (ajout de 8 bits 0 à droite de la suite binaire). On va ensuite effectuer la division du polynôme
Division polynomiale dans GF2
x15 | +x13 | +x11 | +x9 | x8 | +x4 | +x3 | +x2 | +1 | ||||||||||||||||
x15 | +x11 | +x10 | +x9 | +x7 | x7 | +x5 | +x2 | +x | +1 | |||||||||||||||
x13 | +x10 | +x7 | ||||||||||||||||||||||
x13 | +x9 | +x8 | +x7 | +x5 | ||||||||||||||||||||
x10 | +x9 | +x8 | +x5 | |||||||||||||||||||||
x10 | +x6 | +x5 | +x4 | +x2 | ||||||||||||||||||||
x9 | +x8 | +x6 | +x4 | +x2 | ||||||||||||||||||||
x9 | +x5 | +x4 | +x3 | +x | ||||||||||||||||||||
x8 | +x6 | +x5 | +x3 | +x2 | +x | |||||||||||||||||||
x8 | +x4 | +x3 | +x2 | +1 | ||||||||||||||||||||
x6 | +x5 | +x4 | +x | +1 |
Division binaire dans GF2
1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 |
1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | |||||||
0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | ||||||||||||||||
1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | ||||||||||||||||
1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | ||||||||||||||||
0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | ||||||||||||||||
1 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | ||||||||||||||||
1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | ||||||||||||||||
0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | ||||||||||||||||
1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | ||||||||||||||||
1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | ||||||||||||||||
0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | ||||||||||||||||
1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | ||||||||||||||||
1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | ||||||||||||||||
0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | ||||||||||||||||
1 | 1 | 1 | 0 | 0 | 1 | 1 |
Le reste est {0,1,1,1,0,0,1,1} qui correspond au polynôme :
Code C de la fonction de calcul
unsigned crc8(unsigned char *data,int dataSize,unsigned polynome) {
unsigned crc,bit7crc;
unsigned char bitoctet;
crc=0;
for(int i=0;i<dataSize;i+=1) {
for(int k=0;k<8;k+=1) {
bitoctet = ((data[i] >> (7-k)) & 0x01);
bit7crc = (crc >> (7)) & 0x01;
crc <<= 1 ;
if ((bit7crc ^ bitoctet)==1) {
crc ^= polynome;
}
}
}
return crc & 0xff;
}
Le code C est la traduction de cet algorithme de calcul du reste de la division binaire modulo 2 de la page CRC.
Le calcul se fait octet par octet. Pour chaque octet on calcul le CRC que l'on cumule avec le CRC de l'octet précédent. Pour le premier octet le CRC vaut 0.
La masque de fin permet de ne conserver que les 8 bits du CRC.
Programme client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
int main(int nbargs,char *args[]) {
struct sockaddr_in sock_adresse;
int clientsocket;
int port = PORT_DEFAUT ;
char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
int lg ;
int taille;
printf("connexion à %s port %d\n",nom,port);
lg = createsock(nom,port,&sock_adresse);
if (lg == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
clientsocket=InitClientSocket(port,&sock_adresse);
if (clientsocket == SOCK_ERR) {
perror("Erreur connect");
return EXIT_FAILURE;
}
printf("connecte\n");
if (nbargs == 2) {
strcpy((char *)message,args[1]);
}
else {
strcpy((char *)message,"bonjour");
}
taille = strlen((char *)message);
uint8_t crc = crc8(message,++taille,0x1d);
printf("crc = %02x\n",crc);
bcopy(message,trame,taille);
trame[taille++] = crc;
printhex(trame,taille);
send(clientsocket,trame,taille,0);
close(clientsocket);
return EXIT_SUCCESS;
}
Ce programme transmet le message bonjour ou bien le message transmis en ligne de commande.
L'augmentation de la taille (++taille) permet d'intégrer le 0 de fin de chaîne dans la transmission.
trame[taille++]=crc ajoute le CC en fin de trame de données en mettant à jour la taille de la trame.
La trame qui inclut le crc est affichée en hexadécimal.
Affichage du résultat
connexion à localhost port 1234 connecte crc = e0 62 6f 6e 6a 6f 75 72 00 e0
Programme serveur
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
int main(int nbargs,char *args[]) {
int serveursocket, client;
struct sockaddr_in socket_adresse;
socklen_t long_adr;
int port = PORT_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
int taille;
long_adr = createsock("0.0.0.0",port,&socket_adresse);
if (long_adr == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
serveursocket = InitServerSocket(port,&socket_adresse);
if (serveursocket == -1) {
perror("Erreur socket ");
return EXIT_FAILURE;
}
printf("Ecoute port %d\n",port);
client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
printf("client connecte, ...\n");
taille = recv(client,trame,TAILLE_BUFFER,0);
printhex(trame,taille);
bcopy(trame,message,--taille);
uint8_t crcrecu = trame[taille];
uint8_t crccalcule = crc8(message,taille,0x1d);
if (crcrecu == crccalcule) {
printf("crc %02x OK\n",crcrecu);
}
else {
printf("recu %02x, calcule %02x\n",crcrecu,crccalcule);
}
close(client);
close(serveursocket);
return EXIT_SUCCESS;
}
Après réception bcopy(trame,message,--taille) copie la trame reçue dans message sans le crc en décrémentant la taille de 1 octet.
Le CRC reçu est stocké dans crcrecu, on calcule le CRC du message reçu : crccalcule. On vérifie que les 2 CRC sont égaux et on affiche le message correspondant.
Affichage du résultat
Ecoute port 1234 client connecte, ... 62 6f 6e 6a 6f 75 72 00 e0 crc e0 OK
La cryptographie est une technique qui permet de protéger des données en les rendant incompréhensibles pour celui qui n'a pas la clé.
Cette technique a été utilisée à travers les siècles comme par exemple, le chiffrement par décalage ou chiffrement de césar, utilisé par Jules César, est un système où une lettre est remplacée par une autre, la clé étant le nombre de lettres de décalage dans l'alphabet.
On peut également citer la machine enigma utilisée par les allemands pendant la deuxième guerre mondiale. Ce système de codage, supposé parfaitement indéchiffrable, fût pourtant déchiffré au centre de décryptage de Bletchley Park en Angleterre. Parmi les personnes ayant participé à cette opération, on compte le mathématicien Alan Turing qui est le concepteur du projet ACE (Automatic Computing Engine) que l'on peut considérer comme le premier ordinateur.
Parmi les méthodes utilisées aujourd'hui, nous présenterons deux méthodes :
Cette méthode est composée de deux étapes
Avec cette méthode les fonctions de cryptage et décryptage sont identiques.
Les fonctions RC4 sont disponibles dans la librairie openssl (prototypes dans openssl/rc>4.h, édition de lien avec la librairie crypto) :
Exemple de chiffrement RC4
Programme client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
#include <openssl/rc4.h>
int main(int nbargs,char *args[]) {
struct sockaddr_in sock_adresse;
int clientsocket;
int port = PORT_DEFAUT ;
char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
char cle[TAILLE_BUFFER];
int lg ;
int taille;
RC4_KEY key;
printf("connexion à %s port %d\n",nom,port);
lg = createsock(nom,port,&sock_adresse);
if (lg == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
clientsocket=InitClientSocket(port,&sock_adresse);
if (clientsocket == SOCK_ERR) {
perror("Erreur connect");
return EXIT_FAILURE;
}
printf("connecte\n");
if (nbargs == 2) {
strcpy((char *)message,args[1]);
strcpy(cle,"monsecret");
}
else {
if (nbargs == 3) {
strcpy((char *)message,args[1]);
strcpy(cle,args[2]);
}
else {
strcpy((char *)message,"bonjour");
strcpy(cle,"monsecret");
}
}
RC4_set_key(&key,strlen(cle),(unsigned char *)cle);
taille = strlen((char *)message);
printf("Message : ");
printhex(message,++taille);
RC4(&key,taille,(unsigned char*)message,(unsigned char *)trame);
printf("Trame chiffrée : ");
printhex(trame,taille);
send(clientsocket,trame,taille,0);
close(clientsocket);
return EXIT_SUCCESS;
}
Le programme accepte 1 argument qui est le message ou bien 2 arguments qui sont le message suivi du mot de passe secret.
Le zéro de fin de chaîne est ajouté au message avant le chiffrement. Le message et la trame chiffrée sont affichés en hexadécimal.
Affichage des résultats
connexion à localhost port 1234 connecte Message : 62 6f 6e 6a 6f 75 72 00 Trame chiffrée : d6 9d 55 1e 59 5b 6b 5b
Programme serveur
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
#include <openssl/rc4.h>
int main(int nbargs,char *args[]) {
int serveursocket, client;
struct sockaddr_in socket_adresse;
socklen_t long_adr;
int port = PORT_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
char cle[TAILLE_BUFFER];
int taille;
RC4_KEY key;
long_adr = createsock("0.0.0.0",port,&socket_adresse);
if (long_adr == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
serveursocket = InitServerSocket(port,&socket_adresse);
if (serveursocket == -1) {
perror("Erreur socket ");
return EXIT_FAILURE;
}
if (nbargs == 2) {
strcpy(cle,args[1]);
}
else {
strcpy(cle,"monsecret");
}
RC4_set_key(&key,strlen(cle),(unsigned char *)cle);
printf("Ecoute port %d\n",port);
client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
printf("client connecte, ...\n");
taille = recv(client,trame,TAILLE_BUFFER,0);
printf("Trame chiffrée : ");
printhex(trame,taille);
RC4(&key,taille,trame,message);
printf("Message : ");
printhex(message,taille);
close(client);
close(serveursocket);
return EXIT_SUCCESS;
}
Le programme accepte 1 argument qui est le mot de passe secret. Après réception, la trame est déchiffrée. La trame chiffrée et le message en clair sont affichés en hexadécimal.
Affichage des résultats
Ecoute port 1234 client connecte, ... Trame chiffrée : d6 9d 55 1e 59 5b 6b 5b Message : 62 6f 6e 6a 6f 75 72 00
AES est un chiffrement par blocs qui peuvent être par exemple 4x4 qui donnent des blocs de 16 octets.
Ce type de calcul nécessite de fonctions de cryptage et décryptage différentes.
Le chiffrement par blocs utilise plusieurs modes d'opérations, comme par exemple :
Les fonctions AES sont également disponibles dans la librairie openssl (prototype dans openssl/aes.h, édition de lien avec la librairie crypto) :
Exemple de chiffrement AES
Fonction de chiffrement
#include <stdlib.h>
#include <string.h>
#include <openssl/aes.h>
#define TAILLEBLOC 16
int AES_encode(unsigned char *message,int taille,unsigned char *trame,unsigned char *cle) {
unsigned char buffer[16];
AES_KEY key;
int nbpaquets = taille / TAILLEBLOC ;
int reste = taille % TAILLEBLOC ;
int tailletrame = nbpaquets*TAILLEBLOC + (reste!=0?TAILLEBLOC:0) ;
AES_set_encrypt_key(cle, 128, &key);
trame[0] = taille ;
int i=0;
for(i=0;i<nbpaquets;i+=1) {
bcopy(&message[i*TAILLEBLOC],buffer,TAILLEBLOC);
AES_encrypt((unsigned char *)buffer, (unsigned char *)&trame[i*TAILLEBLOC+1], &key);
}
bcopy(&message[i*TAILLEBLOC],buffer,reste);
AES_encrypt((unsigned char *)buffer, (unsigned char *)&trame[i*TAILLEBLOC+1], &key);
return tailletrame;
}
La fonction de chiffrement découpe le message en blocs de 16 octets. La clé doit avoir une taille de 16 octets.
Les blocs sont chiffrés avec l'opération ECB.
La taille de la trame (valeur de retour) est calculée en nombre d'octets multiples de 16 octets.
La taille du message est enregistrée dans la trame avant le message chiffré. En effet le chiffrement envoie des paquets de 16 octets, ce qui signifie que la taille du message ne sera pas connue du récepteur (serveur). IL faut donc transmettre cette information.
Fonction de déchiffrement
#include <stdlib.h>
#include <string.h>
#include <openssl/aes.h>
#define TAILLEBLOC 16
int AES_decode(unsigned char *trame,int taille,unsigned char *message,unsigned char *cle) {
unsigned char buffer[16];
AES_KEY key;
int taillecrypte = taille - 1 ;
int nbpaquets = taillecrypte / TAILLEBLOC ;
int taillemsg = trame[0] ;
AES_set_decrypt_key(cle, 128, &key);
int i=0;
for(i=0;i<nbpaquets;i+=1) {
bcopy(&trame[i*TAILLEBLOC+1],buffer,TAILLEBLOC);
AES_decrypt(buffer,&message[i*TAILLEBLOC], &key);
}
return taillemsg;
}
La taille de la trame chiffrée est un multiple de 16 octets + 1 octet pour la taille du message qui se situe en début de trame (0).
La trame chiffrée, qui démarre à l'octet 1, est découpée en paquets des 16 octets, qui sont déchiffrés. Les paquets déchiffrés sont ensuite concaténés pour former le message complet.
Programme client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
int main(int nbargs,char *args[]) {
struct sockaddr_in sock_adresse;
int clientsocket;
int port = PORT_DEFAUT ;
char nom[TAILLE_NOM_IP] = NOM_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
unsigned char cle[TAILLE_BUFFER];
int lg ;
int taille;
int tailletrame;
printf("connexion à %s port %d\n",nom,port);
lg = createsock(nom,port,&sock_adresse);
if (lg == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
clientsocket=InitClientSocket(port,&sock_adresse);
if (clientsocket == SOCK_ERR) {
perror("Erreur connect");
return EXIT_FAILURE;
}
printf("connecte\n");
if (nbargs == 2) {
strcpy((char *)message,args[1]);
strcpy((char *)cle,"0123456789ABCDEF");
}
else {
if (nbargs == 3) {
strcpy((char *)message,args[1]);
strcpy((char *)cle,args[2]);
}
else {
strcpy((char *)message,"0123456789ABCDEF");
strcpy((char *)cle,"0123456789ABCDEF");
}
}
printf("cle : ");
printhex((unsigned char *)cle,strlen((char *)cle));
printf("Message : %s\n",message);
taille = strlen((char *)message);
printf("Message en clair (%d octets) : ",taille);
printhex(message,taille);
tailletrame = AES_encode(message,taille,trame,cle);
printf("Trame chiffrée (%d octets) : ",tailletrame);
printhex((unsigned char *)&trame[1],tailletrame++);
send(clientsocket,trame,tailletrame,0);
close(clientsocket);
return EXIT_SUCCESS;
}
La passphrase est de 16 octets. Elle peut être saisie en ligne de commande après le message.
Le zéro de fin de chaîne n'est pas transmis. La passphrase, le message en clair ainsi que le message chiffré sont affichés en hexadécimal avant d'être envoyés.
Affichage des résultats
connexion à localhost port 1234 connecte cle : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 Message : 0123456789ABCDEF Message en clair (16 octets) : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 Trame chiffrée (16 octets) : 8d 83 5d 0c ff d8 ba d5 85 fe 8b 42 94 c4 21 88
Programme serveur
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "netcommon.h"
#include "common.h"
int main(int nbargs,char *args[]) {
int serveursocket, client;
struct sockaddr_in socket_adresse;
socklen_t long_adr;
int port = PORT_DEFAUT;
unsigned char trame[TAILLE_BUFFER];
unsigned char message[TAILLE_BUFFER];
unsigned char cle[TAILLE_BUFFER];
int taille;
int taillemsg;
long_adr = createsock("0.0.0.0",port,&socket_adresse);
if (long_adr == 0) {
perror("Erreur nom machine ou ip");
return EXIT_FAILURE;
}
serveursocket = InitServerSocket(port,&socket_adresse);
if (serveursocket == -1) {
perror("Erreur socket ");
return EXIT_FAILURE;
}
if (nbargs == 2) {
strcpy((char *)cle,args[1]);
}
else {
strcpy((char *)cle,"0123456789ABCDEF");
}
printf("cle : ");
printhex((unsigned char *)cle,strlen((char *)cle));
printf("Ecoute port %d\n",port);
client = accept (serveursocket, (struct sockaddr *)&socket_adresse, &long_adr);
printf("client connecte, ...\n");
taille = recv(client,trame,TAILLE_BUFFER,0);
int taillecrypte = taille - 1;
printf("Trame chiffrée (%d octets) : ",taillecrypte);
printhex(&trame[1],taillecrypte);
taillemsg = AES_decode(trame,taille,message,cle);
printf("Message en clair (%d octets) : ",taillemsg);
printhex(message,taillemsg);
message[taillemsg] = 0;
printf("Message : %s\n",message);
close(client);
close(serveursocket);
return EXIT_SUCCESS;
}
La clé de 16 octets peut être saisie en ligne de commande.
Après réception, la trame chiffrée est affichée en hexadécimal.
Après décodage, le message en clair est affiché en hexadécimal, puis sous la forme texte, cela est rendu possible par l'ajout d'un zéro à la fin du message reçu.
Affichage des résultats
cle : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 Ecoute port 1234 client connecte, ... Trame chiffrée (16 octets) : 8d 83 5d 0c ff d8 ba d5 85 fe 8b 42 94 c4 21 88 Message en clair (16 octets) : 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 Message : 0123456789ABCDEF
Pour la conception de programmes en python, voir le chapitre sur le langage python.
L'accès au réseau en python utilise le module socket décrit dans cette documentation.
Cela se fait avec la fonction socket.getaddrinfos(nom,port,famille,type) avec nom qui peut être le nom DNS (FQDN (Fully qualified domain name))ou bien l'adresse IP, port qui est le numéro de port, famille qui est socket.AF_INET ou socket.AF_INET6 et type qui peut être socket.SOCK_STREAM ou socket.SOCK_DGRAM. Cette fonction retourne une liste de données où chaque élément contient l'ensemble des informations :
Exemple avec python en ligne de commande :
>>> import socket >>> socket.getaddrinfo("localhost",80,socket.AF_INET,socket.SOCK_DGRAM) [(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('127.0.0.1', 80))]
On utilise les fonctions du module socket
Programme client
import socket
nom="localhost"
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
message = args[1]
else:
message = "bonjour"
print(f"connexion à {nom} port {port}")
parametres = socket.getaddrinfo(nom, port)
ip=parametres[0][4][0]
numport=parametres[0][4][1]
print(f"{ip} , {numport}")
clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(serveur)
print("connecte")
donnees = bytes(message, 'utf-8')
print(f"envoi : {message}")
clientsocket.send(donnees)
donnees = clientsocket.recv(1024)
message = donnees.decode('utf-8')
print(f"reçu : {message}")
message = "merci pour tout"
donnees = bytes(message, 'utf-8')
print(f"envoi : {message}")
clientsocket.send(donnees)
clientsocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La connexion se fait sur localhost avec le port 1234. La fonction connect utilise directement le nom DNS (FQDN (Fully qualified domain name)). Ce qui fait que l'utilisation de getaddrinfo n'est pas nécessaire. Elle est présente uniquement pour montrer un exemple d'utilisation.
La conversion chaîne vers tableau d'octets se fait avec le constructeur avec le message ainsi que le type d'encodage du message.
La conversion tableau d'octets vers chaîne de caractères se fait avec la méthode decode de la classe bytes en précisant le type d'encodage des caractères.
Affichage du résultat
connexion à localhost port 1234 127.0.0.1 , 1234 connecte envoi : bonjour reçu : bonjour OK envoi : merci pour tout
Programme serveur
import socket
nom=""
port=1234
serveur=(nom,port)
def main(args):
serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveursocket.bind(serveur)
serveursocket.listen(1)
print(f"écoute : {port}")
client,addresse = serveursocket.accept()
print("client connecté, ...")
donnees = client.recv(1024)
message = donnees.decode('utf-8')
print(f"reçu : {message}")
reponse = message + " OK"
donnees = bytes(reponse, 'utf-8')
print(f"envoi : {message}")
client.send(donnees)
donnees = client.recv(1024)
message = donnees.decode('utf-8')
print(f"reçu : {message}")
client.close()
serveursocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La méthode bind utilise une liste serveur dans laquelle l'adresse est laissée vide pour accepter un client quelque soit son adresse IP.
La méthode accept retourne une liste qui contient l'identifiant de connexion utilisé pour l'échange de données ainsi que l'adresse IP du client.
Affichage du résultat
écoute : 1234 client connecté, ... reçu : bonjour envoi : bonjour reçu : merci pour tout
Programme client
import socket
nom="localhost"
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
message = args[1]
else:
message = "bonjour"
print(f"{nom} , {port}")
parametres = socket.getaddrinfo(nom, port,socket.AF_INET6)
ip=parametres[0][4][0]
numport=parametres[0][4][1]
print(f"{ip} , {numport}")
clientsocket=socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
clientsocket.connect(serveur)
print("connecte")
donnees = bytes(message, 'utf-8')
print(f"envoi : {message}")
clientsocket.send(donnees)
donnees = clientsocket.recv(1024)
message = donnees.decode('utf-8')
print(f"reçu : {message}")
message = "merci pour tout"
donnees = bytes(message, 'utf-8')
print(f"envoi : {message}")
clientsocket.send(donnees)
clientsocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
Il n'y a pas de différence avec la version ipv4 en dehors du paramètre socket.AF_INET6.
Affichage du résultat
localhost , 1234 ::1 , 1234 connecte envoi : bonjour reçu : bonjour OK envoi : merci pour tout
Il n'y a pas de différence avec la version ipv4 en dehors du paramètre socket.AF_INET6.
Programme serveur
import socket
nom=""
port=1234
serveur=(nom,port)
def main(args):
serveursocket=socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
serveursocket.bind(serveur)
serveursocket.listen(1)
print(f"écoute : {port}")
client,addresse = serveursocket.accept()
print("client connecté, ...")
donnees = client.recv(1024)
message = donnees.decode('utf-8')
print(f"reçu : {message}")
reponse = message + " OK"
donnees = bytes(reponse, 'utf-8')
print(f"envoi : {reponse}")
client.send(donnees)
donnees = client.recv(1024)
message = donnees.decode('utf-8')
print(f"reçu : {message}")
client.close()
serveursocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
Affichage du résultat
écoute : 1234 client connecté, ... reçu : bonjour envoi : bonjour OK reçu : merci pour tout
On utilise les fonctions du module socket
Programme client
import socket
nom="localhost"
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
message = args[1]
else:
message = "bonjour"
print(f"connexion à {nom} port {port}")
parametres = socket.getaddrinfo(nom, port)
ip=parametres[0][4][0]
numport=parametres[0][4][1]
print(f"{ip} , {numport}")
clientsocket=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
donnees = bytes(message, 'utf-8')
print(f"envoi : {message}")
clientsocket.sendto(donnees,(ip,port))
donnees,adresse = clientsocket.recvfrom(1024)
message = donnees.decode('utf-8')
print(f"reçu de {adresse}")
taille = len(donnees)
print(f"reçu {taille}: {message}")
clientsocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
Il n'y a plus de connexion, mais uniquement l'initialisation suivi des méthodes de lecture et d'écriture qui contiennent les paramètres IP et port du serveur.
La fonction de lecture renvoie les données ainsi que l'adresse IP et le port de l'expéditeur.
Affichage du résultat
connexion à localhost port 1234 127.0.0.1 , 1234 envoi : bonjour reçu de ('127.0.0.1', 1234) reçu 10: bonjour OK
Programme serveur
import socket
nom=""
port=1234
serveur=(nom,port)
def main(args):
serveursocket=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
serveursocket.bind(serveur)
print(f"écoute : {port}")
donnees,adresse = serveursocket.recvfrom(1024)
message = donnees.decode('utf-8')
print(f"reçu de {adresse}")
taille = len(donnees)
print(f"reçu {taille}: {message}")
reponse = message + " OK "
donnees = bytes(reponse, 'utf-8')
print(f"envoi : {reponse}")
serveursocket.sendto(donnees,adresse)
serveursocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
Après l'assignation, il n'y a plus les méthodes utilisées dans la connexion, mais seulement les méthodes de lecture et écriture.
L'adresse IP et le numéro de port du client (expéditeur) est utilisé par la fonction sendto pour la réponse.
Affichage du résultat
écoute : 1234 reçu de ('127.0.0.1', 50356) reçu 7: bonjour envoi : bonjour OK
La somme est calculée avec la fonction sum de python.
Programme client
import socket
nom="localhost"
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
message = args[1]
else:
message = "bonjour"
print(f"{nom} , {port}")
parametres = socket.getaddrinfo(nom, port)
ip=parametres[0][4][0]
numport=parametres[0][4][1]
print(f"{ip} , {numport}")
clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(serveur)
print("connecte")
donnees = bytearray(message, 'utf-8')
somme = sum(donnees) % 256
print(f"somme={hex(somme)}")
donnees.append(somme)
print(donnees)
clientsocket.send(donnees)
clientsocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La checksum est calculée sur les données puis ajoutée à la fin des données.
Affichage du résultat
localhost , 1234 127.0.0.1 , 1234 connecte somme=0xff bytearray(b'bonjour\xff')
Programme serveur
import socket
nom=""
port=1234
serveur=(nom,port)
def main(args):
serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveursocket.bind(serveur)
serveursocket.listen(1)
print(f"écoute : {port}")
client,addresse = serveursocket.accept()
print("client connecte, ...")
trame = client.recv(1024)
donnees = bytearray(trame)
sommerecu = donnees.pop()
sommecalcule = sum(donnees) % 256
if (sommecalcule == sommerecu):
print(f"somme={hex(sommerecu)} OK")
else:
print(f"reçu {hex(sommerecu)}, calcule {hex(sommecalcule)}")
print(donnees)
client.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La checksum est extraite des données reçues. Un autre checksum est calculée à partir des données reçues. Ces deux valeurs sont comparées afin d'afficher un message d'intégrité des données reçues.
Affichage du résultat
écoute : 1234 client connecte, ... somme=0xff OK bytearray(b'bonjour')
Le calcul du crc utilise le module crcmod décrit dans cette documentation
On définit la fonction de calcul de CRC avec la fonction :
crcmod.mkCrcFun(polynome, ...) qui retourne un lien vers la fonction de calcul de CRC, polynome est la valeur hexadécimale du CRC incluant tous les bits. Les autres paramètres définissent le type de polynôme :
Programme client
import socket
import crcmod
nom="localhost"
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
message = args[1]
else:
message = "bonjour"
fonction_crc = crcmod.mkCrcFun(0x11d, initCrc=0, rev=False)
print(f"{nom} , {port}")
parametres = socket.getaddrinfo(nom, port)
ip=parametres[0][4][0]
numport=parametres[0][4][1]
print(f"{ip} , {numport}")
clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(serveur)
print("connecte")
donnees = bytearray(message, 'utf-8')
crc = fonction_crc(donnees)
print(f"crc={hex(crc)}")
donnees.append(crc)
print(donnees)
clientsocket.send(donnees)
clientsocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La fonction crcmod.mkCrcFun créé la fonction de calcul fonction_crc avec le polynôme :
Le crc calculé est ajouté à la fin du message.
Affichage du résultat
localhost , 1234 127.0.0.1 , 1234 connecte crc=0x64 bytearray(b'bonjourd')
Programme serveur
import socket
import crcmod
nom=""
port=1234
serveur=(nom,port)
def main(args):
fonction_crc = crcmod.mkCrcFun(0x11d, initCrc=0, rev=False)
serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveursocket.bind(serveur)
serveursocket.listen(1)
print(f"écoute : {port}")
client,addresse = serveursocket.accept()
print("client connecte, ...")
trame = client.recv(1024)
donnees = bytearray(trame)
crcrecu = donnees.pop()
crccalcule = fonction_crc(donnees)
if (crccalcule == crcrecu):
print(f"crc={hex(crcrecu)} OK")
else:
print(f"reçu {hex(crcrecu)}, calcule {hex(crccalcule)}")
print(donnees)
client.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La fonction de calcul du crc est identique à celle du client.
On extrait le CRC et le message des données reçues, puis on calcule le crc du message. On compare les deux CRC afin de vérifier l'intégrité des données.
Affichage du résultat
écoute : 1234 client connecte, ... crc=0x64 OK bytearray(b'bonjour')
On utilise le module ARC4 de Crypto.Cipher décrit dans cette documentation.
Le codage se fait en deux étapes :
Programme client
import socket
from Crypto.Cipher import ARC4
nom="localhost"
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
message = args[1]
cle = "monsecret"
elif (len(args)==3):
message = args[1]
cle = args[2]
else:
message = "bonjour"
cle = "monsecret"
chiffrage = ARC4.new(bytes(cle,"utf-8"))
print(f"{nom} , {port}")
parametres = socket.getaddrinfo(nom, port)
ip=parametres[0][4][0]
numport=parametres[0][4][1]
print(f"{ip} , {numport}")
clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(serveur)
print("connecte")
donnees = bytes(message, 'utf-8')
print(donnees)
trame = chiffrage.encrypt(donnees)
print(trame)
clientsocket.send(trame)
clientsocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La clé fournie sous la forme d'une chaîne de caractères est transformée en tableau d'octets. L'objet chiffrage crée va permettre de chiffrer le message en clair en utilisant la méthode chifrage.encrypt(donnees).
Ensuite la trame est transmise.
Affichage du résultat
localhost , 1234 127.0.0.1 , 1234 connecte b'bonjour' b'\xd6\x9dU\x1eY[k'
Programme serveur
import socket
from Crypto.Cipher import ARC4
nom=""
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
cle = args[1]
else:
cle = "monsecret"
chiffrage = ARC4.new(bytes(cle,"utf-8"))
serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveursocket.bind(serveur)
serveursocket.listen(1)
print("écoute : " + str(port))
client,addresse = serveursocket.accept()
trame = client.recv(1024)
print(f"trame chiffrée : {trame}")
donnees = chiffrage.decrypt(trame)
message = donnees.decode('utf-8')
print(f"message : {donnees}")
client.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La trame chiffrée reçue est déchiffrée avec la méthode chiffrage.decrypt(trame), elle retourne le message en clair.
Affichage du résultat
écoute : 1234 trame chiffrée : b'\xd6\x9dU\x1eY[k' message : b'bonjour'
On utilise le module AES de Crypto.Cipher décrit dans cette documentation.
Le codage se fait en deux étapes :
Fonction chiffrement
from Crypto.Cipher import AES
TAILLEBLOC=16
def AESencode(message,cle):
donnees = bytes(message, 'utf-8')
taille = len(donnees)
nbpaquets = taille // TAILLEBLOC
reste = taille % TAILLEBLOC
trame = bytearray([taille])
chiffrage = AES.new(bytes(cle,"utf-8"))
for i in range(nbpaquets):
blocdonnees = donnees[i*TAILLEBLOC:(i+1)*TAILLEBLOC]
blocchiffre = chiffrage.encrypt(blocdonnees)
trame = trame + blocchiffre
if (reste != 0):
blocdonnees = donnees[nbpaquets*TAILLEBLOC:nbpaquets*TAILLEBLOC+reste] + bytes(TAILLEBLOC-reste)
blocchiffre = chiffrage.encrypt(blocdonnees)
trame = trame + blocchiffre
return trame
Le message est découpé en blocs de 16 octets. Le dernier bloc est complété par des 0 afin d'obtenir un bloc de 16 octets.
La trame créée commence par la valeur de la taille du message en clair qui est suivie du message crypté.
Fonction déchiffrement
from Crypto.Cipher import AES
TAILLEBLOC=16
def AESdecode(trame,cle):
tramemodifiee = bytes(trame)
taillemsg = tramemodifiee[0]
trame = bytes(tramemodifiee[1:])
taille = len(trame)
nbpaquets = taille // TAILLEBLOC
chiffrage = AES.new(bytes(cle,"utf-8"))
donnees = bytearray()
for i in range(nbpaquets):
blocdonnees = trame[i*TAILLEBLOC:(i+1)*TAILLEBLOC]
blocdechiffre = chiffrage.decrypt(blocdonnees)
donnees = donnees + blocdechiffre
donneesmessage = donnees[0:taillemsg]
return donneesmessage
La taille de la trame reçue est un multiple de 16 octets + 1 octet. Après avoir sauvé la taille du message, Cette trame est découpée en blocs de 16 octets. Chaque bloc est déchiffré, puis concaténé avec le précédent afin de former la trame en clair.
Le message en clair est extrait des données déchiffrées en utilisant la taille du message reçue.
Programme client
import socket
nom="localhost"
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
message = args[1]
cle = "0123456789ABCDEF"
elif (len(args)==3):
message = args[1]
cle = args[2]
else:
message = "0123456789ABCDEF"
cle = "0123456789ABCDEF"
chiffrage = AES.new(bytes(cle,"utf-8"))
print(f"{nom} , {port}")
parametres = socket.getaddrinfo(nom, port)
ip=parametres[0][4][0]
numport=parametres[0][4][1]
print(f"{ip} , {numport}")
clientsocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(serveur)
taille = len(message)
print("connecte")
print(f"Message ({taille} octets ) : {message}")
trame = AESencode(message,cle)
taille = len(trame)
print(f"Trame ({taille} octets ) : {trame}")
clientsocket.send(trame)
clientsocket.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
Le message est chiffré puis transmis
Affichage du résultat
localhost , 1234 127.0.0.1 , 1234 connecte Message (16 octets ) : 0123456789ABCDEF Trame (17 octets ) : bytearray(b'\x10\x8d\x83]\x0c\xff\xd8\xba\xd5\x85\xfe\x8bB\x94\xc4!\x88')
Programme serveur
import socket
nom=""
port=1234
serveur=(nom,port)
def main(args):
if (len(args)==2):
cle = args[1]
else:
cle = "0123456789ABCDEF"
chiffrage = AES.new(bytes(cle,"utf-8"))
serveursocket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveursocket.bind(serveur)
serveursocket.listen(1)
print(f"écoute : {port}")
client,addresse = serveursocket.accept()
trame = client.recv(1024)
taille = len(trame[1:])
print(f"Trame chiffrée ({taille} octets ): {trame}")
donnees = AESdecode(trame,cle)
taillemsg = trame[0]
print(f"donnees ({taillemsg} octets) : {donnees}")
print(donnees.decode('utf-8'))
client.close()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La trame chiffrée reçue est déchiffrée, puis le message en clair est affiché.
Affichage du résultat
écoute : 1234 Trame chiffrée (16 octets ): b'\x10\x8d\x83]\x0c\xff\xd8\xba\xd5\x85\xfe\x8bB\x94\xc4!\x88' donnees (16 octets) : bytearray(b'0123456789ABCDEF') 0123456789ABCDEF
On a écrit des codes en C et en python. Les programmes clients et serveurs ne sont pas sur le même système, ce qui fait qu'il peut y avoir un programme client écrit en C et un programme serveur écrit en python ou vice-versa.
Afin de tester la robustesse des programmes, on teste chaque client en C avec le serveur correspondant en python, ensuite on inverse le procédé avec un client en python et un serveur en C. Tous ces programmes doivent fonctionner correctement, ce qui est le cas des programmes donnés ici.
Ce test n'est pas suffisant, mais il permet de vérifier la rigueur des formats de transmission.