Blue Flower

Chercher

I) Les sockets en C/C++

Généralités

Socket image

Une socket est un point d'accès aux couches réseau TCP/UDP. les sockets constituent un mécanisme qui permet la communication sur le réseau Internet  ou entre processus locaux tournant sur une même machine; ces processus locaux sont  les FIFOs (une extension du concept "pipe" du monde UNIX), les pipes et les files de messages IPC.

On crée une socket par la fonction "socket(int domaine, int type, int protocole);" qui retourne un "int" qui n'est rien d'autre que le  descripteur de la fonction "socket()" dans la table des descripteurs des processus. La socket s'utilise comme un fichier ordinaire et on se sert des routines classiques réservées aux fichiers (les routines "read", "write", ...) ou des opérations spécifiques aux communications (send, sendto, recv, recvfrom, ...).

Après sa création, une socket n’est connue que du processus qui l’a créée et par ses descendants. Elle doit être désignée par une adresse pour pouvoir être contactée de l’extérieur par d'autres processus locaux ou distants.
Les communications qu'on réalise avec les sockets sont soit  en mode connecté soit en mode datagramme.

Dans la routine socket (int domaine, int type, int protocole), le paramètre "domaine" précise si le socket est pour une communication réseau ou une communication inter-processus; si on est dans le premier cas,  on parle de socket internet et le paramètre domaine prend la valeur   "AF_INET". Si c'est le deuxième  cas, on a un socket UNIX c'est à dire une communication inter-processus et le paramètre "domaine" vaut AF_UNIX; toutefois quelque soit le domaine  choisi  on se retrouve  dans l'un des quatre modes  de communication suivantes:

  1. en mode connecté à travers Internet: TCP
  2. en mode connecté à travers Unix: TCP
  3. en mode datagramme à travers Internet: UDP
  4. en mode datagramme à travers Unix: UDP

II) Communication

Généralités

Les deux modes connectées (TCP) et les deux modes datagrammes (UDP),  sont deux modes de socket respectivement appelés  SOCK_STREAM et  SOCK_DGRAM.
Si le paramètre le domaine vau AF_INET,  ce sont les numéros de port qui donnent le point de rendez-vous des sockets et si il est égale à AF_UNIX, ce sont des noms de fichiers qui donnent le point de rendez-vous.
Les concepts fondamentaux des sockets ( adresses, communication par datagrammes et flots, client-serveur, etc) utilisés sont les mêmes pour les sockets locales (communications sur une même machine - AF_UNIX) que pour les échanges par internet. Les particularités viennent de l' élaboration des adresses.

Stream Sockets et Datagram Sockets
On fabrique une adresse à partir d'un nom de machine (résolution) et d'un numéro de port  ou on retrouve le nom d'une machine à partir d'une adresse (résolution inverse).
Il y a plusieurs type de sockets (exemples les "Raw Sockets") mais les deux types qui nous intéressent ici sont les sockets de flux ("Stream Sockets") et les sockets de paquets ("Datagram Sockets") qui sont référencées respectivement par SOCK_STREAM et SOCK_DGRAM dans la suite de ce document. Les "sockets de paquets" sont parfois appelées les "sockets sans connection".
Les sockets de flux ("Stream Sockets") sont deux voies de communications bi-directionnelles et fiables. Si on y envoie deux éléments  dans l'ordre "1", "2", ces deux éléments arriveront dans le même ordre "1", "2" à l'autre bout de la connexion  sans erreur.

Les protocoles utilisés lors d'une communication entre réseaux font partie d'une organisation multicouche. Les informations échangées lors d'une communication internet se font par paquets (paquet ici n'a rien à voir avec datagramme); chaque paquet est emballé (encapsulé) avec une en-tête par le protocole qui va prendre en charge le paquet. Exemple, si on utilise le protocole TFTP, l'ensemble en-tête protocole est encapsulé par le protocole qui suit (par exemple UDP si c'est lui qui va prendre le relais ou IP sinon)  et finalement par la couche du protocole matériel (ou physique) (disons Ethernet).
Quand à l'ordinateur qui reçoit le paquet, sa couche matérielle enlève l'entête Ethernet, le noyau enlève les en-têtes IP ou UDP, le programme TFTP enlève l'entête TFTP et récupère finalement les données envoyées c'est à dire l'information.
Tout ce qu'on doit faire si on utilise les sockets de flux est d'envoyer les données avec la routine  send() et pour les sockets de paquets d'encapsuler le paquet avec la méthode de votre choix et d'appeler sendto(). Le noyau appelle alors la couche Transport et Internet à partir de vos données et le matériel appelle la couche d'accès réseau.

Création d'un socket

La création d'une socket se fait par la routine socket (int domaine, int type, int protocole), et l'utilisation de cette routine nécessite la présence des fichiers en-tête "#include <sys/types.h>" et  "#include <sys/socket.h>".
Les arguments de la  routine socket (int domaine, int type, int protocole) sont:

  • Domaine
    Ce paramètre définit une famille de protocoles (protocol family):
    PF_INET  ou AF_INET: cette constante comme domaine désigne les protocoles internet IPv4 (protocole fondé sur IP à savoir TCP,  UDP,  ICMP),
    AF_UNIX: celle-ci comme domaine désigne les protocoles pour les processus résidents sur la même machine
    PF_INET6: protocoles internet IPv6,
    PF_IPX: protocoles Novel IPX,
    PF_X25: protocoles X25 (ITU-T X.25 / ISO-8208),
    PF_APPLETALKAppletalk  protocoles Apple, etc.
  • type
    le type indique le style de communication désiré entre les deux participants; le type est soit SOCK_DGRAM ou SOCK_STREAM
  • protocole
    Ce troisième argument permet de spécifier le protocole à utiliser. Il est du type UDP ou TCP en général et les types de protocoles les plus courants sont:
    IPPROTO_TCP pour TCP
    IPPROTO_SCTP pour SCTP
    IPPROTO_UDP pour UDP
    IPPROTO_RAW et IPPROTO_ICMP uniquement avec SOCK_RAW PROTOCOL; le protocole est souvent mis à zéro car l'association de la famille de protocole et du type de communication définit explicitement le protocole de transport.
    Exemple, si on a PF_INET  comme domaine et SOCK_STREAM pour type,  --------> TCP => c'est à dire que le paramètre protocole vaut IPPROTO_TCP et si on a PF_INET + SOCK_DGRAM --------> UDP => protocole vaut IPPROTO_UDP; ce paramètre protocole est une constante définie dans le fichier d'en-têtes "/usr/include/netinet/in.h" et qui reflète le contenu du fichier système "/etc/protocols".

III) Communication par le réseau

Généralités

La communication par le réseau en utilisant les sockets internet se fait par flots de données (TCP) ou par datagrammes (UDP) et l'adresse de chaque  socket est la combinaison d'un numéro IP et  d'un numéro de port. Dans un système d'adressage IPv4, on utilise des structures de type "struct sockaddr_In" de la famille  AF_INET ( ou PF_INET) pour gérer les adresses IP car elle a un champ de type  "struct in_addr"  réservé à l'adresse: 

 struct in_addr {
union {
struct {
u_char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct {
u_short s_w1,s_w2;
} S_un_w;
u_long S_addr;
} S_un;
#define s_addr S_un.S_addr /* cette define doit être utilisée pour tout le code */
#define s_host S_un.S_un_b.s_b2 /* OBSOLETE: host on imp */
#define s_net S_un.S_un_b.s_b1 /* OBSOLETE: network */
#define s_imp S_un.S_un_w.s_w2 /* OBSOLETE: imp */
#define s_impno S_un.S_un_b.s_b4 /* OBSOLETE: imp # */
#define s_lh S_un.S_un_b.s_b3 /* OBSOLETE: logical host */
};

La structure struct sockaddr_In est donc:

struct sockaddr_in {
short sin_family; /* doit être AF_INET */
u_short sin_port;
struct in_addr sin_addr; /* adresse IP de la machine */
char sin_zero[8];
};

et les membres de la structure "sockaddr_in"  sont les  3 champs sin_family, sin_addr et sin_port:

  1. sin_family   donne la famille d'adresses, qui vaut AF_INET ( ou PF_INET)
  2. sin_addr, pour l'adresse IP
  3. sin_port, pour le numéro de port

Pour IPv6, on utilise des structures de type "struct sockaddr_in6"

struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};

