III/ DOMAINE INTERNET
Les sockets du domaine Internet appartiennent à la famille AF_INET. Afin d'être visible de l'extérieur du processus, ce qui est essentiel pour recevoir des données, la socket est attachée à un port de la machine. De cette façon, un grand nombre de processus peuvent écouter en même temps sur une même interface réseau car les trames contiennent un port de destination pour viser le processus voulu sur une machine donnée. L'intermédiaire entre un processus et un port est la socket AF_INET, qui va gérer la réception et l'émission de données.
Pour faciliter la compréhension
de ces notions, il suffit de prendre un exemple de la vie courante:
Ce qui représente le réseau I.P. est une rue, les maisons sont des machines,
et les habitants dans chaque maison sont les processus.
Lorsque qu'un habitant veut envoyer une lettre à un autre, il va indiquer sur
l'enveloppe:
- le nom de la rue, ce qui correspond à une partie du
réseau.
- Le numéro de la maison dans la rue, ce qui correspond à l'adresse I.P. de
la machine. Celui-ci doit donc être unique sur un réseau.
- Le nom de la personne destinatrice dans cette maison car il
est évident qu'il n'y a pas une boîte aux lettres par personnes, ceci
correspond au numéro de port du processus. Ici la
boîte aux lettres correspond à une interface réseau dans un ordinateur.
On ne peut pas faire plus simple ni plus pratique.
De plus, si l'expéditeur désire que
le destinataire lui réponde, il va indiquer derrière l'enveloppe ses propres
coordonnées. Les protocoles de la couche transport de T.C.P. / I.P. ne vous
donne pas le choix, l'adresse de l'émetteur est forcément indiquer. Alors
l'A.P.I. socket le fait pour vous de manière transparente, sans vous demander
vos coordonnées puisqu'elles sont évidentes... Faudra donc faire autrement
qu'avec les sockets pour envoyer des données avec une fausse adresse !.
1/ Mode déconnecté
Une socket AF_INET en mode déconnecté correspond au protocole U.D.P. Ce protocole est une des deux couches transport possible qui repose sur la couche réseau I.P.
+ Rapide et léger en trafic.
+ Souple.
+ Possibilité d’envoyer un paquet à plusieurs destinataires (multicast).
- Non fiable, possibilité de perdre des paquets sans le savoir.
- Ordre des paquets peut changer à l’arrivé (si différent routage par paquet).
Architectures d'un client et d'un serveur communiquant grâce aux protocole U.D.P. en C
EXEMPLES DE CODE COURANT EN U.D.P.
FONCTION D’OUVERTURE ET D’ATTACHEMENT D’UNE SOCKET U.D.P.
Pour améliorer la clarté du programme et comme c'est toujours la même chose, il est préférable de faire une fonction qui prenne juste le numéro de port local, pour créer la socket et l'attacher à ce dernier. Il ne faut pas oublier de fermer les sockets lorsque l'on quitte le programme, même si c'est sur une erreur car sinon le port reste ouvert et inutilisable.
int ouvreSocket(int port)
{
int skD;
size_t tailleAd;
struct sockaddr_in adLocale;
adLocale.sin_family = AF_INET; /* Type de la socket (TCP/IP) */
adLocale.sin_port = htons(port); /* Affectation du port local */
adLocale.sin_addr.s_addr = htonl(INADDR_ANY); /* Identificateur de l'hote */
skD = socket(AF_INET, SOCK_DGRAM, 0);
/* Créé socket UDP (déconnectée) */
if(skD == -1)
{
perror("Erreur lors de la création de la socket\n");
return -1;
}
tailleAd = sizeof(adLocale);
retVal = bind(skD, (struct sockaddr*) &adLocale, tailleAd); /* Attache
socket */
if(retVal == -1)
{
perror("Erreur lors du bind\n");
close(skD);
return -1;
}
return skD;
}
ENVOI D'UN LONG
Voici les quelques lignes qu'il
faut taper pour envoyer dans un datagramme un nombre de type long. Il ne
faut pas oublier de le mettre au format réseau avant de l'émettre pour qu'il
n'y ait pas d'erreur à la réception. Pour envoyer un datagramme, il faut
utiliser la fonction sendto, qui prend en argument un pointeur sur une
structure de type struct sockaddr_in, castée en struct sockaddr.
Celle-ci contient les coordonnées réseau du destinataire, ici un serveur
qui va travailler avec le nombre pour le renvoyer au client.
long nb;
long nbNet;
size_t tailleAd;
struct hostent* infosServeur = NULL;
struct sockaddr_in adServeur; /* Structure de l’adresse du serveur */
infosServeur = gethostbyname(argv[1]); /* Récupère infos du serveur */
adServeur.sin_family = infosServeur->h_addrtype; /* Type de socket serveur */
adServeur.sin_port = htons(portServeur);
/* Port du serveur */
memcpy(&adServeur.sin_addr, infosServeur->h_addr, infosServeur->h_length);
skDesc = ouvreSocket(PORTLOCAL);
nbNet = htonl(nb); /* Format machine -> format réseau */
tailleAd = sizeof(adServeur);
retVal = sendto(skDesc, &nbNet, sizeof(long), 0,
(struct sockaddr*) &adServeur, tailleAd);
if(retVal == -1)
{
perror("Erreur lors du sendto\n");
close(skDesc);
return -1;
}
RECEPTION D'UN LONG
Et maintenant le code pour recevoir une variable de type long. Par défaut la lecture d'une socket est bloquante. Donc le programme reste bloqué à la ligne du recvfrom jusqu'à ce qu'un datagramme arrive. Cette fonction est comme sendto, elle prend aussi l'adresse d'une structure de type struct sockaddr_in. Celle-ci est vide et c'est recvfrom qui va remplir cette structure automatiquement avec les coordonnées de l'émetteur. Le programme sort de la fonction recvfrom lorsque qu'elle a lu dans la socket le nombre d'octets indiqué en troisième argument. Cela veut dire que l'on doit toujours savoir à l'avance la taille des données utiles à extraire du datagramme. Maintenant il appartient à chacun d'établir son propre protocole de communication entre les processus. Pour cela, on peut par exemple convenir d'une taille fixe des messages. Une autre solution, meilleur, consiste a utiliser le début du message pour indiquer la taille totale du message. En effet, cette taille sera toujours une variable de type long (ou short si vous voulez !, l'important est que ce soit fixe), alors il suffit au récepteur de lire dans la socket le premier long du tampon en mode MSG_PEEK (option pour lire les données sans les retirer du tampon de réception), d'allouer une variable avec la bonne taille et d'extraire du tampon la bonne taille du datagramme. Pour faire ça, j'utilise la fonction memcpy. En effet celle-ci copie des données dans un buffer de type char* sans rajouter le caractère '\0', contrairement à strcpy. En plus, on peut facilement choisir l'endroit dans le buffer où on copie les octets en spécifiant le début du buffer, plus l'offset désiré. Chaque nombre devra être reconverti en format machine.
Il est très déconseillé d'envoyer la taille du prochain message dans un message de taille fixe car en mode déconnecté l'ordre des messages peut changer à la réception ou un des deux messages peut ne jamais arriver. De plus, si plusieurs processus sont susceptibles d'envoyer un datagramme au récepteur, cela devient très compliqué de gérer la réception et de ne pas tout mélanger.
sizeof(long)
Taille buffer - sizeof(long)
Taille buffer
long nb;
long nbNet;
size_t tailleAd;
struct hostent* infosClient = NULL;
struct sockaddr_in adClient;
/* Structure de l’adresse du client */
unsigned long adresseClient;
retVal = recvfrom(skDesc, &nbNet, sizeof(long), 0,
(struct sockaddr*) &adClient, &tailleAd);
if(retVal == -1)
{
perror("Erreur lors du recvfrom\n");
close(skDesc);
return -1;
}
adresseClient = adClient.sin_addr.s_addr; /* Adresse IP codée en ulong */
infosClient = gethostbyaddr((char*) &adresseClient, sizeof(long), AF_INET);
printf("*** INFORMATIONS CLIENT ***\n");
printf("Nom: %s \n", infosClient->h_name);
printf("Adresse: %s \n", inet_ntoa(adClient.sin_addr));
printf("Port: %d\n", ntohs(adClient.sin_port));
nb = ntohl(nbNet); /* Format réseau --> format machine */
printf("### Nombre reçu: %ld\n", nb);
ENVOI EN BROADCAST
Un grand avantage de l'U.D.P. est de pouvoir adresser un message à plusieurs processus. En broadcast, c'est simple, toute les machines reçoivent le message. Si elles ont un processus qui est attaché au port spécifié, alors il le reçoit. L'avantage est qu'on émet un seul datagramme sur le réseau , à destination de toutes les applications qui écoutent sur le port spécifié. Cela fait donc beaucoup moins de trafic physique que de l'envoyer à chaque processus. De plus, on n'a pas besoin de connaître le nombre de destinataires, ni leur adresse I.P. Dans certain cas c'est donc pratique. Seulement par défaut le broadcast n'est pas autorisé. Il faut donc mettre l'option en marche sur la socket grâce à setsockopt. Il faut aussi saisir l'adresse de broadcast qui est l'adresse du réseau suivi de 1 à la place de la partie adresse machine. Cette saisie se fait ici sur la ligne de commande. Il suffit ensuite de convertir l'adresse I.P. de broadcast du format char* (exemple "192.255.255.255") vers le format unsigned long, utilisable par les fonctions. C'est le rôle de la fonction inet_addr. Ensuite, le reste est classique. Il suffit déclarer une structure de type struct sockaddr_in et de renseigner l'adresse de destination (ici de broadcast), le port de diffusion et la famille de la socket des récepteurs.
int on;
int portDiff;
struct sockaddr_in adDiffusion;
unsigned long adDiff;
adDiff = inet_addr(argv[2]);
/* Adresse -> broadcast */
printf("Adresse de broadcast = 0x%lX\n", adDiff);
adDiffusion.sin_addr.s_addr = adDiff; /* Identificateur de l'hote */
adDiffusion.sin_family = AF_INET; /* Type de la socket du serveur */
adDiffusion.sin_port = htons(portDiff); /* Port des serveurs */
tailleAd = sizeof(adDiffusion);
/* Met la socket en mode broadcast */
on = 1;
setsockopt(skDesc, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
sendto(skDesc, &nbNet, sizeof(long), 0, (struct sockaddr*) &adDiffusion,
tailleAd);
2/ Mode connecté
Il permet une communication par flux entre deux processus reliés par un circuit virtuel fiable. C’est le protocole T.C.P. Le serveur ouvre une socket serveur qui attend des connexions. Dès qu’il y en a une, la fonction accept (bloquante) retourne le descripteur d’une socket de service pour communiquer avec client. Cela veut dire que si le serveur délègue à un processus fils le traitement de la requête du client, le serveur peut à nouveau écouter la socket serveur pour accepter un autre client. Cela s'appelle un serveur concurrent car il traite plusieurs clients à la fois. Une connexion virtuel s'établit entre chaque client accepté et le serveur. La couche transport T.C.P. s'occupe ensuite toute seul de faire en sorte que le processus voit un canal fiable full-duplex similaire à deux tubes système (pipe) à partir des couches inférieures qui ne fonctionnent pas du tout comme ça. Pour cela T.C.P. rajoute donc beaucoup de trafic sur le réseau, inutile à la couche application (par exemple des accusés de réception, des demandes de réémission etc...).
+ Fiable.
+ Ordonné.
- Consommateur de trafic.
- Pas de multi-destinataires.
Architectures d'un serveur et d'un client communiquant grâce aux protocole T.C.P. en C
EXEMPLES DE CODE COURANT EN T.C.P.
FONCTION DE CONNEXION A UN SERVEUR (CLIENT)
Le client doit d'abord créer une socket locale. Il doit ensuite faire une demande de connexion à la socket du serveur. Pour cela, il lui faut le nom ou l'adresse I.P. de la machine et le numéro de port du programme serveur. A noter que si le client n'attache pas sa socket explicitement, le système le fera pour lui sur le premier port libre car le numéro n'a pas d'importance, étant donné qu'une fois connecté, un canal est établit entre le client et le serveur et que celui-ci est dénommable par le descripteur de la socket locale. Cela devient donc exactement pareil que n'importe quel flux, d'où l'utilisation des fonctions read, send, select ...
int connecteSocket(int portLocal, char* nomServeur, int portServeur)
{
int skD;
size_t tailleAd;
struct hostent* infosServeur; /* Informations du serveur */
struct sockaddr_in adLocale;
struct sockaddr_in adServeur; /* Coordonnées du serveur (nom, port...) */
adLocale.sin_family = AF_INET; /* Type de la socket (TCP/IP) */
adLocale.sin_port = htons(portLocal);
/* Affectation du port local */
adLocale.sin_addr.s_addr = htonl(INADDR_ANY); /* Identificateur de l'hote */
skD = socket(AF_INET, SOCK_STREAM, 0);
/* Créé socket TCP */
if(skD == -1)
{
perror("Erreur lors de la création de la
socket\n");return -1;
}
tailleAd = sizeof(adLocale);
retVal = bind(skD, (struct sockaddr*) &adLocale, tailleAd);
if (retVal == -1)
{
perror("Erreur lors du bind\n");
close(skD);
return -1;
}
infosServeur = gethostbyname(nomServeur); /* Récupère infos du serveur */
if(infosServeur == NULL)
{
perror("Erreur lors de la récupération des
informations serveur\n");
close(skD);
return -1;
}
adServeur.sin_family = infosServeur->h_addrtype; /* Type de socket
serveur */
adServeur.sin_port = htons(portServeur); /* Affectation du port destination*/
memcpy(&adServeur.sin_addr, infosServeur->h_addr, infosServeur->h_length);
printf("Adresse du serveur: %s \n", inet_ntoa(adServeur.sin_addr));
tailleAd = sizeof(adServeur);
retVal = connect(skD, &adServeur, tailleAd); /* Connecte avec serveur */
if(retVal == -1)
{
perror("Erreur lors de la
connexion\n");
close(skD);
return -1;
}
return skD;
}
FONCTION D'OUVERTURE DE SOCKET ET ATTENTE DE CONNEXION (SERVEUR)
Voici maintenant la fonction réciproque pour le serveur. Celui-ci doit spécifier le nombre de connexion pouvant être mis en attente grâce à la fonction listen. Une fois ce nombre de client atteint, les prochains sont refusés c'est à dire que leur connect renvoie -1 et qu'il faut analyser errno. Ceux qui sont en attente restent bloqués sur la fonction connect en attendant que le serveur soit à nouveau libre pour l'accepter.
int ouvreSocket(int port)
{
int skD;
size_t tailleAd;
struct sockaddr_in adLocale;
adLocale.sin_family = AF_INET; /* Type de la socket (TCP/IP) */
adLocale.sin_port = htons (port); /* Affectation du port local */
adLocale.sin_addr.s_addr = htonl(INADDR_ANY); /* Identificateur de
l'hote */
skD = socket(AF_INET, SOCK_STREAM, 0); /* Créé socket TCP (connectée) */
if(skD == -1)
{
perror("Erreur lors de la création de la socket\n");
return -1;
}
tailleAd = sizeof(adLocale);
retVal = bind(skD, (struct sockaddr*) &adLocale, tailleAd);
if(retVal == -1)
{
perror("Erreur lors du bind\n");
close (skD);
return -1;
}
retVal = listen(skD, 5) /* Ecoute sur la socket */
if(retVal == -1)
{
perror("Erreur pendant listen\n");
close(skD);
return -1;
}
return skD;
}
ENVOI D'UN MOT
Comme en U.D.P., il faut convenir d'un protocole avec le destinataire pour qu'il sache la taille du message à lire. Ici nous avons convenu d'envoyer d'abord la taille du mot, puis le mot lui-même.
tailleMot = strlen(argv[i]) + 1; /* Taille du mot (+1 pour '\0') */
tailleMotRes = htonl(tailleMot); /* Converti le type long en format réseau */
write(skDesc, &tailleMotRes, sizeof(long)); /* Envoie taille du mot */
write(skDesc, argv[i], tailleMot);
/* Envoie le mot lui-même */
RECEPTION D'UN MOT
skDesc = ouvreSocket(portLocal);
tailleAd = sizeof(adClient);
skServDesc = accept(skDesc, &adClient, &tailleAd); /* Bloquant */
if (skServDesc == -1)
{
perror("Erreur pendant accept\n");
close(skDesc);
return -1;
}
/* Connexion établie, AdClient renseigné */
printf("\n*** INFORMATIONS CLIENTS ***\n");
printf("Adresse: %s \n", inet_ntoa(adClient.sin_addr)); /* Format u_long -> char* */
printf("Port: %d\n\n", ntohs(adClient.sin_port));
retVal = read(skServDesc, &tailleMotRes, sizeof(long));
tailleMot = ntohl(tailleMotRes); /* Format réseau --> format machine */
mot = (char*) malloc(tailleMot * sizeof(char));
retVal = read(skServDesc, mot, tailleMot);
if (retVal == -1)
{
perror("read(skServDesc, &tailleMotRes, sizeof(long)) erreur");
return -1;
}
SERVEUR CONCURRENT
L'intérêt de la fonction accept est qu'elle créé une socket de service lorsqu'un client est accepté sur la socket serveur, la seule visible et connue du client. Seulement cela ne sert à rien si le serveur doit attendre la déconnexion du client en cours avant d'en accepter un autre. C'est pour cela qu'il est important de faire un serveur concurrent. Cela veut dire que le processus serveur père attend la connexion d'un client, l'accepte, puis créé un processus fils, qui héritant de la table des descripteurs, possède aussi la socket de service connectée au client. Le fils doit alors fermer la socket serveur car il n'en a pas besoin, le père a juste à fermer le descripteur de la socket de service car ce n'est pas lui qui traite le client puis à se remettre en écoute sur la socket serveur pour accepter un autre client. Tous les clients sont ainsi traités en parallèle.
Architectures d'un serveur concurrent en T.C.P.
while (1)
{
tailleAd = sizeof(adClient);
skServDesc = accept(skDesc, &adClient, &tailleAd);
if (skServDesc == -1)
{
perror("Erreur pendant accept\n");
close(skDesc);
return -1;
}
numPID = fork();
if (numPID == 0) /* Fils, s'occupe du client actuel */
{
close(skDesc); /* Le fils n'a pas besoin de socket serveur */
/* ... */
/* TRAITEMENT DU CLIENT */
/* ... */
close (skServDesc); /* Ferme socket de service */
return 0;
}
else
{
close(skServDesc); /* Le père n'a pas besoin de socket de service */
}
}