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...
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
de port et
processus.
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 :
) qui est beaucoup plus compliqué
qu'un simple nom de fichier.
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.
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
.
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. */ |
| 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.
| 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 '' ?
| 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 |
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 :
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.
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).
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 */
} ;
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 :
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 :
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.
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.
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
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 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.
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 :
La primitive send à la forme suivante :
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.
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 :
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.
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 :
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 :
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 :
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
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 :
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.
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é.
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 ''.
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.
Qui s'utilise comme suit :
int newsock ; newsock = accept(sockfd, addr, addrlen) ;
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.
Dans le cas du mode connecté on termine une connexion avec la primitive close ou shutdown.
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 ''.
Sockfd est le descripteur à fermer, how permet de fermer partiellement le descripteur suivant les valeurs qu'il prend :
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
uvre 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é :
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.
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é :
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.
Exemple d'usage :
$ ./DTCPcli 192.168.52.232 Date(192.168.52.232) = Wed Dec 10 20:59:46 2003Une capture des trames réseau échangées lors de l'exécution de cette commande se trouve page
.
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 ''.
pour UDP et page
pour TCP).
Source du client `` DTCPcli.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.
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
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).
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.
).
).
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).
) 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.
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
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.
Source du client `` DUDPcli.c ''
$ ./DUDPcli 192.168.52.232 Date(192.168.52.232) = Wed Dec 10 20:56:58 2003
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
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
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.
En conclusion on peut établir le tableau suivant :
Protocole
Adresses locale
Adresse éloignée
et N
de port. et N
de port.