next up previous contents index
Next: XIV Compléments sur les Up: D Sockets BSD et Previous: D Sockets BSD et   Contents   Index

Subsections

XIII Généralités sur les sockets de Berkeley

1 Généralités

La version BSD 4.1c d'Unix pour VAX, en 1982, a été la première à inclure TCP/IP dans le noyau du système d'exploitation et à proposer une interface de programmation de ces protocoles : les socketsXIII1.

Les sockets sont ce que l'on appelle une API (`` Application Program Interface '') c'est à dire une interface entre les programmes d'applications et la couche transport, par exemple TCP ou UDP. Néanmoins les sockets ne sont pas liées à TCP/IP et peuvent utiliser d'autres protocoles comme AppleTalk, Xérox XNS, etc...

\includegraphics{fig.socket.01.ps}

figure XIII.01 -- Les sockets, une famille de primitives

Les deux principales API pour Unix sont les sockets Berkeley et les TLI System V. Ces deux interfaces ont été développées en C.

Les fonctionnalités des sockets que nous allons décrire, sont celles apparues à partir de la version 4.3 de BSD Unix, en 1986. Il faut noter que les constructeurs de stations de travail comme HP, SUNXIII2, IBM, SGI, ont adopté ces sockets, ainsi sont-elles devenues un standard de fait et une justification de leur étude.

Pour conforter ce point de vue il n'est pas sans intérêt d'ajouter que toutes les applications majeures (named, dhcpd, sendmail, ftpd, apache,...) `` Open Sources '' de l'Internet, utilisent cette API.

Enfin, et avant d'entrer dans le vif du sujet, le schéma ci-dessous rappelle les relations entre pile ARPA, N$^{\circ }$ de port et processus.

\includegraphics{fig.socket.02.ps}

figure XIII.02 -- Relation pile IP, numéro de port et process ID

2 Présentation des sockets

Les créateurs des sockets ont essayé au maximum de conserver la sémantique des primitives systèmes d'entrées/sorties sur fichiers comme open, read, write, et close. Cependant, les mécanismes propres aux opérations sur les réseaux les ont conduits à développer des primitives complémentaires (par exemple les notions de connexion et d'adresse IP n'existent pas lorsque l'on a besoin d'ouvrir un fichier !).

Quand un processus ouvre un fichier (open), le système place un pointeur sur les structures internes de données correspondantes dans la table des descripteurs ouverts de ce processus et renvoie l'indice utilisé dans cette table. Par la suite, l'utilisateur manipule ce fichier uniquement par l'intermédiaire de l'indice, aussi nommé descripteur de fichier.

Comme pour un fichier, chaque socket active est identifiée par un petit entier appelé descripteur de socket. Unix place ce descripteur dans la même table que les descripteurs de fichiers, ainsi une application ne peut-elle pas avoir un descripteur de fichier et un descripteur de socket de même valeur.

Pour créer une socket, une application utilisera la primitive socket et non open, pour les raisons que nous allons examiner. En effet, il serait très agréable si cette interface avec le réseau conservait la sémantique des primitives de gestion du système de fichiers sous Unix, malheureusement les entrées/sorties sur réseau mettent en jeux plus de mécanismes que les entrées/sorties sur un système de fichiers, ce n'est donc pas possible.

Il faut considérer les points suivants :

  1. Dans une relation du type client-serveur les relations ne sont pas symétriques. Démarrer une telle relation suppose que le programme sait quel rôle il doit jouer.

  2. Une connexion réseau peut être du type connectée ou non. Dans le premier cas, une fois la connexion établie le processus origine discute uniquement avec le processus destinataire. Dans le cas d'un mode non connecté, un même processus peut envoyer plusieurs data-grammes à plusieurs autres processus sur des machines différentes.

  3. Une connexion est définie par un quintuplet (cf cours TCP page [*]) qui est beaucoup plus compliqué qu'un simple nom de fichier.

  4. L'interface réseau supporte de multiples protocoles comme XNS, IPX, APPLETALKXIII3, la liste n'est pas exhaustive. Un sous système de gestion de fichiers sous Unix ne supporte qu'un seul format.

En conclusion de ce paragraphe on peut dire que le terme socket désigne, d'une part un ensemble de primitives, on parle des sockets de Berkeley, et d'autre part l'extrémité d'un canal de communication (point de communication) par lequel un processus peut émettre ou recevoir des données. Ce point de communication est représenté par une variable entière, similaire à un descripteur de fichier.

3 Étude des primitives

Ce paragraphe est consacré à une présentation des primitives essentielles pour programmer des applications en réseaux. Pour être bien complet il est fortement souhaitable de consulter les pages de manuels associées aux primitives et la documentation citée en fin de chapitre page [*].


3.1 Création d'une socket

La création d'une socket se fait par l'appel système socket.
#include <sys/types.h> /* Pour toutes les primitives */
#include <sys/socket.h> /* de ce chapitre il faut */
#include <netinet/in.h> /* inclure ces fichiers. */
cm

int socket(int PF, int TYPE, int PROTOCOL) ;
PF
Spécifie la famille de protocole (`` Protocol Family '') à utiliser avec la socket. On trouve (extrait) par exemple sur FreeBSD XIII4 7.0 :