Cette structure possède un membre de type  "struct in6_addr" qui gère les  adresses IPv6: 

struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};

Comme pour les IPV4 on a les 3 champs de sockaddr_in6 qui permettent de bien construire l'adresse d'une socket:

  1. sin6_family, valant AF_INET6
  2. sin6_addr, l'adresse IP sur 6 octets
  3. sin6_port, pour le numéro de port.

Les octets de l'adresse IP et le numéro de port sont stockés dans l'ordre réseau (big-endian); cela n'est pas forcément le cas de toutes les machines hôtes sur laquelle s'exécuteront tous les programmes s'appuyant sur les sockets internet d'où la nécessité de faire certaines conversions et adaptations pour construire une adresse de socket.

Préparation d'une adresse

Structure de type "struct sockaddr"
Pour accéder à une socket sur une machine il faut une adresse. Les opérations sur les adresses concernent un type d'adresse général abstrait de type struct sockaddr qui recouvre tous les types concrets particuliers.
Pour réaliser une communication par socket internet, on dispose du nom de la machine hôte et d'un numéro de port. La structure "struct hostent" retournée par le pointeur de fonction *gethostbyname(const char *name) (voir #include <netdb.h>) donne  accès à plusieurs informations sur la machine hôte. Cette structure hostent est définie dans <netdb.h> comme suit:

struct hostent {
   char    *h_name;       /* Nom officiel de l'hôte.   */
   char   **h_aliases;    /* Liste d'alias.            */
   int      h_addrtype;   /* Type d'adresse de l'hôte. */
   int      h_length;     /* Longueur de l'adresse.    */
   char   **h_addr_list;  /* Liste d'adresses.         */
}

Ses membres sont :

  • h_name : Nom officiel de la machine hôte
  • h_aliases Une table, terminée par zéro, d'alternatives au nom officiel de l'hôte.
  • h_addrtype: Le type d'adresse (actuellement, toujours AF_INET).
  • h_length: La longueur, en octets, de l'adresse.
  • h_addr_list: Une table, terminée par zéro, d'adresses réseau pour l'hôte, avec l'ordre des octets du réseau.
  • h_addr: La première adresse dans h_addr_list pour respecter la compatibilité ascendante.

Si une machine a plusieurs adresses, ce qui est le cas quand on a plusieurs cartes réseaux/interfaces présentes sur une machine, on précise la carte concernée   en remplissant correctement la structure sin_addr
struct sockaddr_in {
short sin_family; /* must be AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
avec
struct in_addr {
unsigned long s_addr; // charger avec la routine inet_pton()
};

L’adresse IP d’une machine ( de type "struct in_addr") est sur 4 octets écrits  sous la forme "xyz.xzy.zxf.sdv" et quand au port adéquat, sa valeur est celle du champ sin_port,   un entier court en ordre réseau; si on y met un entier ordinaire, il faut le convertir par htons() (Host TO Network Short).
Pour l'adresse de la machine locale (à ne pas confondre avec la notion de socket local - socket UNIX -) on utilise, dans le cas le plus fréquent, l'adresse INADDR_ANY (0.0.0.0); ainsi La socket est ouverte (avec le même numéro de port) sur toutes les adresses IP de toutes les interfaces de la machine.
On peut aussi se servir, de l'adresse INADDR_LOOPBACK qui correspond à l'adresse locale 127.0.0.1 (alias localhost). et dans ce cas la socket n'est accessible que depuis la machine elle-même, d'une des adresses IP de la machine et de l'adresse de diffusion générale (broadcast) INADDR_BROADCAST (255.255.255.255)

Construction de l'adresse
  Du Côté du Serveur, on remplit une structure de type "struct sock_addr"  et on indique sur quel port le serveur se met en attente de connexion; on définit une structure de  type "struct sock_addr" et on assigne à chaque champ de la structure  une valeur comme suit:
struct sock_addr soc_in; /* on déclare la structue*/
soc_in.sin_family = AF_INET; /* on assigne au champ sin_family la valeur AF_INET,*/
soc_in.sin_addr.s_addr = htonl (INADDR_ANY);
soc_in.sin_port = htons (port);
Le champ sin_addr de la structure sock_addr  est une structure; son champ sin_addr.s_addr reçoit la valeur de retour de la fonction htonl(INADDR_ANY).

Du côté client, ...

Utilisation d'une socket côté serveur

Pour travailler avec des sockets, du côté serveur,  on a besoin d'un certain nombres de routines telles que :

  1. socket(): cette permet de créer la socket (voir ci-dessus)
  2. bind: cette routine attache une adresse (port) à une socket, la socket créée par la routine socket(). Les adresses >= 1024 sont réservées au super-utilisateur. Pour l'utiliser il faut avoir les éléments qui suivent:
    struct sockaddr_in soc_in1;
    bzero(&soc_in1,sizeof(soc_in1));
    soc_in1.sin_family = AF_INET;
    soc_in1.sin_port = htons(7000);
    soc_in1.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(sockfd,&soc_in1,sizeof(soc_in1) < 0) /* error */
  3. listen(): cette routine donne la longueur maximale de la queue d'attente des demandes de connexions non servies (par exemple 5). ce qui est très utile car, en général le serveur fait un fork() (voir dans le cours sur les processus la définition de fork() )dès qu'il accepte une connexion.
  4. accept (sockfd, &soc_in1, &lg); // l'appel à cette fonction fournit une nouvelle socket qui servira pour  dialoguer avec le client; c'est une fonction bloquante qui attend les demandes de connexion et qui retourne un nouveau file descriptor qui permettra de faire les entrées/sorties sur la nouvelle connexion.
    struct sockaddr_in soc_in1; // soc_in1 permet de recevoir l'adresse client
    int lg = sizeof(soc_inc1);
    int newsockfd = socket(sockfd, &soc_in1,&lg); // sockfd est le descripteur retourner par socket() 
  5. A partir de là on travaille avec le socket ce qui donne la séquence ci-dessus côté serveur:

La séquence côté serveur:
int fd, newfd;
if ((fd = socket(...)) < 0)
erreur ("ouverture socket");
if (bind(fd, ...) < 0)
erreur ("bind");
if (listen (fd, 5) < 0)
erreur ("listen");
for (; ;) {
if ((newfd = accept (fd, ...)) < 0) /* blocant */
erreur ("accept");
if (fork() == 0) {
close (fd);
.... /* on continue avec newfd */
exit (0);
} close (newfd);
}

Utilisation d'une socket côté client

La séquence côté client:

fd = socket(domaine, type, protocole);
connect (fd, adresse_serveur, longueur_adresse_serveur);
read (fd, ..., ...);
write (fd, ..., ...);

........


Communication par le réseau: réalisation d'un serveur TCP

Les fonctions essentielles

Un serveur TCP traite des connexions venant d'un ou plusieurs clients. Après avoir créé et nommé une socket, le serveur spécifie qu'il accepte les communications entrant par la fonction listen(int s, int backlog), et se met effectivement en attente d'une connexion de client par la fonction accept(int s, struct sockaddr *addr,socklen_t *addrlen); ces routines nécessitent la présence des deux fichiers include "#include <sys/types.h>" et " #include <sys/socket.h>".

Le paramètre backlog de la fonction listen indique la taille maximale de la file des connexions qui peuvent être mise en attente. Sous Linux cette limite est donnée par la constante SOMAXCONN (qui vaut 128), sur d'autres systèmes elle est limitée à 5.
La fonction accept() renvoie une autre socket, qui servira à la communication avec le client. L'adresse du client peut être obtenue par les paramètres addr et addrlen.
En général les serveurs TCP traitent plusieurs connexions simultanément. La solution habituelle est de lancer, après l'appel à accept() un processus fils (par fork())qui traite la communication avec un seul client. Ceci induit une gestion des processus, donc des signaux liés à la terminaison des processus fils. Cette façon de gérer les serveurs TCP sera traitée dans la partie de l'article concernant les processus.

Réalisation d'un serveur TCP

Etude d'un serveur TCP (serveur web) qui renvoie les fichiers (textes) d'un répertoire sous forme de pages HTML.

