Au chapitre précédent nous avons utilisé la primitive bind
pour assigner une adresse à une socket, dans ce paragraphe nous
précisons comment choisir le numéro de port qui va bien, selon le type
d'application envisagé. Nous avons déjà examiné ce point dans les grandes
lignes page
.
Il y a deux manières d'assigner un N
de port à une socket :
En règle générale le développeur d'application ne s'attribue pas au hasard un (ou plus) numéro de port. Il doit respecter quelques contraintes comme ne pas utiliser les ports déjà attribués. Ceux-ci figurent dans une RFC particulière. La dernière en date est la RFC 1700 [Reynolds & Postel 1994]) au paragraphe `` WELL KNOWN PORT NUMBERS ''. Plus simplement, sur toute machine Unix à jour, une liste de ces ports se trouve dans le fichier /etc/services XIV1.
Codé sur deux octets non signés, le numéro de port permet 65536 possibilités de 0 à 65535. Cette échelle est fragmentée de deux manières, l'ancienne ou la nouvelle méthode. Toutes les applications sur tous les systèmes d'exploitation n'ont pas encore adopté la nouvelle approche, les deux vont donc cohabiter un certain temps, ne serait-ce qu'à cause d'applications plus anciennes non encore mises à jour...
1.1 Réservation de port -- Ancienne méthode
0
1.2 Réservation de port -- Nouvelle méthode
0
2 Ordre des octets sur le réseau
Nous reprenons ici un point déjà évoqué page
:
figure XIV.01 -- Ordre des octets sur le réseau
Le problème de l'ordre des octets sur le réseau est d'autant plus crucial que l'on travaille dans un environnement avec des architectures hétérogènes.
La couche Réseau (page
) ne transforme
pas les octets de la couche Internet
(page
) qui elle même ne modifie pas ceux de la
couche de TransportXIV2(page
).
Pour cette raison, le numéro de port inscrit dans l'en-tête TCP (vs UDP) de l'émetteur est exploité tel quel par la couche de transport du récepteur et donc il convient de prendre certaines précautions pour s'assurer que les couches de même niveau se comprennent.
D'un point de vue plus général, les réseaux imposent le `` poids fort '' avant le `` poids faible '', c'est le `` Network Byte Order ''. Les architectures qui travaillent naturellement avec cette représentation n'ont donc théoriquement pas besoin d'utiliser les fonctions qui suivent, de même pour les applications qui doivent dialoguer avec d'autres ayant la même architecture matérielle. Néanmoins écrire du code `` portable '' consiste à utiliser ces macros dans tous les casXIV3 !
Pour se souvenir de ces fonctions, il faut connaître la signification des quatre lettres utiles :
s
`` short ''
Entier court sur 16 bits, un numéro de port par exemple.
l
`` long ''
Entier long sur 32 bits, une adresse IP par exemple.
h
`` host ''
La machine sur laquelle s'exécute le programme.
n
`` network ''
Le réseau sur lequel on envoie les données.
D'où les protoptypes :
| #include | <sys/types.h> |
| u_long | htonl | (u_long) ; | /* host to network -- long */ |
| u_short | htons | (u_short) ; | /* host to network -- short */ |
| u_long | ntohl | (u_long) ; | /* network to host -- long */ |
| u_short | ntohs | (u_short) ; | /* network to host -- short */ |
cm
Par exemple, pour affecter le numéro de port 13 (service `` daytime '') au champ sin_port d'une structure de type sockaddr_in :
cm
Cette écriture est valable quelle que soit l'architecture sur laquelle elle est compilée. S'il avait fallu se passer de la macro htons sur une architecture Intel (`` little endian ''), pour l'affection du même numéro de port, il eut fallu écrire :
Dans le même ordre d'idée qu'au paragraphe précédent, les réseaux interconnectent des architectures hétérogènes et donc aussi des conventions de réprésentation des chaînes de caractères différentes. Pour être plus précis, le caractère NULL marqueur de fin de chaîne bien connu des programmeurs C, n'est pas valide partout, voire même est associé à une autre signification !
En conséquence, pour toutes les fonctions et primitives qui lisent et écrivent des octets sur le réseau, les chaînes de caractères sont toujours associées au nombre de caractères qui les composent.
Le corollaire est que les fonctions `` classiques '' de manipulation de chaînes en C (strcpy, strcat, ...) ne sont à utiliser qu'avec une extrême prudence.
Pour copier des octets d'une zone vers une autre il faut utiliser bcopy, pour comparer deux buffers, bcmp, enfin pour mettre à zéro (remplir d'octets NULL) une zone, bzero.
| #include | <string.h> |
| void | bcopy (const void *src, void *dst, size_t len) ; | |
| int | bcmp (const void *b1, const void *b2, size_t len) ; | |
| void | bzero (const void *b, size_t len) ; |
Il exite des outils similaires, issus du système V : memcpy, memcmp, memset,....
La plupart du temps, par exemple dans une forme de saisie pour la configuration d'un appareil, les adresses IP sont fournies par l'utilisateur dans le format `` décimal pointé '', or la structure d'adresse (sockaddr_in) a besoin d'un entier non signé sur 32 bits qui respecte le NBO. Une conversion est donc nécessaire pour passer d'une représentation à une autre.
4.1 Conversion d'adresse - IPv4 seul
La fonction inet_addr convertit une adresse décimale pointée en un entier long non signé et qui respecte le NBO. La fonction inet_ntoa effectue le travail inverse. Ces deux fonctions ne sont valables que pour les adresses 32 bits d'IPv4 et sont présentes dans la majeure partie des codes réseaux. Dans le cadre d'un nouveau développement on leur préfèrera les fonctions décrites après.
cm
| #include | <sys/socket.h> |
| #include | <netinet/in.h> |
| #include | <arpa/inet.h> |
cm
| in_addr_t | inet_addr | (char *) ; /* in_addr_t == unsigned long */ |
| char * | inet_ntoa | (struct in_addr) ; |
cm
Remarque : Ces deux fonctions ne sont pas symétriques, le fait que inet_addr ne renvoie pas une structure du type in_addr n'est pas une inconsistance mais est dû au fait que la structure in_addr était prévue au départ pour évoluer.
cm Exemple d'utilisation :
struct sockaddr_in saddr ;
if ((saddr.sin_addr.s_addr = inet_addr("138.195.52.130")) != INADDR_NONE)
printf("Adresse IP = %s\n",inet_ntoa(saddr.sin_addr)) ;
else
printf("Erreur sur l'argument de 'inet_addr' !\n" );
Il faut juste noter qu'en cas d'erreur la fonction inet_addr renvoie la constante INADDR_NONE.
4.2 Conversion d'adresse - Compatible IPv4 et IPv6
Avec les mêmes fichiers d'en-tête, les deux nouvelles fonctions de conversion :
cm
| const char * | inet_ntop | (int af, const void *src, char *dst, size_t size); |
| int | inet_pton | (int af, const char *src, void *dst); |
cm
Le p signifie `` presentation '', comprendre lisible par l'humain, alors que le n signifie `` numeric '', c'est à dire compréhensible par le noyau (entier qui respecte le NBO). Donc ntop convertit le format système vers l'humain et pton effectue le travail inverse.
Du fait de leur compatibilité entre les familles de protocoles, ces fonctions sont un peu plus compliquées à l'usage : Il faut préciser PF_INET ou PF_INET6.
cm Exemple d'utilisation :
struct sockaddr_in saddr ;
if (inet_pton(PF_INET,"138.195.52.130",&saddr.sin_addr) != 1)
(void)fprintf(stderr,"L'adrese n'est pas exploitable !\n") ;
else {
char adr[INET_ADDRSTRLEN] ; /* 16 == 4 x 3(digits) + 3 (.) + 1 (NULL) */
printf("Adresse IP = %s\n",inet_ntop(PF_INET,&saddr.sin_addr,adr,sizeof(adr))) ;
}
Il faut noter que le code de retour de la fonction inet_pton peut prendre les valeurs -1, 0 ou 1. 1 signifie que transformation l'adresse transcodée est utilisable.
Le code de retour de inet_ntop est soit NULL si la conversion a échoué, ou un pointeur sur la chaîne affichable. Ici, par construction, on suppose que la conversion sera toujours réussie.
5 Conversion hôte - adresse IPv4
Se pose régulièrement le problème de convertir un nom symbolique en une adresse IP et inversement. L'origine de cette information dépend de la configuration de la station de travail : c'est un serveur de noms (DNS), c'est un fichier (/etc/hosts) ou tout autre système de propagation des informations (NIS...). Dans tous les cas l'information arrive à un programme via une entité nommée le resolver, qui unifie les sources d'informations.
Les paragraphes 5.1, 5.2
(p.
), 6.1 (p.
)
et 6.2 (p.
présentent une approche
traditionnelle, seulement valable avec IPv4 alors que le paragraphe
7 (p.
) expose une démarche beaucoup
plus récente et adaptée également à IPv6. L'écriture de nouveaux codes
ne devraient faire appel qu'à cette nouvelle api.
5.1 Une adresse IP à partir d'un nom d'hôte
#include
<netdb.h>
| struct hostent * | gethostbyname | (char *name) ; |
struct hostent {
char *h_name ; /* Le nom officiel */
char **h_aliases ; /* Tableau de synonymes */
int h_addrtype ; /* PF_INET pour ARPA */
int h_length ; /* Long. de l'adresse */
char **h_addr_list ; /* Adresses possibles */
} ;
#define h_addr h_addr_list[0]
cm
la macro h_addr sert à assurer la compatibilité avec les premières versions dans lesquelles il n'y avait qu'une seule adresse IP possible par hôte.
Le nom `` officiel '' s'oppose aux noms synonymes. Par exemple, soit
une machine officiellement baptisée pasglop.mon-domain.fr ; si
pour répondre au besoin d'une certaine application l'administrateur
du réseau lui donne le surnom www.mon-domain.fr, celui-ci
sera considéré comme un `` alias '' vis à vis du nom officiel et
donc lu dans h_aliases. (voir page
)
gethostbyname.c
La fin du tableau de pointeurs est marquée par un pointeur NULL.
La liste des adresses est un tableau de pointeurs, le marqueur de fin de liste est également un pointeur NULL. Chaque adresse est une zone de h_length octets (cf fonction impnet dans l'exemple ci-après).
Le programme d'usage qui suit affiche toutes les informations contenues dans cette structure pour les hôtes dont le nom est passé en argument (le code source de cet exemple, gethostbyname.c, est à la page suivante).
$ gethostbyname gw-sio.sio.ecp.fr srv-sio.sio.ecp.fr
Nom officiel : gw-sio.sio.ecp.fr
Type d'adresse : AF_INET
Adresse Internet : 138.195.52.33
Adresse Internet : 138.195.53.1
Adresse Internet : 138.195.10.52
Nom officiel : srv-sio.sio.ecp.fr
Type d'adresse : AF_INET
Adresse Internet : 138.195.52.130
$ gethostbyname anna.sio.ecp.fr
anna.sio.ecp.fr : hote inconnu !
5.2 Un nom d'hôte à partir d'une adresse IP
le problème inverse se résoud avec la fonction gethostbyaddr. La définition du prototype se trouve au même endroit que précédement, la fonction renvoie un pointeur sur une structure du type hostent.
| #include | <netdb.h> |
| struct hostent * | gethostbyaddr | (char *addr, int len, int type) ; |
addr
:
Pointe sur une structure du type in_addr
len
:
Est la longueur de addr
type
:
PF_INET quand on utilise la pile ARPA
6 Conversion N
de port - service
Couramment les ports bien connus sont donnés par leur nom plutôt que par leur valeur numérique, comme par exemple dans les sorties de la commande tcpdump.
Un tel programme a besoin de faire la conversion symbolique -- numérique, la fonction getservbyname effectue ce travail. L'utilisateur récupère un pointeur sur une structure du type servent, NULL dans le cas d'une impossibilité. La source d'informations se trouve dans le fichier /etc/services.
| #include | <netdb.h> |
| struct servent * | getservbyname | (char *name, char *proto) ; |
struct servent {
char *s_name ;
char **s_aliases ;
int s_port ;
char *s_proto ;
} ;
Voici un programme de mise en application de la fonction, le code source de l'exemple, getservbyname.c se trouve à la page suivante.
$ getservbyname domain
Le service domain est reconnu dans /etc/services
protocole :tcp - N° de port :53
$ getservbyname domain udp
Le service domain est reconnu dans /etc/services
protocole :udp - N° de port :53
getservbyname.c
Symétriquement la fonction getservbyport effectue le travail inverse. Elle renvoie aussi un pointeur sur une structure servent, NULL dans le cas d'une impossibilité.
| #include | <netdb.h> |
| struct servent * | getservbyport | (int port, char *proto) ; |
Exemples d'usage :
$ getservbyport 53
Le port 53 correspond au service "domain" (protocole tcp).
$ getservbyport 53 udp
Le port 53 correspond au service "domain" (protocole udp).
Exemple de programmation :
getservbyport.c
7 Getaddrinfo, pour IPv4 et IPv6
Les apis des deux paragraphes qui précèdent (gethostbyname et getservbyname et leur symétrique) sont des standards de facto et ce depuis le début des années 80. On les trouve sur toutes les variantes d'Unix et même au delà, ce qui a participé à une grande portabilité du code écrit qui les utilise.
L'arrivée d'IPv6 et de sa probable très longue cohabitation avec IPv4 oblige à modifier les habitudes de programmation au profit d'une nouvelle approche, que ces concepteurs souhaitent aussi stable et largement répandue que la précédente. L'écriture de tout nouveau code devrait s'appuyer sur cette nouvelle API, définie dans la RFC 3493.
La nouvelle fonction getaddrinfo de la libc ne se contente pas seulement de synthétiser gethostbyname et getservbyname en une seule fonction, elle banalise également l'usage d'IPv6.
La démarche est plus concise (une fonction au lieu d'une), et la
manipulation des adresses IP est rendue plus aisée en retour, puisque
que la structure de données utilisée par la fonction contient
directement une structure d'adresse conforme à la famille de protocole
utilisée, sockaddr_in pour IPv4, directement utilisable par
exemple avec les primitives bind, connect,...On peut songer
par comparaison au champ h_addr_list de la structure hostent
(page
) qui ne contient que les adresses IP.
La fonction getaddrinfo combine les fonctionnalités de gethostbyname et getservbyname pour les protocoles IPv4 et IPv6. Son prototype est donc le reflet de sa relative complexité, par contre son fonctionnement est très logique , il découle de celui des deux fonctions qu'elle remplace.
7.1.1 Prototype de getaddrinfo
#include <netdb.h>
int getaddrinfo(const char *hostname, const char *servname,
const struct addrinfo *hints, struct addrinfo **res);
Comme il y a beaucoup d'informations à transmettre et à recevoir, tout (ou presque) s'effectue via une nouvelle structure de données nommée addrinfo. Elle apparait en troisième argument (informations fournies à l'appel) et quatrième (dernier) argument, le résultat.
Si cette fonction ne retourne pas une valeur nulle (0), le code d'erreur est à exploiter à l'aide d'une fonction spécialisée, gai_strerror qui ajoute une bonne dizaine de messages d'erreurs supplémentaires spécialisés.
7.1.2 Description des arguments
Les deux premiers arguments, hostname ou servname, sont soit des chaînes de caractères, soit un pointeur nul. Il ne peuvent pas être tous les deux nuls.
La fonction alloue la mémoire nécessaire pour le stockage
des données, au programme appelant de la
restituer au système. Il dispose à cet effet d'une fonction
spécialisée : freeaddrinfo.
7.1.3 La structure addrinfo
Voici les membres de la structure addrinfo, définis
dans le fichier d'en-têtes netdb.h :
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
socklen_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for hostname */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
Signalons tout de suite au lecteur le dernier membre de la structure, ai_next. Il est du même type que celui de la structure elle même (structure auto-référente en langage C) ce qui signifie qu'il peut pointer vers une autre structure du même type. Il s'agit alors d'une liste chaînéeXIV4 afin de retourner une information multiple, comme peut l'être par exemple la réponse d'une requête DNS (type A ou PTR en l'occurence), ou la liste des protocoles prévus pour un service donné. La fin de la liste est marquée par un pointeur de valeur nulle (NULL).
La structure hints doit être mise à zéro avant chaque usage. Les quatre premiers champs sont utilisés à l'appel, les autres éléments sont à zéro lors de la transmission à la fonction.
Il peut prendre la valeur PF_UNSPEC quand
le protocole n'est pas fixé.
Si la valeur est laissée à 0 cela signifie que
n'importe quel protocole est accepté.
.
Il y a 6 variables à configurer, non toutes utiles selon les utilisations. Il est évident que certaines des nombreuses possibilités offertes par la combinatoire ne sont pas consistante. Le bon sens doit prédominer et le test de retour de la fonction être toujours exploité... !
7.1.5 Exemple d'usage à la place de gethostbyname
Cet exemple remplace celui de la page
. Le
code n'est pas plus simple.
getaddrinfo_1.c
Notez l'utilisation des éléments de la structure
d'information pour compléter les arguments d'appel de
inet_ntop. Il faut utiliser un cast
(struct sockaddr_in *) afin d'accéder au
champ sin_addr de la structure d'adresse
IPv4. La structure ai_addr
est générique et n'y donne pas accès.
7.1.6 Exemple d'usage à la place de getservbyname
Cet exemple remplace celui de la page
getaddrinfo_2.c
et qui sert à retrouver
la valeur symbolique d'un protocole, connaissant
son codage numérique (fichier /etc/protocols).
...
8 Conversion nom de protocole - N
de protocole
Les fonctions getservbyname et getservbyport délivrent un nom de protocole, donc un symbole.
Lors de la déclaration d'une socket le troisième argument est numérique, il est donc nécessaire d'avoir un moyen pour convertir les numéros de protocoles (IPPROTO_UDP, IPPROTO_TCP, IPPROTO_IP,IPPROTO_ICMP,...) en symboles et réciproquement.
Le fichiers /etc/protocols contient cette information, et la paire de fonctions getprotobyname et getprotobynumber l'exploitent.
#include <netdb.h>
struct protoent *getprotobyname (const char *name) ;
struct protoent *getprotobynumber (int proto) ;
struct protoent {
char *p_name ;
char **p_aliases ;
int p_proto ;
} ;
getprotobyname.c
Le source qui précède donne un exemple de programmation de la fonction getprotobyname. Usage de ce programme :
$ getprotobyname ip
Le protocole ip est reconnu dans /etc/protocols - N° : 0
$ getprotobyname tcp
Le protocole tcp est reconnu dans /etc/protocols - N° : 6
$ getprotobyname vmtp
Le protocole ipv6 est reconnu dans /etc/protocols - N° : 41
Chaque retour de primitive devrait être soigneusement testé, le code généré n'en est que plus fiable et les éventuels disfonctionnements plus aisés à détecter.
Quelques unes des erreurs ajoutées au fichier d'en-tête errno.h
erreur
Description de l'erreur
ENOTSOCK
Le descripteur n'est pas celui d'une socket
EDESTADDRREQ