PF_INET : Pour les sockets IPv4
PF_INET6 : Pour les sockets IPv6
PF_LOCAL : Pour rester en mode local (pipe)...
PF_UNIX : Idem AF_LOCAL
PF_ROUTE : Accès à la table de routage
PF_KEY : Accès à une table de clefs (IPsec)
PF_LINK : Accès à la couche `` Link ''

Mais il existe d'autres implémentations notamment avec les protocolesXIII5 :

PF_APPLETALK : Pour les réseaux Apple
PF_NS : Pour le protocole Xerox NS
PF_ISO : Pour le protocole de l'OSI
PF_SNA : Pour le protocole SNA d'IBM
PF_IPX : Protocole Internet de Novell
PF_ATM : `` Asynchronous Transfert Mode ''
... : ...

Le préfixe PF est la contraction de `` Protocol Family '' On peut également utiliser le préfixe AF, pour `` Address Family ''. Les deux nommages sont possibles ; l'équivalence est définie dans le fichier d'en-tête socket.h.

TYPE
Cet argument spécifie le type de communication désiré. En fait avec la famille PF_INET, le type permet de faire le choix entre un mode connecté, un mode non connecté ou une intervention directe dans la couche IP :

SOCK_STREAM : Mode connecté Couche transport
SOCK_DGRAM : Mode non connecté Idem
SOCK_RAW : Dialogue direct avec la couche IP  

Faut-il repréciser que seules les sockets en mode connecté permettent les liaisons `` full-duplex '' ?

PROTOCOL
Ce troisième argument permet de spécifier le protocole à utiliser. Il est du type UDP ou TCP le plus courammentXIII6.

IPPROTO_TCP : TCP
IPPROTO_SCTP : SCTP
IPPROTO_UDP : UDP
IPPROTO_RAW, IPPROTO_ICMP : uniquement avec SOCK_RAW

PROTOCOL est typiquement mis à zéro car l'association de la famille de protocole et du type de communication définit explicitement le protocole de transport :

PF_INET + SOCK_STREAM $\Longrightarrow$ TCP = IPPROTO_TCP
PF_INET + SOCK_DGRAM $\Longrightarrow$ UDP = IPPROTO_UDP
C'est une constante définie dans le fichier d'en-têtes /usr/include/netinet/in.h et qui reflète le contenu du fichier système /etc/protocols.

3.1.1 Valeur retournée par socket

La primitive socket retourne un entier qui est le descripteur de la socket nouvellement créée par cet appel.

Par rapport à la connexion future cette primitive ne fait que donner le premier élément du quintuplet :

{protocole, port local, adresse locale, port éloigné, adresse éloignée}

Si la primitive renvoie -1, la variable globale errno donne l'indice du message d'erreur idoine dans la table sys_errlist, que la bibliothèque standard sait parfaitement exploiter XIII7.

Remarque importante :

Comme pour les entrées/sorties sur fichiers, un appel système fork duplique la table des descripteurs de fichiers ouverts du processus père dans le processus fils. Ainsi les descripteurs de sockets sont également transmis.

Le bon usage du descripteur de socket partagé entre les deux processus incombe donc à la responsabilité du programmeur.

cm

Enfin, quand un processus a fini d'utiliser une socket il appelle la primitive close avec en argument le descripteur de la socket :

close(descripteur de socket) ;

Si un processus ayant ouvert des sockets vient à s'interrompre pour une raison quelconque, en interne la socket est fermée et si plus aucun processus n'a de descripteur ouvert sur elle, le noyau la supprime.

3.2 Spécification d'une adresse

Il faut remarquer qu'une socket est créée sans l'adresse de l'émetteur - comprendre le couple (numéro de port, adresse IP) - ni celle du destinataire. Il y a deux couples à préciser, celui coté client et l'autre coté serveur. La primitive bind effectue cette opération pour la socket de l'hôte local.

3.2.1 Spécification d'un numéro de port

L'usage d'un numéro de port est obligatoire. Par contre le choix de sa valeur est largement conditionné par le rôle que remplit la socket : client versus serveur.

S'il s'agit d'un serveur, l'usage d'une valeur de port `` bien connue '' est essentiel pour être accessible systématiquement par les clients (par exemple le port 25 pour un serveur SMTP ou 80 pour un serveur HTTP).