 /* serveur-web.c
* Ce Serveur TCP est un
* serveur web, qui renvoie les fichiers (textes)
* d'un répertoire sous forme de pages HTML
* usage : serveur-web port repertoire
* exemple: serveur-web 8000 /usr/doc/exemples
*/
#include <unistd.h>
#include <sys/types.h>
#include <sys/errno.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include "webCServer.h"
void arreter_serveur(int numero_signal);
void attendre_sous_serveur(int numero_signal);
int fd_serveur; /* variable globale, pour partager avec traitement signal fin_serveur */

void demarrer_serveur(int numero_port, char repertoire[]){
int numero_client = 0;
int fd_client;
struct sigaction action_int, action_chld;
fd_serveur = serveur_tcp(numero_port);

/* arrêt du serveur si signal SIGINT */
action_int.sa_handler = arreter_serveur;
sigemptyset(&action_int.sa_mask);
action_int.sa_flags = 0;
sigaction(SIGINT, &action_int, NULL);

/* attente fils si SIGCHLD */
action_chld.sa_handler = attendre_sous_serveur;
sigemptyset(&action_chld.sa_mask);
action_chld.sa_flags = SA_NOCLDSTOP;
sigaction(SIGCHLD, &action_chld, NULL);

printf("> Serveur " VERSION
" (port=%d, répertoire de documents=\"%s\")\n",
numero_port, repertoire);
while (1) {
struct sockaddr_in a;
size_t l = sizeof a;
fd_client = attendre_client(fd_serveur);

getsockname(fd_client, (struct sockaddr *) &a, &l);
numero_client++;
printf("> client %d [%s]\n", numero_client,
inet_ntoa(a.sin_addr));
if (fork() == 0) {
/* le processus fils ferme le socket serveur et s'occupe du client */
close(0);
close(1);
close(2);
close(fd_serveur);
servir_client(fd_client,repertoire);
close(fd_client);
exit(EXIT_SUCCESS);
}
/* le processus père n'a plus besoin du socket client.
Il le ferme et repart dans la boucle */
close(fd_client);
}
}
/*
Traitement des signaux
*/
void arreter_serveur(int numero_signal){
printf("=> fin du serveur\n");
shutdown(fd_serveur, 2); /* utile ? */
close(fd_serveur);
exit(EXIT_SUCCESS);
}
void attendre_sous_serveur(int numero_signal){
/* cette fonction est appelée chaque fois qu'un signal SIGCHLD
indique la fin d'un processus fils _au moins_. */
while (waitpid(-1, NULL, WNOHANG) > 0) {
/* attente des fils arrêtés, tant qu'il y en a */
}
}
/* -------------------------------------------------------------*/
void usage(char prog[]){
printf("Usage : %s [options\n\n", prog);
printf("Options :"
"-h\tcemessage\n"
"-p port\tport du serveur [%d]\n"
"-d dir \trépertoire des documents [%s]\n",
PORT_PAR_DEFAUT, REPERTOIRE_PAR_DEFAUT);
}
/* -------------------------------------------------------------*/
int main(int argc, char *argv[]){
int port = PORT_PAR_DEFAUT;
char *repertoire = REPERTOIRE_PAR_DEFAUT; /* la racine des documents */
char c;
while ((c = getopt(argc, argv, "hp:d:")) != -1)
switch (c) {
case 'h':
usage(argv[0]);
exit(EXIT_SUCCESS);
break;
case 'p':
port = atoi(optarg);
break;
case 'd':
repertoire = optarg;
break;
case '?':
fprintf(stderr, "Option inconnue -%c. -h pour aide.\n",
optopt);
break;
};
demarrer_serveur(port, repertoire);
exit(EXIT_SUCCESS);
}

Explications

La fonction "int getopt(argc, argv, optstring)"
Cette fonction analyse les arguments de la ligne de commande. Ses éléments argc et argv correspondent aux nombres et à la table d'arguments qui sont transmis à la fonction main() lors du lancement du programme. Un élément de argv qui commence par `-' (et qui ne soit pas uniquement "-" ou "--") est considéré comme une option. Les caractères à la suite du '-' initial sont les caractères de l'option. Si getopt() est appelée à plusieurs reprises, elle renverra successivement chaque caractère de chaque option.
Cette fonction permet de récupérer les options relatives à l'utilisation de argrc et argv dans un programme C/C++; elle retourne le prochain caractère d'option dans argv qui concorde le caractère dans optstring; optstring doit contenir les caractères d'option que la fonction getopt() doit reconnaître ; si le caratère est suivi par deux points, l'option s'attend à avoir un argument, ou un groupe d'arguments, qui doivent être séparés par un espaces blanc.
Une fois qu'on est dans le bon tempo, on appelle la fonction demarre_serveur() qui, à son tour, appelle la fonction serveur_tcp(numero_port); après cet appel il installe un certain nombre de signaux qui permettent de gérer le serveur .
La fonction serveur_tcp(numero_port) démarre un service TCP sur le port indiqué et retourne le descripteur de la socket si le démarrage s'est bien passé. Une fois le serveur démarré on entre dans une boucle infini et on attend les clients ( voir les fonctions serveur_tcp(int numero_port), attendre_client(fd_serveur) et le fichier "webCServer.h" ci-dessus).

Les fonctions réseau du projet

La fonction serveur_tcp (int numero_port)

 /*
  Projet serveur Web
   Fonctions réseau
 */
  #include <sys/types.h>
  #include <sys/errno.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
  #include <sys/stat.h>
  #include <netinet/in.h>
  #include <signal.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include "declarations.h"
 /*----------------------------------------------------------
  Fonctions réseau
  -----------------------------------------------------------*/
  int serveur_tcp(int numero_port){
    
        int fd;
        /* démarre un service TCP sur le port indiqué */
        struct sockaddr_in addr_serveur;
        size_t lg_addr_serveur = sizeof addr_serveur;
        /* création de la prise */
        fd = socket(AF_INET, SOCK_STREAM, 0);
        if (fd < 0)
         FATAL("socket");
         /* nommage de la prise */
        addr_serveur.sin_family = AF_INET;
        addr_serveur.sin_addr.s_addr = INADDR_ANY;
        addr_serveur.sin_port = htons(numero_port);
        if (bind(fd, (struct sockaddr *) &addr_serveur, lg_addr_serveur) < 0)
             FATAL("bind");             
        /* ouverture du service */
        listen(fd, 4);
        return (fd);
     }

.......

La fonction attendre_client (int fd_serveur)

      int attendre_client(int fd_serveur)
     {
         int fd_client;
         /* A cause des signaux SIGCHLD, la fonction accept()
            peut etre interrompue quand un fils se termine.
            Dans ce cas, on relance accept().
          */
          while ((fd_client = accept(fd_serveur, NULL, NULL)) < 0) {
              if (errno != EINTR)
                  FATAL("Fin anormale de accept().");
          }
         return (fd_client);
     }
 

...

Le fichier webCServer.h

