IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours d'introduction à TCP/IP


précédentsommairesuivant

13. Généralités sur les sockets de Berkeley

13-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 sockets(166).

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.

Image non disponible
figure XII.01 - Les sockets, une famille de primitives

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

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, SUN(167), IBM, SGI, ont adopté ces sockets, ainsi sont-elles devenues un standard de fait et une justification de leur étude.

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.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.

Image non disponible
figure XII.02 - Relation pile IP, numéro de port et process ID

13-2. Présentation des sockets

Les créateurs des sockets ont essayé au maximum de conserver la sémantique des primitives système 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 jeu 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 :

  • 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 ;
  • 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 datagrammes à plusieurs autres processus sur des machines différentes ;
  • Une connexion est définie par un quintuplet (cf. chapitre TCP) qui est beaucoup plus compliqué qu'un simple nom de fichier ;
  • L'interface réseau supporte de multiples protocoles comme XNS, IPX, APPLETALK(168), 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.

13-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.

13-3-1. Création d'une socket

La création d'une socket se fait par l'appel système socket.

 
Sélectionnez
#include <sys/types.h> /* Pour toutes les primitives */
#include <sys/socket.h> /* de ce chapitre il faut */
#include <netinet/in.h> /* inclure ces fichiers. */
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 (169) 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 protocoles(170) :

  • 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'entêtes 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 couramment(171) ;

  • 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 ==> TCP = IPPROTO TCP ;
  • PF INET + SOCK DGRAM ==> UDP = IPPROTO UDP.

C'est une constante définie dans le fichier d'entêtes /usr/include/netinet/in.h et qui reflète le contenu du fichier système /etc/protocols.

13-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(172).

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 transmises.

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

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

 
Sélectionnez
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.

13-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 côté client et l'autre côté serveur. La primitive bind effectue cette opération pour la socket de l'hôte local.

13-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 due au protocole de l'application elle-même) parce que le numéro de port associé à la socket cliente est communiqué au serveur via l'entê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'entê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.

13-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. La socket est alors associée aux deux adresses IP, voire plus si la machine est du type « multihomed ».

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.

13-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).

 
Sélectionnez
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.

13-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 3493(173), 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.

 
Sélectionnez
struct sockaddr { /* La structure */
uint8_t sa_len ; /* generique */
sa_family_t sa_family ;
char sa_data[14] ;
} ;
  • sa len indique la 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 :

 
Sélectionnez
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 */
} ;
Image non disponible
figure XII.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.

Trois utilisations typiques de la primitive :

  • En règle générale 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 ;
  • Un client peut préciser sa propre adresse, en mode connecté ou non ;
  • 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é.
13-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}

13-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 :

 
Sélectionnez
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.
13-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 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 plus tôt, et l'adresse IP est l'une de celles de l'interface qu'emprunte le datagramme pour son routage initial(174).

13-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é.

13-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. 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.

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

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

13-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.
13-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.

 
Sélectionnez
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, longueur et 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 plutôt qu'un simple buffer, l'argument vectorlen spécifie le nombre d'entrées dans iovector :

 
Sélectionnez
ssize t writev(int descriptor, const struct iovec *iovector, int vectorlen) ;

La primitive send à la forme suivante :

 
Sélectionnez
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 datagramme, 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).
13-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.

 
Sélectionnez
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 :

 
Sélectionnez
ssize t sendmsg(int sockfd, const struct msghdr *messagestruct,int flags) ;

Où 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.

13-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.

13-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 :

 
Sélectionnez
ssize t read(int descripteur, void *buffer,size t longueur) ;

Bien sûr, 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 :

 
Sélectionnez
ssize t readv(int descripteur, const struct iovec *iov, int vectorlen) ;

Avec les mêmes 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 :

 
Sélectionnez
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.
13-3-5-2. Recevoir en mode datagramme
 
Sélectionnez
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 :

 
Sélectionnez
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.

13-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.

 
Sélectionnez
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é.

13-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.

 
Sélectionnez
1.
int accept(int sockfd, struct sockaddr *addr, socklen t *addrlen) ;

Qui s'utilise comme suit :

 
Sélectionnez
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 types 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.

13-3-8. Terminer une connexion

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

 
Sélectionnez
1.
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 ».

 
Sélectionnez
1.
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» est envoyé à l'hôte distant ce qui provoque la fin brutale de la connexion. Les octets éventuellement en attente sont perdus.