À l'inverse, le codage de la partie cliente d'une application réseau ne nécessite pas une telle précaution (sauf contrainte particulière dûe au protocole de l'application elle-même) parceque le numéro de port associé à la socket cliente est communiqué au serveur via l'en-tête de la couche de transport choisie, dès la prise de contact par le réseau.

Le serveur utilise alors la valeur lue dans l'en-tête pour répondre à la requête du client, quel que soit le choix de sa valeur initiale. L'établissement de cette valeur par le client peut donc être le résultat d'un automate, éventuellement débrayable.

3.2.2 Spécification d'une adresse IP

Pour des raisons évidentes de communication, il est nécessaire de préciser l'adresse IP du serveur avec lequel on souhaite établir un trafic réseau.

Par contre, concernant le choix sa propre adresse IP, c'est à dire celle qui va servir d'adresse pour le retour des datagrammes, un comportement par défaut peut être choisi lors de la construction de la socket, qui consiste à laisser au noyau du système le soin d'en choisir la valeur la plus appropriée.

Pour une machine unix standard mise en réseau, c'est le cas par exemple d'une station de travail, celle-ci possède au moins deux adresses IP : une sur le réseau local et une autre sur l'interface de loopback (cf page [*]). La socket est alors associée aux deux adresses IP, voire plus si la machine est du type ``  multi-homed '' (page [*]).

On peut également choisir pour sa socket un comportement plus sélectif, consistant à n'écouter que sur une seule des adresses IP de la station.

3.2.3 La primitive bind

La primitive bind effectue ce travail d'associer une socket à un couple (adresse IP, numéro de port) associés dans une structure de type sockaddr_in, pour IPv4. Mais la primitive bind est généraliste, ce qui explique que son prototype fasse état d'une structure générique nommée sockaddr, plutôt qu'à une structure dédiée d'un protocole particulier (IPv4 ici).

int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen) ;

socket : Usage du descripteur renvoyé par socket.
myaddr :
La structure qui spécifie l'adresse locale que l'on veut associer à la socket préalablement ouverte.
addrlen :
Taille en octets de la structure qui contient l'adresse.

sockaddr est constituée (dans sa forme POSIX) de deux octets qui rappellent la famille de protocole, suivis de 14 octets qui définissent l'adresse en elle-même.

3.2.4 Les structures d'adresses

Avec la présence de plus en plus effective d'IPv6, les implémentations les plus récentes tiennent compte des recommandations de la RFC 3493XIII8, ajoutent un champ sa_len d'une longueur de 8 bits et font passer de 16 à 8 bits la taille du champ sa_family pour ne pas augmenter la taille globale de la structure.

		struct	sockaddr {             /* La structure */
		    uint8_t     sa_len ;       /* generique    */
		    sa_family_t sa_family ;
		    char        sa_data[14] ;
		} ;

sa_len indique taille de la structure en octets, il est présent au même emplacement dans toutes les variantes de cette structure et contient 16 (octets) pour une structure de type sockaddr_in, ou 28 octets pour une structure de type sockaddr_in6 (IPv6).

Pour la famille PF_INET (IPv4) cette structure se nomme sockaddr_in, et est définie de la manière suivante :

struct in_addr {
    unsigned long    s_addr ;     /* 32 bits Internet	*/
} ;

struct sockaddr_in {
    uint8_t         sin_len ;     /* Taille de la structure == 16 octets */
    sa_family_t     sin_family ;  /* PF_INET (IPv4)                      */
    in_port_t       sin_port ;    /* Numero de port sur 16 bits / NBO    */
    struct in_addr  sin_addr ;    /* Adresse IP sur 32 bits / NBO        */
    char            sin_zero[8] ; /* Inutilises                          */
} ;

\includegraphics{fig.socket.03.ps}
figure XIII.03 -- Structures d'adresses

La primitive bind ne permet pas toutes les associations de numéros de port, par exemple si un numéro de port est déjà utilisé par un autre processus, ou encore si l'adresse internet est invalide.

cm

Trois utilisations typiques de la primitive :

  1. En règle général les serveurs fonctionnent avec des numéros de port bien connus (cf /etc/services). Dans ce cas bind indique au système `` c'est mon adresse, tout message reçu à cette adresse doit m'être renvoyé ''. En mode connecté ou non, les serveurs ont besoin de préciser cette information avant de pouvoir accepter les requêtes des clients.

  2. Un client peut préciser sa propre adresse, en mode connecté ou non.

  3. Un client en mode non connecté a besoin que le système lui assigne une adresse particulière, ce qui autorise l'usage des primitives read et write traditionnellement dédiées au mode connecté.

3.2.5 Valeur retournée par bind

Bind retourne 0 si tout va bien, -1 si une erreur est intervenue. Dans ce cas la variable globale errno est positionnée à la bonne valeur.

Cet appel système complète l'adresse locale et le numéro de port du quintuplet qui qualifie une connexion. Avec bind+socket on a la moitié d'une connexion, à savoir un protocole, un numéro de port et une adresse IP :

{protocole, port local, adresse locale, port éloigné, adresse éloignée}

3.3 Connexion à une adresse distante

Prendre l'initiative de l'établissement d'une connexion est typique de la démarche d'un client réseau..

La primitive connect permet d'établir la connexion avec une socket distante, supposée à l'écoute sur un port connu à l'avance de la partie cliente. Son usage principal est d'utiliser une socket en mode `` connecté ''. L'usage d'une socket en mode datagramme est possible mais a un autre sens (voir plus loin) et est moins utilisé.

La primitive connect a le prototype suivant :

int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen) ;

sockfd :
Le descripteur de socket renvoyé par la primitive socket.
servaddr :
La structure qui définit l'adresse du destinataire, du même type que pour bind.
addrlen : La longueur de l'adresse, en octets.

3.3.1 Mode connecté