     /* Serveur Web - webCServer.h */
     #define CRLF "\r\n"
      #define VERSION "MegaSoft 0.0.7 pour Unix"
      #define PORT_PAR_DEFAUT 8000
      #define REPERTOIRE_PAR_DEFAUT "/tmp"
     #define FATAL(err) { perror((char *) err); exit(1);}
     extern void servir_client(int fd_client, char repertoire[]);
     extern void envoyer_document(FILE * out,
                                  char nom_document[], char repertoire[]);
     extern void document_non_trouve(FILE * out, char nom_document[]);
     extern void requete_invalide(FILE * out);
      extern int serveur_tcp(int numero_port);
     extern int attendre_client(int fd_serveur);

On verra dans l'étude des processus une autre implémentation du serveur TCP qui utilisera les threads; on crée préalablement un «pool» de processus que l'on bloque. Lorsqu'un client se présente, on confie la communication à un processus inoccupé puis,

ouvrir une socket côté serveur (socket/bind/listen)
créer un pool de processus
répéter indéfiniment
attendre l'arrivée d'un client (accept)
trouver un processus libre, et lui
confier la communication avec le client
fin-répeter

Les fonctions de dialogue avec les Clients: traitement-client.c

Dans l'étude du serveur ci-dessus on a défini un certain nombre de fonctions dans le fichier "traitement-client.c"; ces fonctions permettent de traiter l'échange et le dialogue avec les clients;

  • dialogue_client() : lecture et traitement de la requête d'un client
  • envoyer_document(),
  • document_non_trouve(),
  • requete_invalide().
  /*
traitement-client.c
projet serveur WEB
Communication avec un client
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/errno.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <signal.h>
#include "webCServer.h"

void dialogue_client(int fdClient, char repertoire[]){
FILE *in, *out;
char verbe[100], nom_document[100];
int fd2;

/* Ouverture de fichiers de haut niveau */
in = fdopen(fdClient, "r");
/* note : si on attache out au même descripteur que in,
la fermeture de l'un entraine la fermeture de l'autre */
fd2 = dup(fdClient);
out = fdopen(fd2, "w");

/* lecture de la requête, du genre
"GET quelquechose ..." */
fscanf(in, "%100s %100s", verbe, nom_document);
fclose(in);

if (strcmp(verbe, "GET") == 0)
envoyer_document(out, nom_document, repertoire);
else
requete_invalide(out);
fflush(out); /* utile ? */
fclose(out);
}
    
void envoyer_document(FILE * out, char nom_document[], char repertoire[]){
char nom_fichier[100];
FILE *fichier;
char ligne[100];

sprintf(nom_fichier, "%s%s", repertoire, nom_document);
if (strstr(nom_fichier, "/../") != NULL) {
/* tentative d'accès hors du répertoire ! */
document_non_trouve(out, nom_document);
return;
}
fichier = fopen(nom_fichier, "r");
if (fichier == NULL) {
document_non_trouve(out, nom_document);
return;
}
fprintf(out,
"HTTP/1.1 200 OK" CRLF
"Server: " VERSION CRLF
"Content-Type: text/html; charset=iso-8859-1" CRLF
CRLF);
fprintf(out,
"<html><head><title>Fichier %s</title></head>"
"<body bgcolor=\"white\"><h1>Fichier %s</h1>" CRLF
"<center><table><tr><td bgcolor=\"yellow\"><listing>"
CRLF, nom_document, nom_fichier);
/* le corps du fichier */
while (fgets(ligne, 100, fichier) > 0) {
char *p;
for (p = ligne; *p != '\0'; p++) {
switch (*p) {
case '<':
fputs("&lt;", out);
break;
case '>':
fputs("&gt;", out);
break;
case '&':
fputs("&amp;", out);
break;
case '\n':
fputs(CRLF, out);
break;
default:
fputc(*p, out);
};
};
}
/* balises de fin */
fputs("</listing></table></center></body></html>" CRLF, out);
}
    void document_non_trouve(FILE * out, char nom_document[]){
/* envoi de la réponse : entête */
fprintf(out,
"HTTP/1.1 404 Not Found" CRLF
"Server: MegaSoft 0.0.7 (CP/M)" CRLF
"Content-Type: text/html; charset=iso-8859-1" CRLF
CRLF);

/* corps de la réponse */
fprintf(out,
"<HTML><HEAD>" CRLF
"<TITLE>404 Not Found</TITLE>" CRLF
"</HEAD><BODY BGCOLOR=\"yellow\">" CRLF
"<H1>Pas trouvé !</H1>" CRLF
"Le document <font color=\"red\"><tt>%s</tt></font> "
"demandé<br>n'est pas disponible.<P>" CRLF
"<hr> Le webmaster"
"</BODY></HTML>" CRLF, nom_document);
fflush(out);
}
     void requete_invalide(FILE * out){
fprintf(out,
"<HTML><HEAD>" CRLF
"<TITLE>400 Bad Request</TITLE>" CRLF
"</HEAD><BODY BGCOLOR=\"yellow\">" CRLF
"<H1>Bad Request</H1>"
"Vous avez envoyé une requête que "
"ce serveur ne comprend pas." CRLF
"<hr> Le webmaster" "</BODY></HTML>" CRLF);
fflush(out);
}

  /* ---------------------------------------------------------
Fonctions réseau
---------------------------------------------------------*/
int serveur_tcp(int numero_port){
int fd;
/* démarre un service TCP sur le port indiqué */
struct sockaddr_in addr_serveur;
size_t lg_addr_serveur = sizeof addr_serveur;
/* création de la prise */
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
FATAL("socket");
/* nommage de la prise */
addr_serveur.sin_family = AF_INET;
addr_serveur.sin_addr.s_addr = INADDR_ANY;
addr_serveur.sin_port = htons(numero_port);
if (bind(fd, (struct sockaddr *) &addr_serveur, lg_addr_serveur) < 0)
FATAL("bind");

/* ouverture du service */
listen(fd, 4);
return (fd);
}
 int attendre_client(int fd_serveur){
int fd_client;
/* A cause des signaux SIGCHLD, la fonction accept()
peut etre interrompue quand un fils se termine.
Dans ce cas, on relance accept().
*/
while ((fd_client = accept(fd_serveur, NULL, NULL)) < 0) {
if (errno != EINTR)
FATAL("Fin anormale de accept().");
}
return (fd_client);
}
 void servir_client(int fdClient, char repertoire[]){
FILE *in, *out;
char verbe[100], nom_document[100];
int fd2;
/* Ouverture de fichiers de haut niveau */
in = fdopen(fdClient, "r");
/* note : si on attache out au même descripteur que in,
la fermeture de l'un entraine la fermeture de l'autre */
fd2 = dup(fdClient);
out = fdopen(fd2, "w");
/* lecture de la requête, du genre
"GET quelquechose ..." */
fscanf(in, "%100s %100s", verbe, nom_document);
fclose(in);
if (strcmp(verbe, "GET") == 0)
envoyer_document(out, nom_document, repertoire);
else
requete_invalide(out);
fflush(out); /* utile ? */
fclose(out);
}

....


Communication par le réseau:  réalisation d'un client TCP-IP

La socket d'un client TCP doit être reliée (par connect()) à celle  du serveur, et elle est utilisée ensuite par les routines read() et des write(), ou des entrées-sorties de haut niveau comme  fprintf(), fscanf()  si on a défini des flots par fdopen().

 /* web_client.c */
/*
Interrogation d'un serveur web
Usage:
web_client serveur port adresse-document
retourne le contenu du document d'adresse
http://serveur:port/adresse-document
Exemple:
web_client www.info.prive 80 /index.html
Fonctionnement:
- ouverture d'une connexion TCP vers serveur:port
- envoi de la requête GET adresse-document HTTP/1.0[cr][lf][cr][lf]
- affichage de la réponse
*/
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#define CRLF "\r\n"
#define TAILLE_TAMPON 1000

