Page prec.
Fin page
Page suiv.
Interface des sockets BSD sous Unix
(voir
[Man98a],
[How99])
Introduction
Le premier manuel d'utilisation de l'interface des sockets, développée par l'université de Berkeley a été publié en 1983.
La dernière mise à jour connue à ce jour, 4.4BSD-Lite2, date de 1995.
Domaine de communication
Une socket est destinée à faire communiquer deux ou plusieurs processus éventuellement distants, par l'intermédiaire du ou des systèmes d'exploitation qui les gèrent (nous ne considérerons dans ce paragraphe que le système Linux).
La désignation de la socket ne peut donc rester interne aux processus : elle doit être désignée par un identificateur reconnu par le système et visible de l'extérieur.
Toutes les entités (système d'exploitation, applications) qui utilisent la même désignation d'une socket appartiennent à un domaine de communication.
Il existe au moins une vingtaine de domaines de communication, correspondant chacun à des conventions de dénomination. En
voici quelques-uns :
-
domaine local : (AF_LOCAL) : AF comme
Adress
Family
: ce domaine est strictement réservé à la communication
de tâche à tâche locales, et peut donc remplacer
les
pipes.
-
domaine dit "Unix" : (AF_UNIX) : ancienne dénomination
(BSD) de AF_LOCAL
-
domaine Internet : (AF_INET) : peut être
utilisé sur des machines distantes reliées via le protocole
Internet
-
domaine DECnet : (AF_DECnet) : idem
-
domaine IBM SNA : (AF_SNA)
-
domaine Apple Talk : (AF_APPLETALK)
-
etc...
Dans la distribution RedHat version 6.2 de Linux, les différentes familles d'adresse sont accessibles par le fichier /usr/include/sys/socket.h[1].
En fait, elles sont équivalentes aux
"familles de protocoles"
correspondants :
/* Address families. */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX
PF_UNIX
#define AF_FILE
PF_FILE
#define AF_INET
PF_INET
... |
Type de socket
Le type d'une socket permet de préciser ses caractéristiques.
Il définit l'ensemble des propriétés des communications qu'elle permet.
Ces propriétés sont les suivantes :
-
la fiabilité de la transmission : aucune donnée transmise n'est perdue,
-
la préservation de l'ordre des données : les données arrivent dans l'ordre dans lequel elles ont été émises,
-
la non-duplication des données : une donnée émise n'arrive qu'une seule fois à destination,
-
la communication en mode connecté : une connexion est établie en début de communication.
Dès lors les données émises à une extrémité de la connexion sont destinées à l'entité située à l'autre extrémité,
-
la conservation des limites entre les messages.
Lorsque les limites ne sont pas conservées, l'application doit elle-même mettre en œuvre un mécanisme d'identification de début et de fin de message.
Si les limites ne sont pas conservées, 3 écritures de 10 octets chacune sont équivalentes à une écriture de 30 octets (comme c'est le cas avec les pipes),
-
la possibilité d'émettre des messages urgents qui seront traités en priorité hors du flot normal.
Le mode de communication par datagrammes (ou mode non connecté) correspond à l'absence de connexion entre les processus.
Quand l'émetteur est actif, le lecteur peut ne pas être à l'écoute. Un datagramme est envoyé sur le réseau, sans aucune certitude qu'il soit reçu. Les données peuvent être transportées dans le désordre et sur des chemins physiques différents. Cela correspond à l'échange d'information par courrier. Le mode connecté, dans lequel la connexion entre les deux correspondants doit être établie avant que l'échange puisse commencer, est recommandé par le CCITT. Si la connexion est fiable, son établissement entre les deux extrémités communicantes implique le séquencement des informations à travers la socket (FIFO).
Les types de sockets sont :
-
le type "datagramme", mode non connecté, non fiable, messages de
longueur maximale fixée,
-
le type "stream", mode connecté, ordre respecté et
non duplication (transmission fiable), flux d'octets à double sens
(full duplex), possibilité de messages urgents,
-
le type "sequenced packet", mode connecté, ordre respecté
et non duplication (transmission fiable), datagrammes de longueur maximale
fixée (conservation des limites). un consommateur doit lire un message
entier,
-
le type "raw", datagramme de bas niveau, réservé aux
développeurs de nouveaux protocoles, d'analyseurs de trames, il
permet l'accès aux couches basses (modèle OSI),
-
le type "rdm", datagramme fiable mais qui ne garantit pas l'ordre.
Dans la distribution RedHat version 6.2 de Linux, les différents types de sockets sont accessibles par le fichier
usr/include/sys/socket.h [1].
/* Types of sockets. */
enum __socket_type
{
SOCK_STREAM = 1,
/* Sequenced, reliable, connection-based
byte streams. */
SOCK_DGRAM = 2,
/* Connectionless, unreliable datagrams
of fixed maximum length. */
SOCK_RAW = 3,
/* Raw protocol interface. */
SOCK_RDM = 4,
/* Reliably-delivered messages. */
SOCK_SEQPACKET = 5,
/* Sequenced, reliable, connection-based,
datagrams of fixed maximum length. */
SOCK_PACKET = 10
/* Linux specific way of getting packets
at the dev level. For writing rarp and
other similar things on the user level. */
}; |
La figure suivante illustre, dans le domaine d'Internet, trois des types de sockets et leur niveau d'accès aux différentes couches de communication :
Désignation d'une socket
Nous avons vu précédemment qu'à l'intérieur d'un
domaine de communication,
les entités désignaient les sockets de la même façon.
Il s'agit de la désignation externe de la socket.
Comme dans le cas des fichiers, la désignation interne d'une socket au sein d'une application dépend du système d'exploitation : sous Linux, il s'agit d'un socket descriptor, sous Windows, il s'agit d'un handle.
Désignation externe
Le nom par lequel une socket peut être
"universellement" désignée dépend du domaine auquel
elle appartient. Une socket a un nom et un chemin d'accès.
Dans le domaine Unix, une socket est vue comme un fichier : <chemin
(path) + nom du fichier>. Dans le domaine Internet, elle est vue comme
un couple <adresse de machine + n° de port> (chemin
= adresse de machine, nom = port). Dans le domaine DECnet, elle
est vue comme un couple <adresse de machine + n° d'objet>
(chemin = adresse de machine, nom = n° d'objet).
Par exemple, la commande
permet de mettre en évidence qu'il s'agit d'un fichier spécial
de type socket du domaine Unix :
srw------- 1 root root
0 Oct 27 16:47 /dev/printer |
s comme socket.
Désignation interne
Pour communiquer à travers une socket,
un processus doit y écrire ou y lire (par les fonctions read(),
write(),
send(),
recv(),
sendto(),
recvfrom(),
sendmsg(),
recvmsg()).
Les E/S étant unifiées sous Unix, toutes les opérations
utilisent un file descriptor quel que soit le support (fichier,
terminal, pipe,
socket). Il sera appelé ici socket
descriptor. Les fonctions ioctl(),
fcntl(),
close()
peuvent
donc aussi être utilisées avec des
sockets.
La fonction système
socket()
permet d'obtenir un socket descriptor sd :
Un élément de la table des descripteurs
(descriptor table) est affectée par le système au
processus, celui-ci peut y lire par exemple par read(sd, ...);
A cet élément de la table des descripteurs Unix associe un nouvel élément dans la file table du système.
C'est seulement à ce niveau que la socket va se distinguer selon le domaine de protocoles utilisé.
Il peut pointer soit sur un élément de la table des i-nœuds dans le domaine Unix, soit sur une structure de socket Internet si le domaine est PF_INET, etc.
C'est à ce niveau (structure de socket ou table des i-nœuds) que sont créés/réservés pour la socket des buffers d'échange (octets en émission/réception) que le protocole (TCP/IP par exemple) va transporter.
#include <sys/socket.h>
int socket (int domain, int type, int protocol); |
Le premier paramètre domain, peut prendre différentes valeurs, comme
PF_UNIX, PF_INET, PF_DECnet, etc.
(PF_... comme Protocol Family).
Il détermine la nature de la structure de socket associée au socket descriptor, et l'ensemble des protocoles utilisables (voir les
familles de protocole sous Linux).
Le type est aussi l'un de ceux vus
ci-dessus :
SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, etc.
Le paramètre protocol est en général égal à 0 : pour une famille de protocoles et un type donnés, il n'y en a en général qu'un.
Dans certains cas, il y a plusieurs protocoles possibles.
La figure ci-dessous montre ce que pourrait être une structure de données pour une socket "stream" Internet :
Family : PF_INET |
Service : SOCK_STREAM |
Local IP : |
Remote IP : |
Local Port : |
Remote port : |
... |
Dans la distribution RedHat version 6.2 de Linux, les différentes familles de protocoles sont accessibles par le fichier
/usr/include/sys/socket.h[1].
Parmi les plus connues :
/* Protocol families. */
#define PF_UNSPEC 0
/* Unspecified.
*/
#define PF_LOCAL 1
/* Local to host (pipes and file-domain). */
#define PF_UNIX
PF_LOCAL /* Old BSD name for PF_LOCAL.
*/
#define PF_INET
2 /* IP protocol family.
*/
#define PF_AX25
3 /* Amateur Radio AX.25.
*/
#define PF_IPX
4 /* Novell Internet Protocol.
*/
...
#define PF_INET6 10
/* IP version 6.
*/
...
#define PF_DECnet 12
/* Reserved for DECnet project.
*/
...
#define PF_MAX
32 /* For now..
*/ |
Lien entre le "socket descriptor" et la socket : notion d'adresse
Le lien entre le descripteur de socket interne (socket descriptor) et le nom de la socket visible de l'extérieur est obtenu par la fonction système bind():
#include <sys/socket.h> // contient le prototype de bind() // et permet d'accéder à struct sockaddr
int bind (int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
|
Ce profil est "pseudo-générique" (la généricité n'existe pas en C!) : l'adresse, qui est la désignation externe de la socket, est variable par sa structure (selon le
domaine)
et par sa taille, indiquée par le paramétre addrlen.
Le paramètre effectif sera en réalité de type
struct sockaddr_un
si le domaine est Unix,
struct sockaddr_in
si le domaine est Internet, etc.
La structure sockaddr est accessible par l'intermédiaire du fichier <sys/socket.h> :
struct sockaddr
{
sa_family_t sa_family; //
famille d'adresse : AF_UNIX, AF_INET, etc...
char
sa_data[14]; // designation de l'adresse
}; |
En fait, dans la distribution RedHat version 6.2 de Linux, la déclaration est un peu plus complexe mais équivalente :
/* fichier /usr/include/bits/sockaddr.h */
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
/* Structure describing a generic socket address. */ |
et
/* fichier /usr/include/bits/socket.h */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /*
Common data: address family and length. */
char sa_data[14];
/* Address data. */
}; |
Domaine Unix
Schéma
général d'une communication par socket
Quel que soit le type de communication (datagramme ou
stream),
un schéma général peut être dégagé
pour le serveur et pour le client, qui sera ensuite précisé
et complété selon le type. Rappelons qu'une socket
est vue et traitée par le système comme un fichier.
C'est par l'intermédiaire d'un socket descriptor
que
client et serveur vont communiquer. La première opération
consiste donc à obtenir du système un descripteur sd
par la fonction système socket().
Pour que les échanges puissent se faire, la
socket
doit exister en tant que ressource du système (un i-nœud dans la
table des i-nœuds en mémoire). Deux cas sont possibles :
-
elle n'existe pas au lancement du serveur, elle est alors créée
par le système au moyen de la fonction système bind()
qui, de plus, la lie à sd,
-
elle existe avant le lancement du serveur, et l'appel de la fonction système
bind()
provoque une erreur (EADDRINUSE) et ne la lie pas à sd.
Il est alors impossible d'utiliser la socket existante.
Il découle de ce qui précède que
c'est au serveur qu'il appartient de créer la socket au début
de la session (appel de la fonction système bind()), et
de la détruire à la fin (appel de la fonction système
unlink()).
Il est d'ailleurs prudent de faire précéder systématiquement
l'appel de bind() par l'appel de unlink(). En fin de
communication, client et serveur doivent refermer la socket (appel
de la fonction système close()).
En mode non connecté (datagrammes) les messages
sont perdus s'ils sont envoyés à une socket qui n'existe
pas ou qui n'est pas liée. En mode connecté, le client ne
peut même pas se connecter si la socket n'existe pas. Les
clients doivent donc être lancés après le serveur et,
s'ils sont émetteurs, se terminer avant lui.
Le schéma ci-dessous illustre le squelette
des deux processus :
// Serveur
//...
int sd = socket (...);
// lien avec le nom
bind (sd, ...);
// Echanges avec le(s) client(s)
close (sd);
unlink (nom_externe); |
// Client
//...
int sd = socket (...);
//...
// Echanges avec le(s) serveur(s)
close (sd);
|
Communication par socket non connectée
Les échanges d'information se font en principe
par les fonctions systèmes sendto(), recvfrom()
[4].
Comme dit précédemment,
c'est toujours le client qui doit prendre l'initiative de l'échange.
Il envoie donc un message (éventuellement vide) au serveur, qu'il
désigne par son adresse au moment de l'envoi. Le serveur reçoit
le message, accompagné de l'adresse de l'envoyeur qu'il peut ignorer
s'il n'a pas à répondre. Sinon, il envoie la réponse
à cette adresse. Les fonctions systèmes sendto()
et recvfrom() ont les profils suivants :
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom (int socket, void * buffer, size_t length,
int flags, struct sockaddr * from, socklen_t * fromlen);
ssize_t sendto (int socket, const void * buffer, size_t length,
int flags, const struct sockaddr *to, socklen_t tolen);
|
Les trois premiers paramètres des fonctions sendto() et recvfrom() sont les mêmes que ceux des fonctions
read()
et
write() :
socket descriptor, adresse de la zone mémoire réceptrice ou émettrice, nombre maximal d'octets à recevoir ou nombre d'octets à émettre.
Le quatrième paramètre, flags, est une option ou une combinaison d'options (par l'opérateur booléen |) :
-
pour recvfrom() :
-
MSG_DONTWAIT : lecture non bloquante
-
MSG_PEEK : lecture sans prélèvement : les données ne sont pas supprimées de la file
-
0 : absence d'options
-
pour sendto() :
-
MSG_DONTWAIT : écriture non bloquante
-
0 : absence d'options
Ces fonctions sont générales à tout domaine, l'adresse indiquée par le cinquième paramètre (destinataire ou expéditeur) est donc "générique".
Le sixième paramètre est le nombre d'octets nécessaires pour stocker l'adresse effective, qui dépend du type du paramètre effectif to ou from.
Dans le domaine Unix, la structure
d'une adresse est définie dans <sys/un.h> :
struct sockaddr_un
{
unsigned short sun_family; // famille d'adresse
char sun_path [108]; // reference UNIX
};
|
Les appels de ces fonctions seront donc :
sockaddr_un Destinataire;
//
// en C
//
ssize_t n = sendto (sd, buffer, size, flags,
(sockaddr*) (&Destinataire), sizeof (Destinataire));
//
// ou, en C++
//
ssize_t n = sendto (sd, buffer, size, flags,
reinterpret_cast <sockaddr*> (&Destinataire), sizeof (Destinataire));
|
et
sockaddr_un Expediteur;
int lg = sizeof (sockaddr_un);
//
// en C
//
ssize_t n = recvfrom (sd, buffer, size, 0,
(sockaddr*) (&Expediteur), &lg);
//
// ou, en C++
//
ssize_t n = recvfrom (sd, buffer, size, 0,
reinterpret_cast <sockaddr*> (&Expediteur), &lg);
|
Lorsque la provenance du message reçu n'a pas besoin d'être connue (pas de réponse à envoyer), les deux derniers paramètres de recvfrom() peuvent être nuls :
ssize_t n = recvfrom (sd, buffer, size, 0, 0, 0);
|
Pseudo-connexion
Lorsque le client communique avec un seul serveur, il
est possible de demander au système d'enregistrer l'adresse du serveur
et de la lier provisoirement avec le socket descriptor. C'est la
fonction système connect() qui effectue cette opération
:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
|
Cette pseudo-connexion est purement locale et peut
être modifiée à tout instant : le client peut à
n'importe quel moment orienter tous ses échanges avec un autre serveur
en appelant de nouveau la fonction connect().
Utilisation
de bind() dans un client non connecté dans le domaine Unix
Dans le domaine Unix, le serveur ne peut recevoir
l'adresse de l'expéditeur, donc lui répondre éventuellement,
que
si celui-ci a appelé préalablement la fonction bind()
en lui passant une adresse virtuelle. Dans le cas contraire, et selon,
les implémentations, soit le serveur reçoit une adresse invalide
à laquelle il ne peut répondre, soit même la fonction
recvfrom() échoue (Linux RedHat 6.2).
Communication par socket connectée
La structure générale du serveur et du client reste identique :
-
du coté du serveur :
-
obtention du socket descriptor par la fonction socket(),
-
lien avec le nom de la socket,
-
puis, en fin, fermeture et suppression de la socket,
-
du coté du client :
-
obtention du socket descriptor sd par la fonction
socket(),
-
puis, en fin, fermeture de la socket.
Cependant le principe de fonctionnement est assez différent.
Du coté du client, une véritable connexion doit être
demandée et obtenue par la fonction système
connect(),
qui lie le
socket descriptor sd avec la socket du
serveur. Dès lors les échanges par l'intermédiaire
de sd sont automatiquement dirigés vers la socket
liée. Du coté du serveur, le mécanisme est plus complexe
: la socket nommée que le serveur offre est une socket
de connexion, qui ne sert pas à la communication. Le serveur attend
qu'une demande de connexion soit effectuée au moyen de la fonction
système
accept() qui est en principe bloquante. Lorsqu'une
demande de connexion lui parvient, le système Unix délivre
un second
socket descriptor, que nous appellerons newsd
et qui correspond à une seconde structure de socket qu'il
gère en interne. Celle-ci n'est pas visible de l'utilisateur, c'est
en fait à elle que le client est attaché, et c'est par elle
que le serveur va échanger avec le client.
Dès que le socket descriptor sd
a été obtenu par le serveur et lié avec le nom, il
doit, par l'appel à la fonction système listen(),
indiquer au système d'exploitation la taille de la file d'attente
des demandes de connexion non satisfaites qu'il souhaite gérer (le
maximum est variable et imposé par le système). Le schéma
ci-dessous illustre le squelette des deux processus :
//
Serveur
int sd = socket (...);
// lien avec le nom
bind (sd, ...);
listen (sd, 5);
int newsd = accept (sd, ...);
// Echanges avec le(s) client(s)
// à l'aide de newsd
close (newsd);
close (sd);
unlink (nom); |
//
Client
int sd = socket (...);
connect (...);
// Echanges avec le(s) serveur(s)
close (sd);
|
Contrairement aux communications de type datagramme,
où les émissions en direction d'une socket inexistante
ne sont pas signalées, une demande de connexion par un client sur
une socket inexistante provoque une erreur. De même, si la
file d'attente des demandes de connexion du serveur est saturée,
toute demande de connexion d'un nouveau client lui est refusée.
Le canal de connexion, sd, est donc bien
distinct du canal de transmission de données newsd. Ce
dernier peut être utilisé en lecture/écriture dans
les deux sens. Les fonctions read(), recv(), write()
et send() sont utilisables sur cette socket.
Remarques :
-
0 : correspond à un comportement identique à read();
-
MSG_PEEK : il s'agit d'une lecture non-destructive : le message
est toujours disponible et le buffer n'est pas vide,
-
MSG_OOB : (Out Of Bound message) :
peut être utilisé pour recevoir des messages urgents.
-
les fonctions d'écriture utilisées sont essentiellement :
write (sd, buffer, taille);
send (int s, const void *msg, int len, unsigned int flags); |
Remarques :
-
Les fonctions read() et write() ne sont pas utilisables sur certaines implémentations de sockets BSD (WinSock par exemple).
-
Le récepteur est averti de l'arrivée d'un message urgent par le signal SIGURG. C'est alors à lui à détourner le signal et à appliquer au message le traitement approprié.
Communication entre processus par socket en utilisant le mode PEEK
- non traité ici
Utilité des sockets
Unix
Il a été montré que, sur une même
machine, les sockets Unix sont deux fois plus rapides que les sockets
Internet. Cela est mis à profit par le système X Window :
lorsqu'un client X11 commence et ouvre une connexion sur un serveur X11,
le client vérifie dans la variable d'environnement DISPLAY
si le serveur est sur la même machine. Si tel est le cas, il ouvre
une socket Unix connectée. Dans le cas contraire, il ouvre
une socket TCP Internet. Une seconde utilisation est la possibilité
de passer un "descriptor" à un autre processus sur la même
machine .
Domaine Internet
Introduction
Il faut dès le départ considérer
que les sockets dans le domaine Internet permettent de faire communiquer
des processus sur des machines éventuellement distantes, éventuellement
hétérogènes (c'est-à-dire n'ayant pas le même
système d'exploitation), appartenant au même réseau
ou à des réseaux interconnectés. Comme dans le cas
des sockets Unix, l'utilisation des sockets Internet nécessite
la mise en œuvre des deux phases suivantes :
-
la connexion,
-
la communication elle même, qui utilise les mêmes primitives
que précédemment : read(), write(), send(),
etc..., et sur laquelle nous ne reviendrons pas.
Notion de service
Un utilisateur (le client) peut utiliser un service
offert par un serveur qui, à un instant donné, est un processus
particulier, hébergé par un système d'exploitation
donné, sur une machine donnée. Certains de ces services sont
standard, et chaque utilisateur peut lui-même créer et offrir
des services sur son site. Le transfert de fichiers FTP par exemple est
un service standard.
Notion de port
L'utilisateur d'un service particulier doit pouvoir
le désigner de façon non ambiguë, quels que soient le
processus qui offre ce service, la machine hôte et le système
d'exploitation qui la gère. Un service ne peut donc pas être
désigné par l'identificateur du processus qui offre ce service.
En effet, cette identification est dépendante du système
d'exploitation. De plus le même processus peut gérer plusieurs
services.
La création d'une socket de communication
Internet correspond à la couche 5 du modèle
OSI (couche session), qui s'appuie sur les protocoles TCP ou UDP de
la couche 4 (couche transport). Les différents services offerts
par la couche 5 (et les couches supérieures) sont vus par la couche
4 comme des numéros de ports. Ils peuvent être considérés
comme des points d'entrée du système. Les numéros
de port des services standard (well-known) sont fixes et réservés.
Par exemple, le point d'entrée du système qui héberge
le service FTP (c'est-à-dire le processus serveur qui gère
les demandes de transfert de fichiers à distance) est 21 (ou symboliquement
IPPORT_FTP).
Les numéros de port inférieurs à
IPPORT_RESERVED
sont réservés et ne doivent pas être utilisés
par des programmes utilisateurs. Dans la distribution RedHat version 6.2
de Linux, les constantes symboliques sont déclarées dans
le fichier <netinet/in.h> :
/* Standard well-known ports. */
enum
{
IPPORT_ECHO = 7,
/* Echo service.
*/
IPPORT_DISCARD = 9,
/* Discard transmissions service.
*/
IPPORT_SYSTAT = 11,
/* System status service.
*/
IPPORT_DAYTIME = 13,
/* Time of day service.
*/
IPPORT_NETSTAT = 15,
/* Network status service.
*/
IPPORT_FTP = 21,
/* File Transfer Protocol.
*/
IPPORT_TELNET = 23,
/* Telnet protocol.
*/
IPPORT_SMTP = 25,
/* Simple Mail Transfer Protocol.
*/
IPPORT_TIMESERVER = 37,
/* Timeserver service.
*/
IPPORT_NAMESERVER = 42,
/* Domain Name Service.
*/
...
/* Ports less than this value are reserved for
privileged processes. */
IPPORT_RESERVED = 1024,
/* Ports greater this value are reserved for
(non-privileged) servers. */
IPPORT_USERRESERVED = 5000
}; |
La liste des services en principe offerts par le
système d'exploitation Unix sur une machine est disponible dans
le fichier /etc/services sous la forme d'une table dont un extrait
figure ci-dessous :
# /etc/services:
# $Id: services,v 1.4 1997/05/20 19:41:21 tobias Exp $
#
# Network services, Internet style
#
# Note that it is presently the policy of IANA to assign a single
well-known
# port number for both TCP and UDP; hence, most entries here have
two entries
# even if the protocol doesn't support UDP operations.
# Updated from RFC 1700, ``Assigned Numbers'' (October 1994).
Not all ports
# are included, only the more common ones.
tcpmux 1/tcp
# TCP port service multiplexer
echo
7/tcp
echo
7/udp
discard 9/tcp
sink null
discard 9/udp
sink null
systat 11/tcp
users
daytime 13/tcp
daytime 13/udp
netstat 15/tcp
qotd
17/tcp quote
...
www
80/tcp http
# WorldWideWeb HTTP
www
80/udp
# HyperText Transfer Protocol
... |
En fait, il s'agit d'un fichier texte et rien ne
prouve que tous les services annoncés sont réellement disponibles.
Il est possible d'interroger ce fichier par les deux fonctions système
(niveau 2) suivantes :
struct servent *getservbyname (char *name, char *protocole);
struct servent *getservbyport (u_short port, char *protocole); |
Par exemple :
struct servent * DNS_udp = getservbyname ("domain", "udp");
struct servent * FTP_tcp = getservbyname ("ftp",
"tcp"); [2]
struct servent * WWW_tcp = getservbyport (htons (80), "tcp"); |
La structure servent (service
entry),
déclarée dans le fichier <netdb.h> est la suivante
:
struct servent
{
char *s_name;
/* official service name
*/
char **s_aliases;
/* alias list
*/
int s_port;
/* port number, network-byte
order */
char *s_proto;
/* protocol to use
*/
}; |
s_aliases est un vecteur, terminé par 0, de
chaînes de caractères.
Désignation
d'un service par le client
Le client et le serveur communiquent par l'intermédiaire
des protocoles TCP ou UDP. Pour désigner le service auquel elle
veut accéder, la couche 5 du processus client doit indiquer :
-
l'adresse Internet de la machine qui héberge le service,
-
le numéro de port de ce service.
Nom et adresse Internet d'une machine
Une machine peut être désignée soit
par son adresse Internet (4 octets pour le protocole IPv4, ou plus simplement
IP, et 16 octets pour la nouvelle version IPv6 [3]),
soit par un nom mnémonique plus facile à mémoriser,
et le passage de l'un à l'autre peut être effectué
au moyen de différents fonctions et fichiers.
Nom de la machine hôte
Un processus peut connaître le nom de la machine
sur laquelle il tourne par la fonction système (niveau 2) :
#include <unistd.h>
int gethostname (char *name, size_t lg); |
où name est l'adresse à laquelle la fonction
doit ranger le nom de la machine (terminé par '\0' si la
place est suffisante), lg est le nombre d'octets qui ont été
réservés pour ce nom. La taille du vecteur name
peut être fixée à MAXHOSTNAMELEN, définie
dans le fichier <sys/param.h>. La fonction renvoie 0 si
elle s'est bien déroulée. En cas d'erreur elle renvoie –1
et la variable errno est positionnée.
Adresse Internet de la
machine hôte
Rappelons qu'une adresse Internet IPv4 est formée
de 4 octets représentant le numéro de réseau
(network number) et le numéro de la machine (host number),
la répartition des octets dépendant de la classe du réseau
(A, B, C ou D) : voir le cours réseau correspondant. La définition
standard d'une adresse Internet (aussi appelée adresse IP) est définie
par la structure suivante :
struct in_addr
{
in_addr_t s_addr; // adresse IPv4 dans l'ordre réseau
}; |
Dans la distribution RedHat version 6.2 de Linux,
elle est déclarée dans le fichier <netinet/in.h>
par :
struct in_addr
{
uint32_t s_addr;
}; |
Un processus peut connaître l'adresse Internet
de la machine sur laquelle il tourne par la fonction système (niveau
2) :
#include <unistd.h>
long int gethostid(void); |
Cette information peut aussi être retrouvée
dans le fichier /etc/hostid.
Fichier /etc/hosts
Pour chaque machine connectée sur un réseau,
ce fichier contient différentes informations regroupées au
sein d'une structure hostent (host entry),
déclarée dans le fichier <netdb.h> :
struct hostent
{
char *h_name;
// nom de la machine
char **h_aliases;
// liste d'alias
int h_addrtype;
// type d'adresse : AF_INET ou AF_INET6
int h_length;
// longueur de l'adresse en octets : 4 ou 16
char **h_addr_list;
// liste d'adresses IPv4 ou IPv6
#define h_addr h_addr_list[0] //
premiere adresse de la liste
}; |
h_aliases est un tableau terminé par 0, de
pointeurs vers des chaines de caractères.
h_addr_list est un tableau, terminé par 0, d'adresses
réseau sur 4 octets, dans l'ordre réseau.
La structure hostent peut être schématisée
ainsi :
Caractéristiques d'une machine connue par son nom ou son adresse
La structure hostent d'une machine de nom connu name
peut être récupérée par deux fonctions de niveau
3 :
-
soit par la fonction système gethostbyname(). Le nom name
peut être soit le nom de la machine, soit son adresse IP en notation
pointée xxx.xxx.xxx.xxx, soit son adresse IPv6,
-
soit par la fonction système gethostbyaddr(). L'adresse
addr
est en fait un pointeur sur une structure in_addr de 4 octets
contenant une adresse IPv4 ou une structure in6_addr de 16 octets
contenant une adresse IPv6. Les seuls types possibles sont AF_INET
et AF_INET6.
#include <netdb.h>
// struct hostent
#include <sys/socket.h> // AF_INET
extern int h_errno;
struct hostent *gethostbyname (const char *name);
struct hostent *gethostbyaddr (const char *addr, int len, int type); |
Les deux fonctions renvoient un pointeur vers une
zone mémoire de type hostent et un pointeur nul en cas
d'erreur. La variable globale h_errno est positionnée.
Transfert d'entiers
dans un réseau hétérogène
Il y a selon les machines, même fonctionnant
avec le même système d'exploitation, deux conventions
de représentation d'entiers de mêmes longueurs : les bits
de faible poids aux adresses faibles ou inversement, ou bits numérotés
de droite à gauche ou de droite à gauche (convention "little-endian"
ou "big-endian"). Le réseau permettant à des
machines hétérogènes de communiquer, la transmission
d'un entier doit suivre un protocole indépendant de la représentation
interne. C'est pourquoi, avant réception ou après réception
d'un entier, il est conseillé d'utiliser les fonctions (ou macros)
de conversion des entiers courts (numéros de port) ou longs (4 octets
: adresses Internet) ntohs(n) (network to host
short),
htons(n),
ntohl(n)
et htonl(n).
Désignation
de l'adresse complète d'un service Internet
Dans le domaine Internet, l'adresse complète
du service est formée du numéro du service (numéro
de port) ainsi que de l'adresse IP de la machine qui héberge le
processus serveur de ce service. La structure d'une adresse de socket est
définie dans le fichier <netinet/in.h> par :
/* Pour le protocole IP, version 4 : IP
*/
struct sockaddr_in
{
__SOCKADDR_COMMON
(sin_); /*
sin_family : Address Family */
uint16_t sin_port;
/* Port number.
*/
struct in_addr sin_addr;
/* Internet address.
*/
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero [sizeof (struct sockaddr)
–
__SOCKADDR_COMMON_SIZE –
sizeof (uint16_t) –
sizeof (struct in_addr)];
};
/* Pour le protocole IP, version 6 : IPv6 */
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
/* sin6_family : Address Family
*/
uint16_t sin6_port;
/* Transport layer port #
*/
uint32_t sin6_flowinfo;
/* IPv6 flow information
*/
struct in6_addr sin6_addr;
/* IPv6 address
*/
}; |
Communications dans le domaine Internet
Tout ce qui a été présenté
dans les paragraphes III.4 à III.4.e reste valable dans le domaine
Internet en ce qui concerne les applications (client(s) et serveur). Leur
structure reste identique, que ce soit en mode connecté ou non connecté.
La seule différence concerne la terminaison du serveur en mode non
connecté, qui ne doit pas appeler la fonction système unlink().
Les modifications sont très importantes quand
on passe du domaine Unix au domaine Internet, mais cela n'a pas de répercussion
directe sur le code des applications. La première modification est
l'utilisation des protocoles TCP/IP ou UDP/IP (protocoles par défaut).
Alors que la communication dans le monde Unix se fait probablement par
des mécanismes implémentés directement dans le système
(pipes, mémoire partagée, files de messages), la communication
en Internet passe par les différentes phases des protocoles : construction/analyse
des trames, vérification de la fiabilité de la transmission
en mode connecté (séquencement, non duplication, etc...).
En effet les mêmes applications doivent fonctionner de façon
parfaitement identique, que les programmes clients et serveur soient sur
le même site ou non.
En mode connecté, une trame peut, avant d'atteindre
le destinataire, traverser de nombreux réseaux et être fragmentée
en trames plus petites. Une application qui fonctionnait correctement localement
peut se révéler défectueuse quand elle est utilisée
sur de grands réseaux si la lecture ne prend pas ce risque en compte.
En mode non connecté, comme la probabilité
de perdre des messages est nulle en utilisant des sockets SOCK_DGRAM
dans le monde Unix, et comme il appartient à l'application d'effectuer
cette vérification dans le mode non connecté, une application
qui ne vérifie pas correctement la fiabilité des transmissions
peut être inutilisable dans le domaine Internet si elle est répartie
sur plusieurs sites.
La communication en mode connecté est effectuée
au moyen du protocole TCP. Celui-ci commence par établir la connexion
de bout en bout par les fonctions systèmes connect() (coté
client) et accept() (coté serveur). Celle-ci est établie
selon le protocole de la triple poignée de main (3-way handshake)
qui prend un certain temps. De plus la communication par TCP se fait de
bout en bout, chaque extrémité devant être accessible
par le même mécanisme. La fonction connect() attribue
donc à la socket de communication du client un numéro
de port arbitraire, non encore utilisé, et que l'utilisateur n'a
pas à connaître.
Cependant, par la fonction accept() qui
établit la connexion, le serveur reçoit l'adresse du client,
qui est de type sockaddr_in. Il est donc possible d'obtenir le
numéro de port qui a été attribué au client
par son système.
Les clients pouvant être répartis sur
de très nombreuses machines différentes, il est possible
que l'une d'elles tombe en panne une fois ou de façon fréquente
et que le client se reconnecte systématiquement. Le serveur ne peut
détecter ce problème et multiplie les connexions pendantes,
sans plus d'interlocuteur. Fonctionnant en boucle infinie, il peut ainsi
être à la longue saturé par manque de ressources (buffers,
dimensions de tables internes, etc...) et bloqué.
Fermeture partielle : shutdown()
La fermeture propre d'une socket est obtenue
par la fonction système close(). Cependant, en mode connecté,
il est parfois difficile de terminer proprement une communication entre
client et serveur, en particulier si le nombre d'octets à transmettre
est inconnu a priori. Imaginons que le serveur sache qu'il a fini d'envoyer
la réponse à une requête du client. Il n'est pas sûr
que celui-ci ne veuille plus lui envoyer de nouvelles requêtes :
il ne peut donc pas prendre la responsabilité de fermer la socket.
D'autre part, le client peut savoir qu'il n'a plus
de requête à transmettre mais il peut ne pas être sûr
d'avoir reçu la totalité de la réponse. Il ne peut
donc lui non plus fermer la socket. La fonction système shutdown()
permet à un processus de signifier qu'il n'a plus l'intention de
lire, d'écrire (ou les deux) dans une socket. L'autre processus
avec lequel il communique, en est informé par la réception
d'une "fin-de-fichier" (lecture de 0 caractère) et peut alors fermer
la socket.
int shutdown (int sd, int sens); |
avec sens = 0 (lecture), 1 ( écriture) ou 2
(les deux).
Numéros de port internes
La communication en mode connecté se fait point
à point ce qui signifie que les processus client et serveur connaissent
chacun l'adresse de l'autre processus, destinataire des données
qu'ils envoient. Or, en Internet, une adresse est constituée non
seulement de l'adresse IP de la machine hôte mais d'un numéro
de port. Cela signifie que le client a lui aussi un numéro de port.
Celui-ci peut n'apparaître nulle part dans le corps du programme
client. En fait, lors de l'exécution par le client de la fonction
connect(),
le système lui attribue un numéro de port interne, unique,
qu'utilisera le protocole TCP (voir le chapitre TCP-UDP / IP, paragraphe
V : "format d'un segment TCP") dès la phase de connexion, puis pendant
toute la communication. De la même façon, lorsque la fonction
système accept() établit la connexion du coté
du serveur, le système attribue un nouveau numéro de port
interne qui est associé au nouveau socket descriptor et le
renvoie au client. A partir de ce moment-là le numéro de
port qui a servi à la connexion n'est plus utilisé dans l'échange
entre le client et le serveur.
Pseudo-connexion
et déconnexion d'une socket UDP
La pseudo-connexion d'une socket UDP présente
plusieurs caractéristiques à connaître :
-
dans certaines implémentations, le noyau effectue une pseudo-connexion
à chaque appel à la fonction sendto(), suivi d'une
immédiate déconnexion. Certains auteurs ont montré
que cela peut représenter le 1/3 du coût de l'émission
du datagramme UDP,
-
sur une socket UDP pseudo-connectée, un processus ne peut utiliser
que les fonctions read(), write(), send() et
recv(),
ou sendto() et recvfrom() en mettant les paramètres
d'adresse à 0,
-
les erreurs asynchrones délivrés par le protocole d'erreur
ICMP (le datagramme n'a pas pu être délivré parce que
la machine hôte ou le numéro de port ne peuvent être
atteints) ne sont accessibles que si la socket UDP est pseudo-connectée,
-
alors qu'une seule socket UDP non connectée peut recevoir des datagrammes
en provenance de toute adresse (multiplexage des datagrammes), une socket
UDP connectée ne peut recevoir des datagrammes qu'en provenance
de l'adresse utilisée lors de la pseudo-connexion.
Une socket UDP connectée peut être déconnectée
de deux façons différentes :
-
en la connectant à une autre adresse IP
-
en la connectant à une adresse IP invalide : AF_UNSPEC
Selon les implémentations, la fonction connect()
peut alors ou non échouer, en renvoyant une valeur de errno
qui peut éventuellement être EAFNOSUPPORT.
[1] en réalité dans le fichier /usr/include/bits/socket.h
[2] FTP n'existe qu'avec le protocole tcp
[3] non implémentée dans la distribution RedHat version 6.2 de Linux
[4] les fonctions sendto() et recvfrom() peuvent aussi être avec des sockets en mode connecté
Les deux derniers paramètres doivent alors être nuls :
ssize_t n = sendto (sd, buffer, size, 0, 0, 0);
ssize_t n = recvfrom (sd, buffer, size, 0, 0, 0);
|
Les flags utilisables sont alors ceux des fonctions send() et recv().
Page prec.
Début page
Page suiv.