Pour les protocoles orientés connexion, cet appel système rend la main au code utilisateur une fois établi le circuit virtuel entre les deux piles TCP/IP. Durant cette phase, des paquets sont échangés comme nous avons pu déjà l'examiner page [*] dans le cas de TCP.

Tant que cette connexion n'est pas complètement établie au niveau de la couche de transport, la primitive connect reste en mode noyau, et est donc bloquante vis à vis du code de l'application.

Dans le cas général, les clients n'ont pas besoin de faire appel à bind avant d'invoquer connect, la définition de la socket locale est complétée automatiquement : le port est attribué automatiquement selon une démarche décrite page [*], et l'adresse IP est l'une de celles de l'interface qu'emprunte le datagramme pour son routage initialXIII9.

3.3.2 Mode datagramme

Dans le cas d'un client en mode datagramme, un appel à connect n'est pas faux mais il ne sert à rien au niveau protocolaire, il redonne aussitôt la main au code utilisateur. Le seul intérêt que l'on peut y trouver est que l'adresse du destinataire est alors fixée et que l'on peut alors utiliser les primitives read, write, recv et send, traditionnellement réservées au mode connecté.

3.3.3 Valeur retournée par connect :

En cas d'erreur elle renvoie la valeur -1 et positionne la variable globale errno à la valeur idoine, par exemple à ETIMEOUT, s'il n'y a pas eu de réponse à l'émission des paquets de synchronisation (cf page [*]). Bien d'autres erreurs liées à des problèmes du réseau sont à consulter dans la section ERRORS de la page de manuel. Un code 0 indique que la connexion est établie sans problème particulier.

cm

Tous les éléments du quintuplet sont en place :

cm {protocole, port local, adresse locale, port éloigné, adresse éloignée} cm

3.4 Envoyer des données

Une fois qu'un programme d'application a créé une socket, il peut l'utiliser pour transmettre des données. Il y a cinq primitives possibles pour ce faire :

send, write, writev, sendto, sendmsg

3.4.1 Envoi en mode connecté

Send, write et writev fonctionnent uniquement en mode connecté, parce-qu'elles n'offrent pas la possibilité de préciser l'adresse du destinataire.

Les différences entre ces trois primitives sont mineures.

ssize_t write(int descripteur, const void *buffer, size_t longueur) ;

Quand on utilise write, le descripteur désigne l'entier renvoyé par la primitive socket. Le buffer contient les octets à transmettre, et longueur leur cardinal.

Tous les octets ne sont pas forcément transmis d'un seul coup, et ce n'est pas une condition d'erreur. En conséquence il est absolument nécessaire de tenir compte de la valeur de retour de cette primitive, négative ou non.

La primitive writev est sensiblement la même que write simplement elle permet d'envoyer un tableau de structures du type iovec plutot qu'un simple buffer, l'argument vectorlen spécifie le nombre d'entrées dans iovector :

ssize_t writev(int descriptor, const struct iovec *iovector, int vectorlen) ;

La primitive send à la forme suivante :

int send(int s, const void *msg, size_t len, int flags) ;

s
Désigne l'entier renvoyé par la primitive socket.

msg
Donne l'adresse du début de la suite d'octets à transmettre.

len
Spécifie le nombre d'octets à transmettre.

flags
Ce drapeau permet de paramètrer la transmission du data-gramme, notamment si le buffer d'écriture est plein ou si l'on désire, par exemple et avec TCP, faire un envoi en urgence ( out-of-band) :

0 : Non opérant, c'est le cas le plus courant.
MSG_OOB : Pour envoyer ou recevoir des messages out-of-band.
MSG_PEEK :
Permet d'aller voir quel message on a reçu sans le lire, c'est à dire sans qu'il soit effectivement retiré des buffers internes (ne s'applique qu'à recv (page [*]).

3.4.2 Envoi en mode datagramme

Les deux autres primitives, sendto et sendmsg donnent la possibilité d'envoyer un message via une socket en mode non connecté. Toutes deux réclament que l'on spécifie le destinataire à chaque appel.

ssize_t sendto(int s,const void *msg,size_t len,int flags, const struct sockaddr *to, socklen_t tolen) ;

Les quatre premiers arguments sont exactement les mêmes que pour send, les deux derniers permettent de spécifier une adresse et sa longueur avec une structure du type sockaddr, comme vu précédemment avec bind.

Le programmeur soucieux d'avoir un code plus lisible pourra utiliser la deuxième primitive :

ssize_t sendmsg(int sockfd, const struct msghdr *messagestruct,int flags) ;

messagestruct désigne une structure contenant le message à envoyer sa longueur, l'adresse du destinataire et sa longueur. Cette primitive est très commode à employer avec son pendant recvmsg car elle travaille avec la même structure.

3.5 Recevoir des données

Symétriquement aux cinq primitives d'envoi, il existe cinq primitives de réception : read, readv, recv, recvfrom, recvmsg.

3.5.1 Reception en mode connecté

La forme conventionnelle read d'Unix n'est possible qu'avec des sockets en mode connecté car son retour dans le code utilisateur ne s'accompagne d'aucune précision quant à l'adresse de l'émetteur. Sa forme d'utilisation est :