void abandon(char message[]){
perror(message);
exit(EXIT_FAILURE);
}
/* -- connexion vers un serveur TCP --- */
int ouvrir_connexion_tcp(char nom_serveur[], int port_serveur){
struct sockaddr_in addr_serveur;
struct hostent *serveur;
int fd;
fd = socket(AF_INET, SOCK_STREAM, 0); /* création prise */
if (fd < 0)
abandon("socket");
serveur = gethostbyname(nom_serveur); /* recherche adresse serveur */
if (serveur == NULL)
abandon("gethostbyname");
addr_serveur.sin_family = AF_INET;
addr_serveur.sin_port = htons(port_serveur);
addr_serveur.sin_addr = *(struct in_addr *) serveur->h_addr;
if (connect(fd, /* connexion au serveur */
(struct sockaddr *) &addr_serveur,
sizeof addr_serveur)
< 0)
abandon("connect");
return (fd);
}

void demander_document(int fd, char adresse_document[]){
char requete[TAILLE_TAMPON];
int longueur;
/* constitution de la requête, suivie d'une ligne vide */
longueur = snprintf(requete, TAILLE_TAMPON,
"GET %s HTTP/1.0" CRLF CRLF,
adresse_document);
write(fd, requete, longueur); /* envoi */
}
void afficher_reponse(int fd){
char tampon[TAILLE_TAMPON];
int longueur;
while (1) {
longueur = read(fd, tampon, TAILLE_TAMPON); /* lecture par bloc */
if (longueur <= 0)
break;
write(1, tampon, longueur); /* copie sur sortie standard */
};
}
int main(int argc, char *argv[]){
char *nom_serveur, *adresse_document;
int port_serveur;
int fd;
if (argc != 4) {
printf("Usage: %s serveur port adresse-document\n", argv[0]);
abandon("nombre de paramètres incorrect");
}
nom_serveur = argv[1];
port_serveur = atoi(argv[2]);
adresse_document = argv[3];
fd = ouvrir_connexion_tcp(nom_serveur, port_serveur);
demander_document(fd, adresse_document);
afficher_reponse(fd);
close(fd);
return EXIT_SUCCESS;
}

Explications

Le coeur du programme est la fonction ouvrir_connexion_tcp(nom_serveur, port_serveur); au sein de cette fonction on crée la socket( avec les vérifications d'usage), on cherche l'adresse du serveur par la fonction gethostbyname() (dans la foulée on a les champs de la structure "sockaddr_in" qui sont remplis) et enfin on a la connexion au serveur qui est réalisée; une fois la connexion faite, la fonction ouvrir_connexion qui retourne le descripteur de la socket est créée.

.......


Communication par le réseau: datagramme (UDP)

Généralités

La communication par socket Internet commence par la création d'une socket par la routine socket(int domain, int type, int protocol). Cette fonction construit une socket et retourne un numéro de descripteur. On utilise la famille AF_INET, et comme on échange des datagrammes on se sert du type SOCK_DGRAM et le paramètre protocole par défaut vaut "0" (Si la routine socket() retourne "-1" on est dans un cas d'échec.
Avec une socket en mode connecté, Une fois la socket créée, la fonction connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen) met en relation la socket (de cette machine) avec une autre socket désignée, qui sera le « correspondant par défaut » pour la suite des opérations.
Dans le cas d'une socket connectée on peut envoyer des datagrammes (contenu dans un tampon "t" de longueur "n") en se servant de la fonction write(sockfd,t,n); sinon on peut se servir de la fonction send(int s, const void *msg, size_t len, int flags) qui permet de positionner des flags comme MSG_DONTWAIT pour une écriture non bloquante. Une autre façon de faire est d'utiliser la fonction sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) qui permet d'envoyer des datagrammes à une adresse spécifiée sur une socket connectée ou non. Pour la reception on peut se servir d'un simple read(), de recv(int s, void *buf, size_t len, int flags) (avec des flags) ou d' un recvfrom(int s, void *buf, size_t len, int flags,struct sockaddr *from, socklen_t *fromlen), qui permet de récupérer l'adresse "from" de l'émetteur. 
.......


Communication par le réseau: réalisation d'un serveur UDP

Le serveur ouvre une socket sur un port en mode non-connecté, affiche les messages(chaînes de caractères) qu'il reçoit par ce socket puis envoie une réponse:
......

         /*
         serveur-echo.c -  Réception de datagrammes
         Exemple de serveur qui
         - ouvre un socket sur un port en mode non-connecté
         - affiche les messages (chaînes de caractères)
         qu'il reçoit par ce socket.
         - envoie une réponse
         */
       #include <unistd.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <signal.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <arpa/inet.h>
      #include <ctype.h>
      #include <string.h>
      #define TAILLE_TAMPON 1000
      static int fd;
      void abandon(char message[])
      {
          printf("SERVEUR> Erreur fatale\n");
          perror(message);
          exit(EXIT_FAILURE);
      }
      void arreter_serveur(int signal)
      {
          close(fd);
          printf("SERVEUR> Arrêt du serveur (signal %d)\n", signal);
          exit(EXIT_SUCCESS);
      }
      int main(int argc, char *argv[])
      {
          struct sockaddr_in adresse_serveur;
          size_t taille_adresse_serveur;
          int numero_port_serveur;
          char *src, *dst;
          struct sigaction a;
          /* 1. réception des paramètres de la ligne de commande */
          if (argc != 2) {
              printf("usage: %s port\n", argv[0]);
              abandon("mauvais nombre de paramètres");
          }
          numero_port_serveur = atoi(argv[1]);
          /* 2. Si une interruption se produit, arrêt du serveur */
          /* signal(SIGINT, arreter_serveur); */
          a.sa_handler = arreter_serveur;
          sigemptyset(&a.sa_mask);
          a.sa_flags = 0;
          sigaction(SIGINT, &a, NULL);
          /* 3. Initialisation du socket de réception */
          /* 3.1 Création du socket en mode non-connecté 
             (datagrammes) */
          fd = socket(AF_INET, SOCK_DGRAM, 0);
          if (fd < 0)
              abandon("socket");
          /* 3.2 Remplissage de l'adresse de réception 
             (protocole Internet TCP-IP, réception acceptée sur toutes 
             les adresses IP du serveur, numéro de port indiqué)
           */
          adresse_serveur.sin_family = AF_INET;
          adresse_serveur.sin_addr.s_addr = INADDR_ANY;
          adresse_serveur.sin_port = htons(numero_port_serveur);
          /* 3.3 Association du socket au port de réception */
          taille_adresse_serveur = sizeof adresse_serveur;
          if (bind(fd,
                   (struct sockaddr *) &adresse_serveur,
                   taille_adresse_serveur) < 0)
              abandon("bind");
          printf("SERVEUR> Le serveur écoute le port %d\n",
                 numero_port_serveur);
          while (1) {
              struct sockaddr_in adresse_client;
              int taille_adresse_client;
              char tampon_requete[TAILLE_TAMPON],
                  tampon_reponse[TAILLE_TAMPON];
              int lg_requete, lg_reponse;
              /* 4. Attente d'un datagramme (requête) */
              taille_adresse_client = sizeof(adresse_client);
              lg_requete = recvfrom(fd, tampon_requete, TAILLE_TAMPON, 0,     /* flags */
                                    (struct sockaddr *) &adresse_client,
                                    (socklen_t *) & taille_adresse_client);
              if (lg_requete < 0)
                  abandon("recfrom");
              /* 5. Affichage message avec sa provenance et sa longueur */
              printf("%s:%d [%d]\t: %s\n",
                     inet_ntoa(adresse_client.sin_addr),
                     ntohs(adresse_client.sin_port), lg_requete,
                     tampon_requete);
              /* 6. Fabrication d'une réponse */
              src = tampon_requete;
              dst = tampon_reponse;
              while ((*dst++ = toupper(*src++)) != '\0');
              lg_reponse = strlen(tampon_reponse) + 1;
              /* 7. Envoi de la réponse */
              if (sendto(fd,
                         tampon_reponse,
                         lg_reponse,
                        0,
                        (struct sockaddr *) &adresse_client,
                        taille_adresse_client)
                 < 0)
                 abandon("Envoi de la réponse");
         }
         /* on ne passe jamais ici */
         return EXIT_SUCCESS;
     }