13-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 œuvre les API vues dans ce chapitre, et de rapprocher les événements réseau de ceux observables sur le système et dans le processus qui s'exécute.

Image non disponible
figure XII.04 - Relation client-serveur en mode connecté

Il faut établir une comparaison entre cette figure et les figures VI.03 et VI.04. Les sockets côté client ou côté 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 a reçu un paquet de demande de connexion, l'acquitte et envoie 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 ( réé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.

Relation client-serveur en mode non connecté :

Image non disponible
figure XII.05 - Relation client-serveur en mode non connecté

13-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 Unix(175).

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.

13-5-1. Client TCP « DTCPcli »

Exemple d'usage :

 
Sélectionnez
$ ./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 plus loin.

  • 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é.

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 évidemment 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 a 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 ».

Source du client « DTCPcli.c »
Sélectionnez
/* $Id: DTCPcli.c 92 2009-02-12 17:39:44Z fla $
 *
 * Client TCP pour se connecter au serveur de date (port 13 - RFC 867).
 * La syntaxe d'utilisation est : DTCPcli <adresse ip sous forme décimale>
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sysexits.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define USAGE "Usage:%s adresse IP du serveur\n"
#define MAXMSG 1024
#define NPORT 13

int
main(int argc, char *argv[])
{
int n, sfd ;
char buf[MAXMSG] ;
struct sockaddr_in saddr ;

if (argc != 2) {
(void)fprintf(stderr,USAGE,argv[0]) ;
exit(EX_USAGE) ;
}
if ((sfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0) {
perror("socket") ;
exit(EX_OSERR) ;
}
saddr.sin_family = AF_INET ;
saddr.sin_port = htons(NPORT) ; /* Attention au NBO ! */
if(inet_pton(AF_INET,argv[1],&saddr.sin_addr) != 1) {
(void)fprintf(stderr,"Address «%s» is not parseable !\n",argv[1] ) ;
exit(EX_DATAERR) ;
}
if (connect(sfd,(struct sockaddr *)&saddr,sizeof saddr) < 0) {
perror("connect") ;
exit(EX_OSERR) ;
}
if ((n = read(sfd, buf,MAXMSG-1)) < 0) {
perror("read") ;
exit(EX_OSERR) ;
}
buf[n] = '\0' ;
(void)printf("Date(%s) = %s\n",argv[1],buf) ;
exit(EX_OK) ; /* close(sfd) implicite */
}
  • 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 inetpton 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.

Du point de vue TCP, les cinq éléments du quintuplet qui caractérisent la connexion sont définis.

Sur la capture des paquets plus loin 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 côté client. côté serveur elle est déjà fermée (séquence write + close) comme on peut le voir ligne 8 (flag FP) 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 côté 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é.

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

Trafic « daytime» TCP, capturé avec tcpdump
Sélectionnez
23:03:29.465183 client.2769 > serveur.daytime: S 2381636173:2381636173(0)
win 57344 <mss 1460,nop,wscale 0,nop,nop,timestamp 299093825 0> (DF)
23:03:29.465361 serveur.daytime > client.2769: S 3179731077:3179731077(0)
ack 2381636174 win 57344 <mss 1460,nop,wscale 0,nop,nop,timestamp
4133222 299093825> (DF)
23:03:29.465397 client.2769 > serveur.daytime: . ack 1 win 57920
<nop,nop,timestamp 299093826 4133222> (DF)
23:03:29.466853 serveur.daytime > client.2769: FP 1:27(26) ack 1 win 57920
<nop,nop,timestamp 4133223 299093826> (DF)
23:03:29.466871 client.2769 > serveur.daytime: . ack 28 win 57894
<nop,nop,timestamp 299093826 4133223> (DF)
23:03:29.467146 client.2769 > serveur.daytime: F 1:1(0) ack 28 win 57920
<nop,nop,timestamp 299093826 4133223> (DF)
23:03:29.467296 serveur.daytime > client.2769: . ack 2 win 57920
<nop,nop,timestamp 4133223 299093826> (DF)

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 :

Trafic « daytime » TCP (reset), capturé avec tcpdump
Sélectionnez
$ ./DTCPcli 192.168.52.232
connect: Connection refused
16:13:21.612274 IP client.57694 > serveur.daytime: S 2248945646:2248945646(0)
win 65535 <mss 1460,nop,nop,sackOK,nop,wscale 1,nop,nop,
timestamp 360942290 0>
16:13:21.612648 IP serveur.daytime > client.57694: R 0:0(0) ack 2248945647 win 0

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

