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 :

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

    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
 
ls –l /dev/printer

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 :
 
int sd = socket (....);

    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 :

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

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

Remarques :

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 :

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 :

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);

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 :
#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 :     Une socket UDP connectée peut être déconnectée de deux façons différentes :     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.