Explications

..........................


Communication par le réseau: réalisation d'un client UDP

     /*
       client-echo.c
       Envoi de datagrammes
       Exemple de client qui 
       - ouvre un socket 
       - envoie des datagrammes sur ce socket (lignes de textes
       de l'entrée standard)
       - attend une réponse
       - affiche la réponse
       */
     #include <unistd.h>
     #include <sys/types.h>
     #include <sys/socket.h>
     #include <netinet/in.h>
     #include <signal.h>
     #include <stdio.h>
     #include <netdb.h>
     #include <string.h>
     #include <stdlib.h>
     #define TAILLE_TAMPON 1000
     static int fd;
     void abandon(char message[])
     {
         printf("CLIENT> Erreur fatale\n");
         perror(message);
         exit(EXIT_FAILURE);
     }
     int main(int argc, char *argv[])
     {
         struct sockaddr_in adresse_socket_serveur;
         struct hostent *hote;
         int taille_adresse_socket_serveur;
         char *nom_serveur;
         int numero_port_serveur;
         char *requete, reponse[TAILLE_TAMPON];
         int longueur_requete, longueur_reponse;
         /* 1. réception des paramètres de la ligne de commande */
         if (argc != 4) {
             printf("Usage: %s hote port message\n", argv[0]);
             abandon("nombre de paramètres incorrect");
         }
         nom_serveur = argv[1];
         numero_port_serveur = atoi(argv[2]);
         requete = argv[3];
         /* 2. Initialisation du socket */
         /* 2.1 création du socket en mode datagramme */
         fd = socket(AF_INET, SOCK_DGRAM, 0);
         if (fd < 0)
             abandon("Creation socket");
         /* 2.2 recherche de la machine serveur */
         hote = gethostbyname(nom_serveur);
         if (hote == NULL)
             abandon("Recherche serveur");
         /* 2.3 Remplissage adresse serveur */
         adresse_socket_serveur.sin_family = AF_INET;
         adresse_socket_serveur.sin_port = htons(numero_port_serveur);
         adresse_socket_serveur.sin_addr =
             *(struct in_addr *) hote->h_addr;
         taille_adresse_socket_serveur = sizeof adresse_socket_serveur;
         /* 3. Envoi de la requête */
         printf("REQUETE> %s\n", requete);
         longueur_requete = strlen(requete) + 1;
         if (sendto(fd, requete, longueur_requete, 0,        /* flags */
                    (struct sockaddr *) &adresse_socket_serveur,
                    taille_adresse_socket_serveur)
             < 0)
             abandon("Envoi requete");
         /* 4. Lecture de la réponse */
         longueur_reponse =
             recvfrom(fd, reponse, TAILLE_TAMPON, 0, NULL, 0);
         if (longueur_reponse < 0)
             abandon("Attente réponse");
         printf("REPONSE> %s\n", reponse);
         close(fd);
         printf("CLIENT> Fin.\n");
         return EXIT_SUCCESS;
     } 

Explications

....


IV) Communication inter-processus 

Généralités

Les concepts utilisés pour les sockets UNIX/Linux (socket local) et les sockets internet sont  identiques; le Langage C fournit un certain nombre de routines (fonctions) qui permettent de les utiliser. En ce qui concerne Windows, Microsoft a rajouté quelques MACROs qui peuvent rendre les programmes non portables d'un environnement à un autre.
Comme pour les sockets Internet qu'on a vu ci-dessus, la création d'une socket UNIX est réalisée par la routine socket(int domain, int type, int protocol) qui retourne un entier appelé descripteur. Le paramètre "domaine" indique le «domaine de communication» utilisé; pour les communications locales c' est PF_LOCAL synonyme de PF_UNIX. Le paramètre "type" est l'un des types SOCK_DGRAM (communication par messages -blocs contenant des octets- appelés datagrammes) ou SOCK_STREAM (communication par flot -bidirectionnel- d'octets une fois que la connection est établie) et quand au paramètre "protocol" il indique le protocole selectionné; ce protocole est égal à zéro ce qui correspond au protocole par défaut pour le domaine et le type sélectionné.

La fonction socket(int domain, int type, int protocol) crée un socket anonyme. Pour qu'un autre processus puisse le désigner, il faut lui associer un nom par l'intermédiaire d'une adresse contenue dans une structure appelée sockaddr_un. L'association d'une adresse à une socket se fait par la routine C "bind(int descsock, struct sockaddr *ptlocsockaddr, int locsockaddrlen)"; ceci associe l’adresse locale "*ptlocsockaddr" à la socket "descsock"  et le paramètre "locsockaddrlen" représente la taille de l’adresse "*ptlocsockaddr". Cette adresse est un chemin d'accès dans l'arborescence des fichiers et des répertoires.
Pour utiliser cette structure "sockaddr_un" dans programme  il faut y inclure les lignes de code suivantes:
#include <sys/types.h >
#include <sys/socket.h >
#include <sys/un.h >
socklen_t longueur_adresse;
struct sockaddr_un adresse;
adresse.sun_family = AF_LOCAL;
strcpy(adresse.sun_path,"/tmp/xyz");
longueur_adresse = SUN_LEN (&dresse);

Dans ces quelques lignes d'instrution qui permettront d'utiliser les sockets UNIX, on voit bien les fonctions en-tête à utiliser et l'utilisation de la variable "adresse" de type "struct sockadd_un":

  struct sockaddr_un {
        sa_family_t  sun_family;               /* AF_UNIX  */
        char         sun_path[UNIX_PATH_MAX];  /* pathname */
};

et l'utilisation de la MACRO SUN_LEN() définie dans <sys/un.h> (le champ sun_path doit se terminer avec un caractère NULL).

......

On va illustrer l'utilisation des fonctions de l'API Socket en construisant des applications qui serviront de serveur et/ou de clients.


Communication inter-processus par datagramme:

Dans l'échange de données en mode non connectée (Datagrammes) chaque acteur (le serveur ou le client) suit une démarche précise.

Le client doit :

  • Créer la socket par la fonction socket(..)
  • Envoyer le message par la fonction sendto(int s, const void *msg, size_t len, int flags,const struct sockaddr *to, socklen_t tolen). sendto utilise le descripteur de socket "s" pour envoyer le message formé des "len" premiers octets de "msg" à l'adresse de longueur "tolen" pointée par "to". Le même descripteur peut être utilisé pour des envois à des adresses différentes.
  • Fermer la socket par la fonction close(..) une fois les message envoyées.

Quand au serveur il doit:

  • Créer la socket par la fonction socket(...)
  • Attacher une adresse à cette socket par la fonction bind(...)
  • Et par la fonction recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen) attendre l'arrivée d'un datagramme qui est stocké dans les "len" premiers octets du tampon "buff". Si "*from" n'est pas NULL, l'adresse de la socket émetteur qui peut servir à expédier une réponse est placée dans la structure pointée par "*from", dont la longueur maximale est contenue dans l'entier pointé par "fromlen". Si la lecture est réussie, la fonction retourne le nombre d'octets du message lu, et la longueur de l'adresse est mise à jour.

........


Communication inter-processus par datagramme: réalisation d'un serveur UDP