13-5-2. Client UDP « DUDPcli»

Source du client « DUDPcli.c»
Sélectionnez
/* $Id: DUDPcli.c 92 2009-02-12 17:39:44Z fla $
*
* Client UDP pour se connecter au serveur de date (port 13 - RFC 867).
* La syntaxe d'utilisation est : DUDPcli <adresse ip sous forme décimale>
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sysexits.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define USAGE "Usage:%s adresse IP du serveur\n"
#define MAXMSG 1024
#define NPORT 13

int
main(int argc, char *argv[])
{
int n, sfd ;
char buf[MAXMSG] ;
struct sockaddr_in saddr ;

if (argc != 2) {
(void)fprintf(stderr,USAGE,argv[0]) ;
exit(EX_USAGE) ;
}
if ((sfd = socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP)) < 0) {
perror("socket") ;
exit(EX_OSERR) ;
}
saddr.sin_family = AF_INET ;
saddr.sin_port = htons(NPORT) ; /* Attention au NBO ! */
if (inet_pton(PF_INET,argv[1],&saddr.sin_addr) != 1) {
(void)fprintf(stderr,"Address «%s» is not parseable !\n",argv[1] ) ;
exit(EX_DATAERR) ;
}
buf[0] = '\0' ;
if (sendto(sfd,buf,1,0,(struct sockaddr *)&saddr, sizeof saddr) != 1) {
perror("sendto") ;
exit(EX_OSERR) ;
}
if ((n=recv(sfd,buf,MAXMSG-1,0)) < 0) {
perror("recv") ;
exit(EX_OSERR) ;
}
buf[n] = '\0' ;
(void)printf("Date(%s) = %s\n",argv[1],buf) ;
exit(EX_OK) ; /* close(sfd) implicite */
}

Exemple d'usage :

 
Sélectionnez
$ ./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 : envoi 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 !

Trafic « daytime » UDP, capturé avec tcpdump
Sélectionnez
23:23:17.668689 client.4222 > serveur.daytime: udp 1
23:23:17.670175 serveur.daytime > client.4222: udp 26

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

Trafic « daytime » UDP (icmp), capturé avec tcpdump
Sélectionnez
16:29:42.090816 IP client.55822 > serveur.daytime: UDP, length: 1
16:29:42.091205 IP serveur > client: icmp 36: serveur udp port daytime
unreachable

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'erreur(176).

13-6. Conclusion et Bibliographie

En conclusion on peut établir le tableau suivant :

  Protocole Adresse locale et N°de port Adresse éloignée et N° de port
Serveur orienté connexion socket bind listen, accept
Client orienté connexion socket connect connect
Serveur non orienté connexion socket bind recvfrom
Client non orienté connexion socket bind sendto
  • 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. - 1994(177)
  • 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

précédentsommairesuivant
Pour un historique très intéressant de cette période, on pourra consulter http://www.oreillynet.com/pub/a/network/2000/03/17/bsd.html.
Les applications natives de ce constructeur utilisent les TLI, par contre il est possible d'utiliser les sockets dans toutes les applications que l'on recompile soi-même, elles sont présentes dans des bibliothèques précisées lors de la compilation des sources.
L'inspection du fichier /usr/include/sys/socket.h sous FreeBSD 6.x en explicite une petite quarantaine.
www.freebsd.org.
On en compte 38 sur une machine FreeBSD 7.0 (22/10/2008).
Il en existe au moins un autre pour les sockets de type PF INET et PF INET6, nommé SCTP et qui n'est pas (encore) traité dans ce cours.
cf man errno ou la page de manuel de perror(3).
Basic Socket Interface Extensions for IPv6.
Est-il nécessaire de rappeler qu'une interface peut comporter plusieurs adresses IP et qu'il peut y avoir plusieurs interfaces réseau sur un même hôte... ?
Son activation est éventuellement à faire à partir du serveur de serveur inetd.
Voir la page de manuel de setsockopt assortie du paramètre SO RCVTIMEO.
On peut également trouver ce document dans le répertoire /usr/share/doc/psd/20.ipctut/ des OS d'inspiration Berkeley 4.4

Copyright © 2009 François Laissus. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.