ssize_t read(int descripteur, void *buffer,size_t longueur) ;

Bien sur, si cette primitive est utilisée avec les sockets BSD, le descripteur est l'entier renvoyé par un appel précédent à la primitive socket. buffer et longueur spécifie respectivement le buffer de lecture et la longueur de ce que l'on accepte de lire.

Chaque lecture ne renvoie pas forcément le nombre d'octets demandés, mais peut être un nombre inférieur.

Mais le programmeur peut aussi employer le readv, avec la forme :

ssize_t readv(int descripteur, const struct iovec *iov, int vectorlen) ;

Avec les même caractéristiques que pour le readv.

En addition aux deux primitives conventionnelles, il y a trois primitives nouvelles pour lire des messages sur le réseau :

ssize_t recv(int s, void *buf, size_t len, int flags) ;

s : L'entier qui désigne la socket.
buf : Une adresse où l'on peut écrire, en mémoire.
len : La longueur du buffer.
flags : Permet au lecteur d'effectuer un contrôle sur les paquets lus.

3.5.2 Recevoir en mode datagramme

ssize_t recvfrom(int s, void *buf,size_t len, int flags,struct sockaddr *from, socklen_t *fromlen);

Les deux arguments additionnels par rapport à recv sont des pointeurs vers une structure de type sockaddr et sa longueur. Le premier contient l'adresse de l'émetteur. Notons que la primitive sendto fournit une adresse dans le même format, ce qui facilite les réponses.

La dernière primitive recvmsg est faite pour fonctionner avec son homologue sendmsg :

ssize_t recvmsg(int sockfd, struct msghdr *messagestruct,int flags) ;

La structure messagestruct est exactement la même que pour sendmsg ainsi ces deux primitives sont faites pour fonctionner de paire.

3.6 Spécifier une file d'attente

Imaginons un serveur en train de répondre à un client, si une requête arrive d'un autre client, le serveur étant occupé, la requête n'est pas prise en compte, et le système refuse la connexion.

La primitive listen est là pour permettre la mise en file d'attente des demandes de connexions.

Elle est généralement utilisée après les appels de socket et de bind et immédiatement avant le accept.

int listen(int sockfd, int backlog) ;

sockfd : l'entier qui décrit la socket.
backlog :
Le nombre de connexions possibles en attente (quelques dizaines). La valeur maximale est fonction du paramètrage du noyau. Sous FreeBSD la valeur maximale par défaut est de 128 (sans paramètrage spécifique du noyau), alors que sous Solaris 10, `` There is currently no backlog limit ''.

Le nombre de fois où le noyau refuse une connexion est comptabilisé et accessible au niveau de la ligne de commande via le résultat de l'exécution de la commande netstat -s -p tcp (chercher `` listen queue overflow ''). Ce paramètre est important à suivre dans le cas d'un serveur très sollicité.


3.7 Accepter une connexion

Accepter une connexion est typique de la démarche d'un serveur sur le réseau.

nous l'avons examiné, un serveur utilise les primitives socket, bind et listen pour se préparer à recevoir les connexions. Il manque cependant à ce trio le moyen de dire au protocole `` j'accepte désormais les connexions entrantes ''. La primitive accept est le chaînon manquant !

Quand le serveur invoque cette primitive, le noyau est prévenu que le processus est en attente d'un évênement réseau le concernant. Le retour dans le code de l'application ne fait que sous deux conditions, réception d'une demande de connexion ou réception d'un signal par le processus.

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;

Qui s'utilise comme suit :

	int newsock ;
	newsock = accept(sockfd, addr, addrlen) ;

sockfd
descripteur de la socket, renvoyé par la primitive du même nom.

addr
Un pointeur sur une structure du type sockaddr.

addlen
Un pointeur sur un entier.

Quand une connexion arrive, les couches sous-jacentes du protocole de transport remplissent la structure addr avec l'adresse du client qui fait la demande de connexion. Addrlen contient alors la longueur de cette adresse. Cette valeur peut être modifiée par le noyau lorsque la primitive est utilisée avec des sockets d'autres type pour lesquelles la taille de la structure d'adresse est variable (sockaddr_un pour les sockets locales par exemple), ce qui justifie un pointeur là où nous ne pourrions attendre qu'un simple passage d'argument par valeur.

Puis le système crée une nouvelle socket par clonage de celle transmise et pour laquelle il renvoie un descripteur, récupéré ici dans newsock. Par cet artifice, la socket originale reste disponible pour d'éventuelles autres connexions (elle est clonée avant que le quintuplet soit complet).

En conclusion, lorsqu'une demande de connexion arrive, l'appel à la primitive accept redonne la main au code utilisateur.


3.8 Terminer une connexion

Dans le cas du mode connecté on termine une connexion avec la primitive close ou shutdown.

int close(descripteur) ;

La primitive bien connue sous Unix peut être aussi employée avec un descripteur de socket. Si cette dernière est en mode connecté, le système assure que tous les octets en attente de transmission seront effectivement transmis dans de bonnes conditions. Normalement cet appel retourne immédiatement, cependant le kernel peut attendre le temps de la transmission des derniers octets (transparent).