Le serveur reçoit des datagrammes par une socket du domaine local et les affiche; il s'arrête quand la donnée envoyée est "stop".

  /* serveur-dgram-local.c   */     
       #include <stdio.h>
       #include <unistd.h>
     #include <stdlib.h >
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #define TAILLE_MAX_DONNEE 1024
      void abandon(char message[]){
          perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[]){
          socklen_t longueur_adresse;
          struct sockaddr_un adresse;
          int fd;
          if (argc != 2) {
              fprintf(stderr, "usage: %s chemin\n", argv[0]);
              abandon("mauvais nombre de parametres");
          }
          adresse.sun_family = AF_LOCAL;
          strcpy(adresse.sun_path, argv[1]);
          longueur_adresse = SUN_LEN(&adresse);
          fd = socket(PF_LOCAL, SOCK_DGRAM, 0);
          if (fd < 0)
              abandon("Création du socket serveur");
          if ( bind(fd, (struct sockaddr *) &adresse, longueur_adresse) < 0)
              abandon("Nommage du socket serveur");
          printf("> Serveur démarré sur socket local \"%s\"\n", argv[1]);
          while (1) {
              int lg;
              /* un caractère supplémentaire permet d'ajouter le
                 terminateur de chaîne, qui n'est pas transmis */
              char tampon[TAILLE_MAX_DONNEE + 1];
              lg = recvfrom(fd, tampon, TAILLE_MAX_DONNEE, 0, NULL, NULL);
              if (lg <= 0)
                  abandon("Réception datagramme");
              tampon[lg] = '\0';      /* ajout terminateur */
              printf("Reçu : %s\n", tampon);
              if (strcmp(tampon, "stop") == 0) {
                  printf("> arrêt demandé\n");
                  break;
              }
          }
          close(fd);
          unlink(argv[1]);
          return EXIT_SUCCESS;
      }

Détails

Comme il est dit un peu plus haut, le serveur commence par créer la socket  par la routine socket, attache la socket à une adresse et puis se met en attente du client; comme on échange des datagrammes, cette attente se fait par la fonction recevfrom:
le paramètre PF_LOCAL de la routine socket()  montre bien qu'on est entrain de créer une socket pour une communication inter_processus, SOCK_DGRAM  précise que ce qu'on échange ce sont des datagrammes; le dernier paramètre vaut zéro car on n'a pas besoin de préciser le protocol puisque au vu des deux premiers paramètres le système sait quel protocole est utilisé. La routine bind() attache la socket à une adresse et enfin  le serveur se met en attente l'arrivée d'un client.


Communication inter-processus par datagramme: réalisation d'un client UDP

Le client (client-dgram-local) envoie des datagrammes à une socket du domain local, et à charge au serveur de les afficher.

/* client-dgram-local.c */
/* 
          Usage: client-dgram-local chemin messages
          Ici chemin indique localhost si client 
          et serveur s'y trouvent.
          les autres des chaînes de caractères.
          Exemple : client-dgram-local un deux "trente et un" stop
  */
       #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #define TAILLE_MAX_DONNEE 1024
      void abandon(char message[])
      {
         perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[])
      {
          socklen_t longueur_adresse;
          struct sockaddr_un adresse;
          int fd;
          int k;
          if (argc <= 2) {
              fprintf(stderr, "usage: %s chemin message\n", argv[0]);
              abandon("mauvais nombre de paramètres");
          }
          adresse.sun_family = AF_LOCAL;
          strcpy(adresse.sun_path, argv[1]);
          longueur_adresse = SUN_LEN(&adresse);
          fd = socket(PF_LOCAL, SOCK_DGRAM, 0);
          if (fd < 0)
              abandon("Création du socket client");
          for (k = 2; k < argc; k++) {
              int lg;
              lg = strlen(argv[k]);
              if (lg > TAILLE_MAX_DONNEE)
                  lg = TAILLE_MAX_DONNEE;
              /* le message est envoyé sans le terminateur '\0' */
              if (sendto(fd, argv[k], lg, 0,
                         (struct sockaddr *) &adresse,
                         longueur_adresse) < 0)
                  abandon("Expédition du message");
              printf(".");
              fflush(stdout);
              sleep(1);
          }
          printf("OK\n");
          close(fd);
          return EXIT_SUCCESS;
      } 

Détails

 On a la création de la socket par la routine socket(PF_Local, SOCK_DGRAM,0); il est précisé via les paramètres de l'appel de la fonction socket (...) qu'on a, utilisation de socket UNIX (voir PF_LOCAL), une échange de datagrammes (voir SOCK_DGRAM); une fois que les données sont prêtes on les envoie par la routine sendto 

.......


Communication inter-processus par flot de données: réalisation d'un client

L'application qui sert de Client suit un certain nombres d'étapes qui font appel aux fonctions suivantes de l'API Socket:

  1. la fonction socket() telle que:  fd= socket(PF_LOCAL, SOCK_STREAM,0) qui  créé la socket
  2. la fonction connect() telle que: connect(fd, (struct sockaddr *) &adresse, longueur_adresse )
  3. une boucle for(;;) par exemple)qui permettra de faire les actions suivantes:
    {
    write() pour émission 
    read() pour receptionr
    }
  4. la fonction close(fd) pour fermer la socket

Le client envoie (en mode connecté) des données par une socket locale : il ouvre une socket, envoie sur cette socket du texte lu à partir de l'entrée standard, attend et affiche la réponse.

Exemple de client TCP

/*     
         client-stream
         Envoi/réception de données par un socket local (mode connecté)
  */       
       #include <unistd.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #include <signal.h>
      #include <stdio.h>
      #include <string.h>
      #include <stdlib.h>
      #define TAILLE_TAMPON 1000
      void abandon(char message[])
      {
          perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[])
      {
          char *chemin;               /* chemin d'accès du socket serveur */
          socklen_t longueur_adresse;
          struct sockaddr_un adresse;
          int fd;
          /* 1. réception des paramètres de la ligne de commande */
          if (argc != 2) {
              printf("Usage: %s chemin\n", argv[0]);
              abandon("Mauvais nombre de paramètres");
          }
          chemin = argv[1];
          /* 2. Initialisation du socket */
          /* 2.1 création du socket   */
          fd = socket(PF_LOCAL, SOCK_STREAM, 0);
          if (fd < 0)
              abandon("Création du socket client");
          /* 2.2 Remplissage adresse serveur */
          adresse.sun_family = AF_LOCAL;
          strcpy(adresse.sun_path, chemin);
          longueur_adresse = SUN_LEN(&adresse);
          /* 2.3 connexion au serveur */
          if (connect(fd, (struct sockaddr *) &adresse, longueur_adresse)
              < 0)
              abandon("connect");
          printf("CLIENT> Connexion établie\n");
          /* 3. Lecture et envoi des données */
          for (;;) {
              char tampon[TAILLE_TAMPON];
              int nb_lus, nb_envoyes;
              nb_lus = read(0, tampon, TAILLE_TAMPON);
              if (nb_lus <= 0)
                  break;
              nb_envoyes = write(fd, tampon, nb_lus);
              if (nb_envoyes != nb_lus)
                  abandon("envoi données");
          }
          /* 4. Fin de l'envoi */
          shutdown(fd, 1);
          printf("CLIENT> Fin envoi, attente de la réponse.\n");
          /* 5. Réception et affichage de la réponse */
          for (;;) {
              char tampon[TAILLE_TAMPON];
              int nb_lus;
              nb_lus = read(fd, tampon, TAILLE_TAMPON - 1);
              if (nb_lus <= 0)
                  break;
              tampon[nb_lus] = '\0';  /* ajout d'un terminateur de chaîne */
              printf("%s", tampon);
          }
          /* et fin */
          close(fd);
          printf("CLIENT> Fin.\n");
          return EXIT_SUCCESS;
      }

Détails

.....


Communication interprocessus par flot de données: réalisation d'un Serveur

L'application qui joue le rôle de serveur  suit les étapes suivantes:

  1. socket()
  2. bind()
  3. listen()
  4. while (1) {
    1. accept()
    2. while (x) { read() ou write() }
    3. close()
    4. }
  5. close()

