Cours 2 - Sockets Experts - 2023
Cours 2 - Sockets Experts - 2023
TUTELLE ACADÉMIQUE
DE
L’INSTITUT UNIVERSITAIRE SIANTOU
ÉCOLE SUPÉRIEURE DE TECHNOLOGIE ET INDUSTRIELLE
SIANTOU
Discipline Travail Efficacité BTS DSEP HND LICENCES PROFESSIONNELLES BACHELOR MASTERS DOCTORAT
CAPACITE EN DROIT FORMATION PROFESSIONNELLE
MASTER INFORMATIQUE
Architecture LOGICIELLE
Année 2022-2023
Table des matières
2 Les sockets des experts .............................................................................................................................. 2
2.1 Généralités............................................................................................................................................... 2
Ii
2.1 Généralités
Toutes les communications Internet entre les ordinateurs reposent sur le protocole TCP/IP
qui englobe les protocoles IP (Internet Protocol), TCP (Transport Control Protocol) et UDP
(Unreliable Datagram Protocol). Le but de ce cours n’est pas de décrire le protocole TCP/IP
aussi, pour de plus amples explications, vous pouvez revoir votre cours de licence ou consulter
l’encyclopédie en ligne wikipedia à l’adresse : [Link] Il est
cependant nécessaire de rappeler qu’un protocole de communication général, comme Internet,
est lui-même composé de plusieurs sous-protocoles. On parle cependant aussi de protocole
pour les sous-protocoles. Un protocole complet est également appelé suite de protocoles ou
pile de protocoles.
Les sous-protocoles sont généralement organisés en couches : les protocoles de plus haut
niveau utilisant ceux des couches basses. Ainsi un protocole de niveau N +1 utilise les
fonctions et propriétés du protocole de niveau N mais n’accède pas directement à ceux de
niveau N −1. Ce schéma d’organisation à été défini dans la norme OSI (Open Systems
Interface) de l’ISO (International Standard Organization). Il est partiellement mis en œuvre
dans TCP/IP où les protocoles TCP et UDP utilisent le protocole IP. L’accès à ces protocoles
se fait à travers une interface de programmation.
L’API (Application Programming Interface, interface de programmation) du protocole
TCP/IP des systèmes Unix repose principalement sur l’utilisation de sockets de
communication. L’API des sockets permet l’utilisation au niveau applicatif des fonctionnalités
du protocole. Cette API a été définie par l’université de Berkeley en 1982 pour le système
Unix BSD 4.1, un des premiers systèmes Unix à inclure une implémentation de TCP/IP. Cette
interface a été étendue par la suite pour servir d’accès à
1L’ensemble des fonctions de l’interface socket est documenté sur les systèmes Unix. Cette documentation est
accessible à travers les commandes man nom_fonction ou info nom_fonction
de nombreux autres protocoles. Parmi ceux-ci, nous pouvons citer X.25, IPX, Netbeui, IPv6,
Bluetooth, etc.
Le mode connecté de programmation des sockets permet de mettre en place des
communications fiables puisqu’il repose sur le protocole TCP qui offre des garanties quant à
l’acheminement des messages. Dans ce mode, nous avons la garantie de ne perdre aucune
donnée et l’ordre de réception est identique à celui d’émission. Le choix du mode connecté
doit donc se faire lorsque la qualité de l’échange doit être assurée.
Pour sa part, le mode de communication non-connecté repose sur le protocole UDP. Ce
protocole ne donne pas la garantie qu’un message envoyé arrive à sa destination. Ainsi, si vous
envoyé un message sur une adresse erronée cela ne produira pas d’erreur. De même, si le trafic
du réseau est important votre message peut tout simplement être supprimé, sans que vous en
soyez-vous en informer. Dans la réalité cela arrive rarement et vous aurez l’impression que ce
protocole délivre aussi bien vos messages que le protocole TCP. Il est cependant nécessaire de
tenir compte de cette éventualité si votre programme est très sensible à la perte de données. Le
risque est plus important si vous envoyé de grosses quantités de données.
où :
descSock est le descripteur de socket alloué à la création. Il nous sert de référence à chaque
fois que nous devons utiliser la socket. Si la socket ne peut être crée, la valeur de
descSock est −1.
domain pour nous il prendra toujours la valeur AF_INET.
type est le type de communication qui est utilisé par la socket. Pour le protocole TCP on met
type à SOCK_STREAM et pour le protocole UDP on met type à SOCK_DGRAM
Le résultat de l’appel à la fonction socket est la création d’une socket à laquelle aucun nom
n’est associé, simplement nous avons la prise à laquelle nous devons, par la suite, attribuer une
identification. Le retour de la fonction retourne donc le descripteur de la socket.
Du point de vue de la programmation, nous voyons une socket comme un fichier. C’est à
dire que le descripteur qui nous est retourné par la fonction socket est en fait un descripteur de
fichier. Ce descripteur de fichier est une entrée dans la table des descripteurs de fichier du
processus qui exécute notre programme. Il correspond au numéro alloué dans la table à la
création de la socket : c’est donc une valeur entière (le système alloue la première entrée libre
de la table des descripteurs). La taille de la table des descripteurs de fichiers étant limitée, nous
aurons donc un nombre limité de sockets créées à un instant donné dans notre programme.
D’où l’intérêt de penser à fermer celles dont nous ne nous servons plus.
Puisque la socket est accédée par le programme sous la forme d’un descripteur de fichier,
la plupart des fonctions utilisables sur un fichier le seront également sur la socket. C’est le cas
des fonctions de lecture et d’écriture mais aussi de manipulation et de contrôle. Par contre, une
socket n’est créée/ouverte qu’avec une fonction spécifique.
Pour utiliser cette fonction, il est nécessaire d’inclure les fichiers définissant les types et
valeurs associés aux sockets :
#include <sys/types.h>
#include <sys/socket.h>
2 Attention, le vocable d’adresse est utilisé plusieurs fois avec des significations différentes dans notre contexte
de travail. En effet, une adresse peut être une adresse en mémoire, l’adresse d’une socket et l’adresse d’une machine
sur le réseau Internet. Nous essayerons donc, par la suite, de conserver la notion d’adresse pour parler de l’adresse
associée à une socket. Pour les adresses en mémoire, nous parlons de pointeur et pour les adresses de machines sur
le réseau, nous parlons de numéro Internet ou IP. Lorsque nous ne pouvons faire autrement, nous précisons adresse
mémoire ou adresse réseau.
Nous associons une adresse à une socket à l’aide de la fonctions bind dont voici le prototype
:
où :
descSock est le descripteur de la socket à laquelle nous voulons associer une adresse,
addr est un pointeur sur l’adresse qui doit être associée à la socket,
addrLen est la taille de la structure d’adresse, parce que les adresses n’ont pas la même taille
dans tous les domaines de communication. Il est possible d’utiliser sizeof( addr).
la valeur de retour est un code d’erreur permettant de savoir comment l’appel s’est passé. En
cas de problème err =−1, sinon err =0.
Remarque : l’interface des sockets a été conçue pour supporter plusieurs protocoles
différents. L’adresse que nous souhaitons associer à notre socket traduit en général la position
dans le réseau de la socket. Cette adresse est donc dépendante de la famille de protocole
utilisée. Or la fonction bind est, elle, générique, c’est à dire qu’elle est utilisée quel que soit le
protocole accédé par la socket. Pour cette raison, les concepteurs de l’interface ont utilisé une
petite subtilité permettant d’utiliser la même fonction quel que soit le protocole de
communication : l’adresse est passée sous la forme d’un pointeur, qui référence l’emplacement
en mémoire de l’adresse, et d’une valeur donnant la taille de l’adresse. La fonction bind peut
ainsi récupérer l’adresse, quelle que soit sa taille et sa structure, en accédant directement à la
zone de mémoire correspondante. Pour résoudre les problèmes de type des paramètres, un type
générique d’adresse struct sockaddr est défini. Il recouvre l’ensemble des types d’adresse
possibles. Le programmeur passe le paramètre sous la forme d’un pointeur sur l’adresse qui
est définie par son protocole. Ainsi, la structure de données passées en paramètre est bien
équivalente puisqu’il s’agit d’un pointeur dans les deux cas. Le pointeur donné par le
programmeur est transtypé en struct sockaddr pour obtenir la correspondance des types. La
fonction bind peut ensuite déterminer quel est le type d’adresse utilisé grâce au premier champ
de la structure d’adresse. Quel que soit le protocole, donc le type d’adresse, le premier champ
de la structure est toujours un entier de deux octets short qui contient un identificateur de la
famille du protocole de communication. Dans notre cas, ce premier champ sera toujours à
AF_INET, puisque nos adresses sont des adresses Internet.
Dans le domaine de communication AF_INET, nous utilisons des adresses décrites par une
structure struct sockaddr_in. Les adresses AF_INET permettent d’accéder à toutes les
machines d’un domaine Internet. Dans une adresse AF_INET, il y a deux identificateurs : un
pour la machine et un pour la socket sur la machine. Cette double identification permet
d’obtenir une adresse unique pour chaque socket. En effet, dans le réseau Internet, chaque
machine est identifiée par un numéro (adresse réseau) IP unique, décrit dans une structure
struct in_addr. C’est ce numéro qui est utilisé dans la partie machine de l’adresse d’une socket
et non le nom de la machine comme nous l’avons fait au chapitre précédent. Ensuite, le numéro
de port permet une identification sur la machine.
Le contenu d’une adresse AF_INET est donc le suivant :
struct sockaddr_in {
short sin_family; /* AF_INET, domaine */
u_short sin_port; /* no de port */
struct in_addr sin_addr; /* adresse internet */
char sin_zero[8]; /* init a 0 */
};
L’utilisation de cette structure et des fonctions qui la manipule nécessite l’inclusion des
fichiers d’entête du protocole Internet :
#include <netinet/in.h>
Le champ sin_zero permet d’aligner, pour des raisons de sécurité, toutes les adresses réseau
sur la même taille. Il faut l’initialiser à 0, par exemple en utilisant la fonction bzero( sin_zero,
8);.
Pour initialiser une adresse AF_INET, il est nécessaire d’initialiser un numéro IP et un
numéro de port. Le champ sin_addr doit donc être initialisé avec le numéro IP de la machine
sur laquelle se trouve la socket et le champ sin_port avec le numéro de port qui doit être associé
à la socket.
Cependant, dans le cas d’une adresse destinée à être associée à une socket (utilisation de la
fonction bind), il n’est pas toujours nécessaire d’initialiser ce champ sin_addr avec l’adresse
d’une machine. Le champ peut être initialisé à INADDR_ANY, dans ce cas c’est l’adresse de
la machine courante (au moment de l’exécution) qui est utilisée. Ceci permet d’écrire des
programmes qui attribuent bien une adresse locale à la socket sans avoir à connaître le numéro
IP de la machine au moment où nous écrivons le programme.
De la même manière, il est possible de pas fixer de numéro de port dans l’adresse lors d’une
association socket-adresse, en positionnant le champ sin_port à 0. Dans cas, le numéro de port
est attribué par le système en fonction des numéros déjà utilisés par les autres sockets.
Dans la suite, nous voyons différentes manières d’initialiser le numéro IP et le numéro de
port.
Dans un domaine Internet, chaque machine est identifiée à l’aide d’un numéro IP. Etant
donné la large utilisation du protocole IP et donc la variété des implémentations, la description
de ce numéro varie. Il est en fait toujours composé de quatre octets. Mais, il peut être décrit de
la manière suivante. Dans des implantations anciennes, il est généralement décrit par :
struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
};
#define s_addr S_un.S_addr
En fait, les deux représentations sont les mêmes puisqu’elles reposent sur une même structure
de quatre octets. La première représentation donne trois visions différentes : sous la forme de
quatre caractères non signés (en Unix u_short équivaut à unsigned short) séparés, sous la forme
de deux entiers de deux octets ou d’un entier de quatre octets. Cette représentation est justifiée
par la définition des différentes classes d’adresse utilisées dans le routage Internet. Ainsi, nous
voyons souvent le numéro IP d’une machine écrit sous la forme de quatre octets. Par exemple
la machine lifc a pour adresse [Link]. La seconde représentation donnée ne définie le
numéro IP que sous la forme d’un entier de 32 bits.
Tout ce discours est un peu complexe. Pour ceux qui ne maîtrisent pas les structures
complexes en langage C, il est suffisant de retenir que le numéro IP est un entier et qu’il peut
être, dans tous les cas, représenté par le champ s_addr de la structure in_addr.
2.4.3 Les noms de machine
Ce numéro IP n’étant pas très facile à mémoriser il est possible, grâce au DNS3, de donner
un nom aux machines et d’établir une correspondance entre les noms et les numéros. Pour
obtenir l’adresse AF_INET (Internet) d’une machine, on peut utiliser la fonction
gethostbyname. Cette fonction permet d’obtenir des informations sur une machine à partir de
son nom. Le prototype de la fonction est le suivant :
où le paramètre name est une chaîne de caractère donnant le nom de la machine dont nous
cherchons l’adresse. La structure hostent, valeur de retour de la fonction est définie de la
manière suivante :
struct hostent {
char* h_name; /* official name of host */
char** h_aliases; /* alias list */
int h_addrtype; /* address type */
int h_length; /* length of address */
char** h_addr_list; /* list of addr from name server */
};
#define h_addr h_addr_list[0]
#include <netdb.h>
Comme l’indique le prototype, cette fonction retourne une structure de données de type hostent
qui décrit les informations associées à la machine du point de vue du DNS4. Cette structure
comprend :
– le nom principal de la machine dans le champ h_name,
– une liste d’alias dans le champ h_aliases. Une machine peut avoir plusieurs noms, par
exemple liyde1 et [Link] sont deux noms différents. Dans ce cas, la
machine possède un nom principal et les autres noms sont mémorisés dans le champ des
alias.
– le type des adresses réseaux, pour nous des numéro IP, associées à la machine dans le
champ h_addrtype. La fonction ayant été conçue pour fonctionner avec différents
protocoles, ce champ permet de savoir comment traiter les adresses réseaux qui sont
mémorisées dans le champ h_addr_list.
– la longueur d’une adresse réseau dans le champ h_length. De la même manière que pour
la fonction bind, les adresses réseaux sont ici gérées sur la base de la zone où elles sont
enregistrées en mémoire. C’est à dire à partir d’un pointeur sur le début de la zone et de
la taille occupée par l’adresse réseau.
3
Pour approfondir ces notions, revoir le cours de système de dernière année de Licence
4
A l’appel de cette fonction le système recherche dans ses tables locales puis dans le DNS, les informations liées à la
machine suivant la politique de résolution de noms définie par le système, dans le fichier /etc/[Link]
– la liste des adresses réseau de la machine dans le champ h_addr_list. De la même manière
qu’une machine peut avoir plusieurs noms, elle peut aussi avoir plusieurs adresses
réseau 3 . L’adresse réseau principale est toujours stockée en premier. Elle est donc
accessible à partir de la simplification h_addr. Il faut remarquer que le tableau obtenu
n’est pas constitué de pointeurs sur un type d’adresse réseau générique mais plutôt de
pointeurs sur caractères. Cette définition était classique lorsque l’interface socket a été
définie, la définition char* était utilisée pour les pointeurs génériques. Il sera donc
nécessaire de transtyper (“caster”) le pointeur donné par h_addr en un pointeur sur une
structure in_addr.
La structure hostent est allouée par le système et la fonction retourne un pointeur sur la
structure associée. Il n’est donc pas nécessaire d’allouer une structure avant l’appel à la
fonction mais il faut déclarer le pointeur qui sert en retour de la fonction.
Encore une fois, cette structure n’est pas très simple à comprendre pour les programmeurs
qui ne maîtrisent pas bien le langage C. Pour cette raison, nous donnons la “formule magique”
qui permet d’obtenir le numéros IP d’une machine.
// Declarations
struct hostent* host; /* description machine serveur */
struct sockaddr_in nom; /* adresse ocket du serveur */
// Instructions
/* Recherche de l’adresse de la machine */
host = gethostbyname ( nomMachine ); if ( host == NULL ) {
//Traitement de l’erreur
}
Il existe la fonction réciproque gethostbyaddr qui permet d’obtenir le nom d’une machine
à partir de son adresse.
Dans certains cas, il n’est possible ni de connaître le nom de la machine sur laquelle
s’exécute le programme, ni de l’obtenir par un passage de paramètre ou à partir d’un fichier de
configuration. Pour obtenir le nom de la machine locale (pour obtenir ensuite son adresse ou
faire bind sur une socket), on peut alors utiliser :
3 C’est surtout le cas pour les machines passerelles, à la frontière entre deux réseaux
Pour localiser une socket sur une machine le système utilise un identificateur local appelé
port. Le numéro de port associé à une socket est un entier codé sur deux octets. Les numéros
de port inférieurs à 1024 sont réservés aux processus superviseurs (root).
Il y a deux manières pour obtenir un numéro de port : le fixer par le programme ou en
demander l’attribution au système. Notons que, pour faire communiquer deux programmes, il
est nécessaire d’une part qu’un des programmes donne une adresse, donc un numéro de port,
à sa socket et, d’autre part, que l’autre programme connaisse cette adresse, donc ce numéro de
port, pour pouvoir contacter le destinataire. De même que pour pouvoir établir une
conversation téléphonique, il est nécessaire d’avoir un combiné avec un numéro et un
interlocuteur qui connaisse le numéro à appeler. En fonction du mode d’attribution du numéro
de port, les programmes qui communiquent n’ont pas les mêmes garanties, ce qui suppose des
interactions différentes. En effet, si nous choisissons de fixer le numéro de port utilisé la même
valeur doit être définie 4 dans les deux programmes. Nous définissons ainsi un point de
rencontre entre deux programmes. Si le numéro de port est attribué dynamiquement par le
système, il faut alors trouver une solution pour donner la valeur attribuée au programme qui
communiquera avec cette socket.
Pour fixer un numéro de port, il suffit de positionner une valeur dans le champ sin_port
avant de faire appel à la fonction bind. Si le programme est appelé à s’exécuter avec des droits
étendus (superviseur), le numéro de port peut être choisi de manière quelconque, sinon, pour
une exécution en mode utilisateur, il doit être supérieur à 1024. Le problème lié au choix de ce
numéro de port consiste alors à être sûr que le numéro de port ne soit pas déjà associé à une
socket, ce qui génère une erreur de la fonction bind. Généralement, ce n’est pas le cas pour les
numéros de ports utilisateurs, sauf si des applications particulières s’exécutent sur la machine.
Pour attribuer dynamiquement un numéro de port, le champ sin_port est initialisé à 0 avant
l’appel à la fonction bind. Pour connaître le numéro de port attribué par le système, il faut
demander au système qu’elle est l’adresse associée à la socket, après l’appel à la foncion bind
et en extraire le numéro du port. Ceci se fait à l’aide de la fonction :
4 Elle sera définie statiquement dans le code du programme ou donné à partir d’un fichier de configuration.
Attention, le mode de codage utilisé par Internet est de type MSB alors que le codage sur
les machines PC est de type LSB. Dans le cas d’un entier sur deux octets comme le numéro de
port, le codage est donc réalisé dans l’ordre inverse. De la même manière, les ordinateurs
utilisés dans la communication n’utilisent pas forcément le même codage. On peut alors utiliser
les fonctions de conversion de l’ordre des octets :
netshort = htons(hostshort);
u_short netshort, hostshort;
hostshort = ntohs(netshort);
u_short hostshort, netshort;
La fonction htons, “host to network”, permet de convertir un numéro de port depuis une
représentation machine vers la représentation IP. La fonction ntohs, “netwrok to host”, permet
de convertir un numéro de port depuis une représentation IP vers une représentation machine.
Ces fonctions sont dépendantes de la machine sur laquelle elles s’exécutent. Elles peuvent ne
rien faire sur certaines machines (Ex : MSB) mais sur d’autres machines (Ex : LSB) elles
inversent l’ordre des octets. Sur des machines de type PC, il est impératif de les utiliser même
si le programme peut marcher sans y faire appel dans la mesure où, en utilisant le même codage
faux sur les deux machines qui communiquent, la valeur est bien la même. Ceci peut cependant
générer des erreurs dans la mesure où la valeur donnée à la socket ne correspond pas à la valeur
attendue.
Un numéro de port ne peut pas être affiché avec un simple format "%d" puisque le type du
port est unsigned short. Il faut donc utiliser soit le format d’affichage "%hu".
Les sockets sont souvent utilisées pour mettre en place des applications accessibles à
travers le réseau, nous parlons alors de service. La notion est service est bien connue dans le
réseau Internet, elle est illustrée par les serveurs WEB ou service HTTP, les services de
messagerie, etc. De la même manière qu’un nom est associé aux machines pour les identifier
à la place du numéro IP, des noms de services sont définis pour éviter d’avoir à mémoriser le
numéro de port lui-même. Un certain nombre de services sont définis et associés à des numéros
de ports fixés. Par exemple, le numéro de port 80 est toujours associé au service HTTP, le
numéro 100 au service POP3, etc. Ces associations sont définies le plus souvent sur chaque
machine dans le fichier /etc/services ou parfois dans des bases de données définies sur le
réseau. Un protocole d’accès peut aussi être défini pour un service. Par exemple, certains
services ne sont accessibles qu’à travers le protocole UDP.
Le numéro de port associé à un service peut être obtenu à l’aide de la fonction
getservbyname, dont l’utilisation est similaire à la fonction gethostbyname. Le prototype de
cette fonction est le suivant :
struct servent *getservbyname(service, proto)
char *service, *proto;
Où les chaînes de caractères service et proto donnent respectivement le service recherché et le
protocole utilisé par le service. La structureservent, valeur de retour de la fonction est définie
par :
struct servent {
char* s_name; /* official name of service */
char** s_aliases; /* alias list */
int s_port; /* port service resides at */
char* s_proto; /* protocol to use */
};
#include <netdb.h>
2.4.6 Synthèse
Après toutes les informations que nous venons de voir, il paraît nécessaire de faire une
petite synthèse sur la manière de créer une socket, pour remplacer les fonctions socketUDP,
socketAddr, socketServeur et socketClient utilisée au chapitre précédent. Cette synthèse sera
d’autant plus utile que la méthode de création des sockets est généralement toujours la même
et que, une fois que nous disposons du code nécessaire à une création, il est souvent suffisant
de le reprendre et d’en modifier quelques valeurs pour obtenir un résultat satisfaisant.
Pour créer une socket identifiée, il est donc nécessaire de suivre les étapes suivantes :
return 0;
}
Dans l’exemple précédent, le numéro IP associé à la socket n’a pas besoin d’être précisé
car il s’agit de la machine locale. Par contre, si je désire établir une connexion en mode
connecté ou envoyer une donnée en mode non-connecté le champ s_addr doit être correctement
initialisé. L’exemple suivant montre l’initialisation d’une adresse avec le port 2610 et la
machine smith :
...
return 0;
}
L’appel à cette fonction suppose que le serveur ait donné une adresse à sa socket au moment
où le client exécute cette fonction. Par contre, il n’est pas indispensable que le client associe
une adresse à la sienne pour la connectée avec celle du serveur.
A l’appel de la fonction, le système envoie une demande de connexion au serveur. Cet
appel est bloquant tant que la connexion n’a pas été acceptée ou refusée. En cas de problème
la fonction retourne un code d”erreur.
La fonction socketClient contient donc principalement une création de socket,
l’initialisation d’une structure d’adresse identifiant le serveur et l’appel de la fonction de
connexion.
Du côté du serveur, nous avons vu qu’il devait disposer d’une socket de connexion pour
ne pas mélanger les demandes de connexion et les données envoyées par les clients. Ceci est
réalisé en créant une socket par la procédure normale puis en la déclarant comme dédiée aux
demandes de connexion grâce à la fonction listen dont le prototype est le suivant :
Les derniers paramètres, que nous n’avions pas utilisé jusque là, permettent de recevoir
l’adresse de l’émetteur dans une structure d’adresse, que nous devons préallouer. Cette structure
est identifiée par le pointeur from de type struct sockaddr*
et un pointeur sur une variable de type socklen_t, contenant la taille toLen de
l’adresse pré-allouée. En retour de la fonction, cette variable contient la taille de l’adresse reçue,
raison pour laquelle il est nécessaire de faire un passage de paramètre par adresse ( au sens
pointeur). Si nous n’avons pas besoin de cette adresse, il est possible de passer la valeur NULL
pour les paramètres from et fromLen.
2.6 La communication
Les principales difficultés dans l’utilisation de l’interface des sockets proviennent de la
création, nous venons donc de les passer, et les fonctions que vous avez utilisé au chapitre
précédent pour la communication avec les sockets sont déjà celles de l’interface, vous avez
donc connaissance du principal. Nous allons cependant revoir la partie de communication dans
le but de donner quelques détails complémentaires.
Nous avons vu que la communication sockets ne se fait que sur la base d’échanges de
données vues par les programmes sous la forme d’un buffeur. Cela signifie que le message est
constitué du contenu d’une zone de mémoire, recopiée dans le message envoyé. De la même
manière, ce que reçoit le récepteur est recopié dans une zone de mémoire.
Les données ainsi échangées le sont sous leur forme brute, non typées. Si vous avez, par
exemple, défini une structure dans votre programme émetteur et qu’il l’envoie au récepteur,
celle-ci est reçue sans indication de typage sur les champs de la structure. En fait, la structure
est reçue telle qu’elle était mémorisée chez l’émetteur. Ceci implique qu’une donnée émise
avec un certain type peut être reçue avec un type différent sans erreur. Il est donc nécessaire
d’être prudent sur le type de réception des messages. Nous verrons au chapitre sur le
client/serveur comment mettre en place les procédures nécessaires au typage des structures...
En ce qui concerne la réception, il est nécessaire de préparer une zone de mémoire pour la
réception du message. Cette zone de mémoire est passée à la fonction de réception sous la
forme d’un pointeur sur le début de la zone et de la taille de la zone. Pour garantir l’intégrité
de vos données, la fonction de réception en recopie jamais en dehors de cette zone de mémoire.
Cela signifie que, si la taille des données arrivées dépasse cette zone, seule la partie qui peut
être stockée dans la mémoire l’est, à partir du début des données. Le reste est laissé dans un
tampon du système en attendant une demande de réception ultérieure. Ainsi, si l’émetteur a
envoyé 100 une chaîne de 100 caractères et que je ne demande qu’à en recevoir 50, seuls les
50 premiers caractères de la chaîne sont reçus, les autres restent en attente5.
5Attention, nous avons vu que les chaînes de caractères sont, en langage C, codées sous la forme d’une suite
de caractères terminée par un octet nul. Cet octet nul est utilisé dans les fonctions de manipulation de chaînes
comme point de fin de traitement. Dans notre exemple, la chaîne de 100 caractères sera suivie d’un 101ième
caractère, un octet nul. Si donc nous ne recevons que 50 caractères, l’octet nul n’en fera pas partie et nous ne
Les données échangées sont transmises sous la forme d’un flux. C’est à dire qu’aucun
marqueur n’est positionné entre les différents envois et il n’est pas possible de déterminer la
taille d’un envoi à partir du buffeur de réception. Ainsi, il est possible de recevoir deux envois
successifs de 50 octets aussi bien sous la forme d’un buffeur de 100 octets que de 10 buffeurs
de 10 octets. C’est à l’application de réaliser la délimitation des données si elle en a besoin. En
utilisant le protocole de communication TCP, nous avons la garantie que l’ordre des données
est préservé à la réception. Par contre, avec le protocole UDP nous n’avons pas cette garantie
puisque des données peuvent être perdues.
Dans le chapitre précédent nous avons évité au maximum l’utilisation des adresses dans la
communication. Ainsi, dans les fonctions sendto et recvfrom, les champs d’adresse ont été
remplis à partir de fonctions pré-définies ou ignorés. Pour une utilisation avancée de ce mode
de communication, il peut être utile de savoir se servir de ces champs.
Rappelons que la fonction sendto a pour prototype :
pouvons utiliser les fonctions de traitement des chaînes de caractères sans risque d’erreur. Il est cependant
possible, d’ajouter manuellement cette valeur.
De même que dans la fonction accept les derniers paramètres permettent de recevoir
l’adresse de la socket avec laquelle le serveur est connecté. Le mode de passage des paramètres
est donc identique et la taille de l’adresse doit être initialisée avant l’appel à la fonction. Si
nous n’avons pas besoin de cette adresse, il est possible de passer la valeur NULL pour les
paramètres addr et addrLen.
Remarque : l’interface initiale des sockets, qu’il n’est pas rare de rencontrer encore sur
certains systèmes, utilise des types char* à la place des types void* pour le pointeur sur les
données, et des types int à la place de size_t et socklen_t.
2.6.3 Le mode connecté
Du fait que les adresse ne sont pas utilisées dans les échanges en mode connecté, nous
avons utilisé les fonctions de l’interface dans le chapitre précédent. Nous pouvons simplement
ajouté que la fonction send peut rester bloquée tant que l’ensemble des données passées en
paramètre ne sont pas envoyées, même si cela doit se faire en plusieurs fois. Ce n’est pas le
cas de la fonction sendto qui rend une erreur si elle ne peut envoyer en une fois.
Généralement on choisit de travailler en mode connecté si l’accès au serveur demande
plusieurs requêtes, peut être anonymes, doit être sûr, etc. Si l’accès au serveur est ponctuel ou
si on communique avec différents processus, il vaut mieux utiliser le mode non-connecté qui
est plus rapide.
Remarques et mises en garde
– l’ensemble des fonctions de l’interface est constitué par des fonctions système. Le
déroulement de ces fonctions dépend donc des structures de données internes aux
systèmes et il n’est pas possible de prédire leur bon déroulement. Par exemple, la
fonction de création des sockets fait appel à des structures de données de taille fixe, ce
qui fait que si d’autres programmes ont déjà consommé ces ressources il ne sera pas
possible de créer la socket. Il est donc indispensable de tenir compte de ce fait dans vos
programmes. Cela doit se traduire par le test systématique des retours des fonctions
systèmes et par un traitement d’erreur qui tient compte de ce risque. L’appel à la fonction
exit constitue un traitement sommaire mais efficace dans ce cas.
– les fonctions recvfrom et accept retournent dans from, l’adresse de l’émetteur, il faut
donc bien se souvenir que la taille de l’adresse est passée par pointeur en paramètre et
qu’elle doit être initialisée.
– dans la mesure où les sockets sont identifiées dans le système comme des fichiers, il est
possible d’utiliser certaines fonctions de manipulation des fichiers sur les sockets. Ainsi,
read = recv, recvfrom avec trois arguments, write = send avec trois arguments. write ne
peut pas l’utiliser à la place de sendto car il faut pouvoir préciser le destinataire. Cette
notation peut être rencontrée dans des programmes utilisant des sockets. Il cependant
recommandé d’utiliser les fonctions spécifiques pour plus de clarté.
– Les fonctions d’interface des sockets sont des fonctions système. Elles retournent donc
toutes un code d’erreur dans la mesure où nous n’avons aucune garantie de leur bon
déroulement. Un retour d’erreur d’une fonction système est toujours (sous Unix)
caractérisé par la valeur -1. Le code d’erreur spécifique est stocké dans la variable errno.
Il est possible de traiter les erreurs systèmes de trois manières différentes. Dans tous les
cas il faut faire extern int errno; et #include < errno.h > :
1. fprintf(stderr, "%d", errno), permet de faire afficher le code d’erreur puis aller voir
dans /usr/include/errno.h à quoi il correspond.
2. void perror( const char *), affiche un message d’erreur et la chaîne donnée en
paramètre, en fonction de la valeur contenue dans errno
3. char *strerror( int errno), rend une chaîne qui contient un message d’erreur en
fonction de errno.
int fd;
fd_set fdset;
Il faut noter que les ensembles de descripteurs sont passés par adresse car ils sont modifiés
par la fonction. L’utilisation de la fonction FD_ZERO est recommandée avant chaque
utilisation et avant chaque ré-utilisation de l’ensemble.
Il n’est pas nécessaire d’avoir un ensemble de chaque type pour utiliser la fonction select.
Une valeur NULL peut-être passée en paramètre pour les ensembles que nous ne souhaitons
pas tester.
En fait un ensemble de descripteur peut être de taille différente. Cette taille dépend du
nombre de descripteurs de fichiers attribué par processus, ce qui est défini par un paramétrage
du système d’exploitation. Pour cette raison, le premier paramètre de la fonction select est la
taille de ces ensembles. Cette taille n’est donc pas liée au nombre de descripteurs que vous
avez positionnez dans votre ensemble mais à la taille du type fd_set. Elle est donnée par la
constante FD_SETSIZE. Cette constante sera donc systématiquement passée en premier
paramètre du select.
#include <sys/types.h>
#include <sys/time.h>
En fonction du code de retour on peut savoir par quoi l’attente de select a été interrompue.
Si retour = 0 elle est sortie par timeout, -1 sortie pour une erreur, n nombre de descripteurs
prêts.
En sortie de l’appel à select, si au moins l’un des descripteurs est prêt (valeur retournée
positive), alors seuls les descripteurs prêts sont maintenus dans les ensembles passés en
paramètre. L’utilisation de la fonction FD_ISSET permet alors, en testant successivement tous
les descripteurs initialement positionnés, de savoir lequel est prêt.
Puisque la fonction select modifie le contenu des ensembles de descripteurs, il est
nécessaire de réinitialiser ces ensembles avant chaque nouvel appel à la fonction.
La structure timeval donne le temps maximum d’attente dans select.
struct timeval {
long tv_sec; /* seconds */
Si le paramètre timeout a la valeur NULL, la fonction select n’a pas de délais de garde. Si
la structure timeval est initialisée à 0, il n’y a pas d’attente mais les descripteurs sont quand
même testés.