Le moyen le plus classique de terminer une connexion est de passer par la primitive close, mais la primitive shutdown permet un plus grand contrôle sur les connexions en `` full-duplex ''.

int shutdown(int sockfd, int how) ;

Sockfd est le descripteur à fermer, how permet de fermer partiellement le descripteur suivant les valeurs qu'il prend :

0
Aucune donnée ne peut plus être reçue par la socket.
1
Aucune donnée ne peut plus être émise.
2
Combinaison de 0 et de 1 (équivalent de close).

Enfin, pour une socket en mode connecté, si un processus est interrompu de manière inopinée (réception d'un signal de fin par exemple), un `` reset '' (voir page [*]) est envoyé à l'hôte distant ce qui provoque la fin brutale de la connexion. Les octets éventuellement en attente sont perdus.

4 Schéma général d'une session client-serveur

Il est temps de donner un aperçu de la structure d'un serveur et d'un client, mettant en \oeuvre les APIs vus dans ce chapitre, et de rapprocher les évênements réseaux de ceux observables sur le système et dans le processus qui s'exécute.

Relation client-serveur en mode connecté :

\includegraphics{fig.socket.04.ps} figure XIII.04 -- Relation client-serveur en mode connecté

Il faut établir une comparaison entre cette figure et les figures VI.03 page [*] et VI.04 page [*]. Les sockets coté client ou coté serveur, si elles participent à l'établissement d'un canal de communication symétrique en fonctionnement, ne passent pas par les mêmes états, de leur création jusqu'au recyclage des octets qui les composent.

La RFC 793 précise 11 états pour une socket et la figure ci-dessus les met en situation de manière simplifiée. Ces états peuvent être visualisés avec la commande netstat -f inet [-a], dans la colonne state de la sortie.

LISTEN
La socket du serveur est en attente passive d'une demande de connexion (ouverture passive).

SYN-SENT
C'est l'état de la socket cliente qui a envoyé le premier paquet de demande d'une connexion avec un flag SYN mais non encore acquitté (ouverture active).

SYN-RCVD
La socket du serveur à reçu un paquet de demande de connexion, l'acquitte et envoi sa propre demande de connexion. Elle attend l'acquittement de sa demande.

ESTABLISHED
Les demandes de connexions sont acquittées aux deux extrémités. La connexion est établie. La totalité du trafic TCP applicatif s'effectue dans cet état. Sa durée est indéfinie, la clôture est à l'initiative des applications.

FIN-WAIT-1
Celui qui est à l'initiative de l'envoi du premier paquet de demande de fin est dans cet état (fermeture active).

FIN-WAIT-2
On a reçu l'acquittement à la demande de fin de connexion.

TIME-WAIT
La socket était en FIN-WAIT-2 et a reçu la demande de fin de la socket distante. On doit attendre le temps suffisant pour être certain que la socket distante a bien reçu l'acquittement (re-émission sinon). Cet état peut donc être long dans le temps, 2 x MSL précise la RFC 793. Cette constante peut aller de quelques dizaines de secondes à une ou deux minutes selon les implémentations.

CLOSE-WAIT
La socket était en ESTABLISHED et a reçu une demande de fin. Cet état perdure jusqu'à ce que la socket envoie à son tour une demande de fin (fermeture passive).

CLOSING
Si la réponse à une demande de fin s'accompagne immédiatement de la demande de fin de la socket locale, cet état remplace FIN-WAIT-1 et FIN-WAIT-2.

LAST-ACK
La dernière demande de fin est en attente du dernier acquittement.

CLOSED
État de fin. Les octets de la socket vont être recyclés.

L'état TIME-WAIT est supporté par celui qui clôt la connexion. Les architectures de serveurs préfèrent une clôture à l'initiative du serveur, ce qui se comprend du point de vue de l'efficacité (rester maître de la durée de la communication), mais le fonctionnement interne du protocole TCP implique ce temps d'attente. Sur un serveur très chargé les sockets dans cet état peuvent être en très grand nombre (des dizaines de milliers...) bloquant ainsi les nouvelles connexions entrantes.

cm

Relation client-serveur en mode non connecté :

\includegraphics{fig.socket.05.ps}
figure XIII.05 -- Relation client-serveur en mode non connecté

5 Exemples de code `` client ''

L'objectif de ces deux exemples est de montrer le codage en C et le fonctionnement d'un client du serveur de date (RFC 867, `` daytime protocol '') présent sur toute machine unixXIII10.

Ce serveur fonctionne en mode connecté ou non, sur le port 13 qui lui est réservé (/etc/services). Ici le serveur est une machine portant l'adresse IP 192.168.52.232. La connaissance de l'adresse IP du client n'est absolument pas utile pour la compréhension de l'exemple.

En mode TCP le simple fait de se connecter provoque l'émission de la chaîne ascii contenant la date vers le client et sa déconnexion.

En mode UDP il suffit d'envoyer un datagramme quelconque (1 caractère) au serveur puis d'attendre sa réponse.

5.1 Client TCP `` DTCPcli ''

Exemple d'usage :

$ ./DTCPcli 192.168.52.232
Date(192.168.52.232) = Wed Dec 10 20:59:46 2003
Une capture des trames réseau échangées lors de l'exécution de cette commande se trouve page [*].

ligne 29
Déclaration de la structure saddr du type sockaddr_in, à utiliser avec IPv4. Attention, il s'agit bien d'une structure et non d'un pointeur de structure.

ligne 35
La variable sfd, reçoit la valeur du descripteur de socket. Celle-ci est dédiée au protocole TCP.

ligne 39
Le champ sin_family de la structure saddr indique que ce qui suit (dans la structure) concerne IPv4.

ligne 40
Le champ sin_port est affecté à la valeur du numéro de port sur lequel écoute le serveur. Il faut remarquer l'usage de la fonction htons (en fait une macro du pré-processeur cpp) qui s'assure que ce numéro de port respecte bien le NBO (`` Network Byte Order ''), car cette valeur est directement recopiée dans le champ PORT DESTINATION du protocole de transport employé (voir page [*] pour UDP et page [*] pour TCP).

Nous en dirons plus sur htons au chapitre suivant.

Si le processeur courant est du type `` little endian '' (architecture Intel par exemple) les octets sont inversés (le NBO est du type `` big endian ''). Vous croyez écrire 13 alors qu'en réalité pour le réseau vous avez écrit 3328 (0x0D00) ce qui bien évidement ne conduit pas au même résultat, sauf si un serveur de date écoute également sur le port 3328, non conventionnel donc très peu probable à priori.

En résumé, si le programmeur n'utilise pas la fonction htons, ce code n'est utilisable que sur les machines d'architecture `` big endian ''.

\includegraphics{DTCPcli.eps} Source du client `` DTCPcli.c ''

ligne 41
Le champ s_addr de la structure sin_addr se voit affecté de l'adresse IP. C'est donc l'écriture de quatre octets (IPv4), pouvant comporter un caractère ascii 0, donc interprétable comme le caractère de fin de chaîne du langage C.

C'est pourquoi à cet endroit on ne peut pas employer les habituelles fonctions de la bibliothèque standard (celles qui commencent par str).

Ici le problème se complique un peu dans la mesure où l'on dispose au départ d'une adresse IP sous sa forme décimale pointée. La gestion d'erreur protège le code des erreurs de syntaxe à la saisie.

La fonction inet_pton gère parfaitement ce cas de figure. Nous en dirons plus à son sujet au chapitre suivant.

ligne 45
Appel à la primitive connect pour établir la connexion avec le serveur distant. Quand celle-ci retourne dans le code du programme, soit la connexion a échoué et il faut traiter l'erreur, soit la connexion est établie. L'échange préliminaire des trois paquets s'est effectué dans de bonnes conditions (page [*]).

Du point de vue TCP, les cinq éléments du quintuplet qui caractérisent la connexion sont définis (page [*]).

Sur la capture des paquets de la page [*] nous sommes arrivés à la ligne 6, c'est à dire l'envoi de l'acquittement par le client du paquet de synchronisation envoyé par le serveur (ligne 3 et 4).

Il faut noter que bien que nous ayons transmis la structure saddr par adresse (caractère &) et non par valeur, la primitive connect ne modifie pas son contenu pour autant.

Notons également l'usage du `` cast '' du C pour forcer le type du pointeur (le prototype de la primitive exige à cet endroit un pointeur de type sockaddr).

ligne 49
Appel à la primitive read pour lire le résultat en provenance du serveur, à l'aide du descripteur sfd.

Sur la capture d'écran on voit ligne 8 (et 9) l'envoi de la date en provenance du serveur, d'une longueur de 26 caractères.

Ce nombre de caractères effectivement lus est affecté à la variable n.

Ce nombre ne peut excéder le résultat de l'évaluation de MAXMSG - 1, qui correspond à la taille du buffer buf moins 1 caractère prévu pour ajouter le caractère 0 de fin de chaîne.

En effet, celui-ci fait partie de la convention de représentation des chaînes de caractères du langage C. Rien ne dit que le serveur qui répond ajoute un tel caractère à la fin de sa réponse. Le contraire est même certain puisque la RFC 867 n'y fait pas mention.

Remarque : le buffer buf est largement surdimensionné compte tenu de la réponse attendue. La RFC 867 ne prévoit pas de taille maximum si ce n'est implicitement celle de l'expression de la date du système en anglais, une quarantaine d'octets au maximum.

ligne 53
Ajout du caractère de fin de chaîne en utilisant le nombre de caractères renvoyés par read.

ligne 55
La sortie du programme induit une clôture de la socket coté client. Coté serveur elle est déjà fermée (séquence write + close) comme on peut le voir ligne 8 (flag FP, page [*]) ci-après dans la capture du trafic entre le client et le serveur.

Remarque : rien n'est explicitement prévu dans le code pour établir la socket coté client, à savoir l'association d'un numéro de port et d'une adresse IP. En fait c'est la primitive connect qui s'en charge. L'adresse IP est celle de la machine. Le numéro de port est choisi dans la zone d'attribution automatique comme nous l'avons examiné page [*].

Il existe bien entendu une possibilité pour le programme d'avoir connaissance de cette information : la primitive getsockname.

\includegraphics{DTCP-tcpdump.eps} Trafic `` daytime '' TCP, capturé avec tcpdump

Un autre exemple d'interrogation, mais avec un autre hôte du même LAN mais sur lequel le service daytime n'est pas en fonctionnement :

$ ./DTCPcli 192.168.52.232
connect: Connection refused

\includegraphics{DTCP-tcpdump-2.eps} Trafic `` daytime '' TCP (reset), capturé avec tcpdump

L'envoi d'un reset (drapeau R) envoyé par le serveur en guise de réponse est bien visible ligne 4.

5.2 Client UDP `` DUDPcli ''

\includegraphics{DUDPcli.eps} Source du client `` DUDPcli.c ''
Exemple d'usage :

$ ./DUDPcli 192.168.52.232
Date(192.168.52.232) = Wed Dec 10 20:56:58 2003

ligne 34
Ouvertude d'une socket UDP, donc en mode non connecté.

ligne 45
Envoit d'un caractère (NULL) au serveur, sans quoi il n'a aucun moyen de connaître notre existence.

ligne 38, 39 et 40
Le remplissage de la structure saddr est identique à celui de la version TCP.

ligne 49
Réception de caractères en provenance du réseau.

Il faut remarquer que rien n'assure que les octets lus sont bien en provenance du serveur de date interrogé.

Nous aurions pu utiliser la primitive recvfrom dont un des arguments est une structure d'adresse contenant justement l'adresse de la socket qui envoie le datagramme (ici la réponse du serveur).

Le raisonnement sur la taille du buffer est identique à celui de la version TCP.

La capture de trames suivante montre l'extrême simplicité de l'échange en comparaison avec celle de la version utilisant TCP !

\includegraphics{DUDP-tcpdump.eps} Trafic `` daytime '' UDP, capturé avec tcpdump

Un autre essai avec la machine 192.168.52.233 qui ne répond pas plus sur le port 13 en UDP :

\includegraphics{DUDP-tcpdump-2.eps} Trafic `` daytime '' UDP (icmp), capturé avec tcpdump

Et le code client reste bloqué en lecture, malgré l'envoi d'un code ICMP qui n'est pas interprété par défaut par recv... Pour éviter une telle situation de blocage, il faudrait configurer la socket en lui ajoutant un délai au delà duquel elle retourne dans le code du client avec un code spécifique d'erreurXIII11.


6 Conclusion et Bibliographie

En conclusion on peut établir le tableau suivant :

Protocole Adresses locale Adresse éloignée
et N$^{\circ }$ de port. et N$^{\circ }$ de port.
Serveur orienté connexion socket bind listen, accept
Client orienté connexion socket connect
Serveur non orienté connexion socket bind recvfrom
Client non orienté connexion socket bind sendto

cm

RFC 867
`` Daytime Protocol ''. J. Postel. May-01-1983. (Format: TXT=2405 bytes) (Also STD0025) (Status: STANDARD)

RFC 793
`` Transmission Control Protocol. '' J. Postel. September 1981. (Format: TXT=172710 bytes) (Updated by RFC3168) (Also STD0007) (Status: STANDARD)

RFC 3493
`` Basic Socket Interface Extensions for IPv6 ''. R. Gilligan, S. Thomson, J. Bound, J. McCann, W. Stevens. February 2003. (Format: TXT=82570 bytes) (Obsoletes RFC2553) (Status: INFORMATIONAL)

Pour en savoir davantage, outre les pages de man des primitives citées dans ce chapitre, on pourra consulter les documents de référence suivants :

  • Stuart Sechrest -- `` An Introductory 4.4BSD Interprocess Communication Tutorial '' -- Re imprimé dans `` Programmer's Supplementary Documents '' -- O'Reilly & Associates, Inc. -- 1994XIII12

  • W. Richard Stevens -- `` Unix Network Programming '' -- Prentice All -- 1990

  • W. Richard Stevens -- `` Unix Network Programming '' -- Second edition -- Prentice All -- 1998

  • W. Richard Stevens - Bill Fenner, Andrew M. Rudoff -- `` Unix Network Programming '' -- Third Edition -- Addison Wesley -- 2003

  • Douglas E. Comer - David L. Stevens -- `` Internetworking with TCP/IP - Volume III '' (BSD Socket version) -- Prentice All -- 1993

  • Stephen A. Rago -- `` Unix System V Network Programming '' -- Addison-Wesley -- 1993

Et pour aller plus loin dans la compréhension des mécanismes internes :

  • W. Richard Stevens -- `` TCP/IP Illustrated Volume 2 '' -- Prentice All -- 1995

  • McKusick - Bostic - Karels - Quaterman -- `` The Design and implementation of the 4.4 BSD Operating System '' -- Addison-Wesley -- 1996


next up previous contents index
Next: XIV Compléments sur les Up: D Sockets BSD et Previous: D Sockets BSD et   Contents   Index
Fran├žois Laissus 2009-02-27