Le serveur monotâche (serveur-stream-monotache) envoie des données par une socket local; ce serveur monotâche gère plusieurs connexions; dès qu'il reçoit une demande de connexion, il lit le texte reçu et envoie une réponse.Comme il traîte une seule communication à la fois, Si le client fait traîner les choses, les autres clients en attente resteront bloqués longtemps.

Exemle de serveur TCP

 /*  
         serveur-stream-monotache
         Envoi/réception de données par un socket local (mode connecté)
         Exemple de serveur monotâche qui gère plusieurs connexions
         - attend une connexion
         - lit du texte
         - envoie une réponse
  */
       #include <unistd.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #include <signal.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <ctype.h>
      #include <assert.h>
      #define TAILLE_TAMPON             1000
      #define MAX_CONNEXIONS_EN_ATTENTE 4
      #define MAX_CLIENTS               10
      /* les données propres à chaque client */
      #define INACTIF -1
      struct {
          int fd;
          int numero_connexion;
          int compteur;
      } client[MAX_CLIENTS];
      void abandon(char message[])
      {
          perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[])
      {
          int fd_serveur;
          struct sockaddr_un adresse;
          size_t taille_adresse;
          char *chemin;
          int nb_connexions = 0;
          int i;
          int nbfd;
          /* 1. réception des paramètres de la ligne de commande */
          if (argc != 2) {
              printf("usage: %s chemin\n", argv[0]);
              abandon("mauvais nombre de paramètres");
          }
          chemin = argv[1];
          /* 3. Initialisation du socket de réception */
          /* 3.1 création du socket   */
          fd_serveur = socket(PF_LOCAL, SOCK_STREAM, 0);
if (fd_serveur < 0)
abandon("Création du socket serveur");
/* 3.2 Remplissage adresse serveur */
adresse.sun_family = AF_LOCAL;
strcpy(adresse.sun_path, chemin);
taille_adresse = SUN_LEN(&adresse);
/* 3.3 Association de l'adresse au socket */
taille_adresse = sizeof adresse;
if (bind(fd_serveur, (struct sockaddr *) &adresse, taille_adresse)
< 0)
abandon("bind");
/* 3.4 Ce socket attend des connexions mises en file d'attente */
listen(fd_serveur, MAX_CONNEXIONS_EN_ATTENTE);
printf("SERVEUR> Le serveur écoute le socket %s\n", chemin);
/* 3.5 initialisation du tableau des clients */
for (i = 0; i < MAX_CLIENTS; i++) {
client[i].fd = INACTIF;
}
/* 4. boucle du serveur */
for (;;) {
fd_set lectures;
/* 4.1 remplissage des masques du select */
FD_ZERO(&lectures);
FD_SET(fd_serveur, &lectures);
for (i = 0; i < MAX_CLIENTS; i++) {
if (client[i].fd != INACTIF)
FD_SET(client[i].fd, &lectures);
}
/* 4.2 attente d'un événement (ou plusieurs) */
nbfd = select(FD_SETSIZE, &lectures, NULL, NULL, NULL);
assert(nbfd >= 0);
/* 4.3 en cas de nouvelle connexion : */
if (FD_ISSET(fd_serveur, &lectures)) {
/* si il y a de la place dans la table des clients,
on y ajoute la nouvelle connexion */
nbfd--;
for (i = 0; i < MAX_CLIENTS; i++) {
if (client[i].fd == INACTIF) {
int fd_client;
fd_client = accept(fd_serveur, NULL, NULL);
if (fd_client < 0)
abandon("accept");
client[i].fd = fd_client;
client[i].numero_connexion = ++nb_connexions;
client[i].compteur = 0;
printf
("SERVEUR> arrivée de connexion #%d (fd %d)\n",
client[i].numero_connexion, fd_client);
break;
}
if (i >= MAX_CLIENTS) {
printf("SERVEUR> trop de connexions !\n");
}
}
};
/* 4.4 traitement des clients actifs qui ont reçu des données */
for (i = 0; (i < MAX_CLIENTS) && (nbfd > 0); i++) {
if ((client[i].fd != INACTIF) &&
FD_ISSET(client[i].fd, &lectures)) {
int nb_lus;
char tampon[TAILLE_TAMPON];
nbfd--;
nb_lus =
read(client[i].fd, tampon, TAILLE_TAMPON - 1);
printf("SERVEUR> données reçues de #%d (%d octets)\n",
client[i].numero_connexion, nb_lus);
if (nb_lus > 0)
client[i].compteur += nb_lus;
else {
int nb_car;
printf
("SERVEUR> envoi de la réponse au client #%d.\n",
client[i].numero_connexion);
nb_car =
sprintf(tampon,
"*** Fin de la connexion #%d\n",
client[i].numero_connexion);
write(client[i].fd, tampon, nb_car);
nb_car =
sprintf(tampon,
"*** Vous m'avez envoyé %d caractères\n",
client[i].compteur);
write(client[i].fd, tampon, nb_car);
nb_car =
sprintf(tampon, "*** Merci et à bientôt.\n");
write(client[i].fd, tampon, nb_car);
close(client[i].fd);
/* enlèvement de la liste des clients */
client[i].fd = INACTIF;
}
}
}
assert(nbfd == 0); /* tous les descripteurs ont été traités */
}
/* on ne passe jamais ici (boucle sans fin) */
return EXIT_SUCCESS;
}

Détails

La technique utilisée ici permet de traiter plusieurs connexions par un processus unique : le serveur maintient une liste de descripteurs ouverts, fait une boucle autour d'un select(), en attente de données venant d'un des descripteurs des clients, et il traite alors les données venant de ce client (il l'enlève de la liste en fin de communication). Cette technique conduit à des performances nettement supérieures aux serveurs multiprocessus ou multithreads (pas de temps perdu à lancer des processus), au prix d'une programmation qui oblige le programmeur à gérer lui-même le ``contexte de déroulement'' de chaque processus.
A la ligne 80 on a la fonction select ("int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);") qui permet attendre que des données soient prêtes à être lues sur un des descripteurs de l'ensemble readfs, ou que l'un des descripteurs de writefds soit prêt à recevoir des écritures, que des exceptions se produisent (exceptfds), ou encore que le temps d'attente timeout soit épuisé. Lorsque select() se termine, readfds, writefds et exceptfds contiennent les descripteurs qui ont changé d'état. select() retourne le nombre de descripteurs qui ont changé d'état, ou -1 en cas de problème.
L'entier n doit être supérieur (strictement) au plus grand des descripteurs contenus dans les 3 ensembles (c'est en fait le nombre de bits significatifs du masque binaire qui représente les ensembles). On peut utiliser la constante FD_SETSIZE. Les pointeurs sur les ensembles (ou le délai) peuvent être NULL, ils représentent alors des ensembles vides (ou une absence de limite de temps). Les macros FD_CLR, FD_ISSET, FD_SET, FD_ZERO permettent de manipuler les ensembles de descripteurs.

.........


V) Exemples utilisant les sockets :

L'application Telnet (Tous les caractères que vous tapez doivent arriver dans le même ordre que celui dans lequel ils ont été tapés), les navigateurs Web et les serveurs exploitant le protocole HTTP utilisent les sockets de flux pour recevoir des pages. Cela se vérifie si vous exécutez telnet sur un serveur WWW en spécifiant le port 80 et que vous tapez "GET pagename", alors vous aurez en retour la page HTML!
Les sockets de flux utilisent un protocole appelé ""The Transmission Control Protocol" plus connu sous le nom de "TCP" (Voir RFC-793 pour beaucoup plus de détails sur TCP). TCP s'assure que vos données arrivent séquentiellement et sans erreurs. TCP s'associe au protocol IP et ce dernier traite uniquement le routage Internet.

précédent suivant