Cours 4 : La programmation en mode TCP

 
 
La communication en mode TCP est un mode de communication fiable, orientée connexion. Une fois, les points de communication crées sur la pile de protocole, le client et le serveur doivent établir une connexion avant de pouvoir s'échanger des données sous forme flux d'octets TCP. L'échange des adresses entre émetteur et récepteur s'effectue au moment de l'établissement de la connexion. Lorsque l'échange est terminé, la connexion est fermée. 

1 Etablissement d'une connexion TCP/IP


Fig 4 : Création des points de communication TCP et établissement de connexion
 
 
Chaque entité communicante, le client et le serveur, commencent par créer une socket de type SOCK_STREAM, puis chacun attâche à cette socket son adresse locale, formée du numéro de port de l'application (client ou serveur) et de l' adresse IP des machines client ou serveur.
Le serveur, ensuite, se prépare en vue de recevoir des connexions de la part de clients. Pour cela, il fait d'abord appel à la primitive Listen pour dimensionner sa file de connexions pendantes sur sa socket initiale que nous appelerons la socket d'écoute..
int listen (sock, nb)
 
 
        int sock; /* socket d’écoute */
 
 
        int nb;  /* nb, nombre de connexions pendantes maximal */
 
 
 
 
Le serveur se met alors en attente de connexion par un appel à la primitive accept. Cette primitive est bloquante, c'est-à-dire que le serveur reste bloqué sur cette primitive jusqu'à ce qu'un client établisse effectivement une connexion. 
int accept (sock, p_adr, p_lgadr)
 
 
          int sock;  /* socket d'écoute */
 
 
          struct sockaddr_in *p_adr; /* adresse de la socket connectée */
 
 
          int *p_lgadr; /* taille en octets de la structure adresse */
 
 
 
 
De son côté, le client demande l'établissement d'une connexion en faisant appel à la primitive connect. De la même manière, la primitive connect est bloquante pour le client, c'est-à-dire que le client est bloqué jusqu'à ce que le serveur accepte sa connexion.
int connect (sock, p_adr, lgadr)
 
 
       int sock; /* socket client */
 
 
       struct sockaddr_in *p_adr; /* adresse de la socket du serveur */
 
 
       int lgadr; /* taille en octets de la structure adresse */
 
 
 
 
Au moment de l'établissement de la connexion, le serveur crée une nouvelle socket que l'on appelle la socket de service. Le descripteur de cette nouvelle socket est retourné par la primitive accept. Cette socket de service est la socket sur laquelle s'effectuent ensuite tous les échanges de données entre les deux entités connectées par le biais des primitives read et write.. 
Fig 5 : Création de la socket de service au moment de l'établissement de la connexion
Définition : Primitive Listen
int listen (int sock, int nb) : création de la file de connexions pendantes (n max) associée à la socket d'écoute sock
Définition : Primitive Connect
int connect (int sock, struct sockaddr_in *p_adr, int lgadr) : demande d'établissement de connexion avec la socket distante dont l'adresse est *p_adr
Définition : Primitive Accept
sock_service = accept (int sock_ecoute, struct sockaddr_in p_adr, int p_lgadr) : Acceptation de connexion sur la socket locale d'écoute avec la socket distante p_adr. En résultante, création de la socket de service sock_service.

2 Echange de données en mode TCP

 
 
L'échange de données s'effectuent sous forme de flux d'octets. Il faut prendre garde au fait que la structure des messages n'est pas conservée et le découpage en messages identifiables correspondant aux différents envois n’est pas préservé sur la socket destinataire.
  • Une opération de lecture peut provenir de différentes opérations d’écriture
  • Une opération d’écriture d’une chaine longue peut provoquer son découpage, les différents fragments étant accessibles sur la socket destinataire       
 
 
La primitive write permet l'envoi d'un ensemble d'octets constituant un message. La primitive read permet la réception d'un ensemble d'octets. 
int write (sock, msg, lg)
 
 
       int sock;
 
 
       char *msg; /* adresse de la zone mémoire contenant du message à envoyer */
 
 
        int lg; /* taille en octets du message */
 
 
     
 
 
     
 
 
int read (sock, msg, lg)
 
 
       int sock;
 
 
       char *msg; /* adresse de la zone mémoire pour recevoir le message */
 
 
       int lg; /* taille en octets du message */
 
 
Définition : Primitive Read (réseau)
int read (int sock,char *msg, int lg) : lecture du message msg de taille lg octets sur la socket sock en mode connecté
Définition : Primitive Write (réseau)
int write (int sock,char *msg, int lg) : écriture du message msg de taille lg octets sur la socket sock en mode connecté

3 Un exemple de programmation TCP : réalisation d'un serveur itératif

 
 
Nous donnons ci-dessous un premier exemple de programmation d'un client-serveur en mode TCP : le serveur écrit dans cet exemple correspond à un serveur de type itératif composé d'un seul processus qui reçoit les connexions, remplit le service demandé, puis rompt la connexion avant d'en accepter une nouvelle. Le service remplit par le serveur consiste tout simplement à afficher une chaine de caractères envoyées par le client. On notera la procédure de lecture de la chaine de caractères sur la socket serveur, qui fait une lecture caractère par caractère jusqu'à trouver le caractère '\n' afin d'être certain d'avoir récupéré la chaine en entier.
 
 
SERVEUR
 
 
     
 
 
     
 
 
  
 
 
************************************************
 
 
Serveur affichant à l'écran la ligne envoyée par le client
 
 
************************************************
 
 
  
 
 
#include <stdio.h>
 
 
#include <sys/types.h>
 
 
#include <sys/socket.h>
 
 
#include <netinet/in.h>
 
 
#include <netdb.h>
 
 
#include <pwd.h>
 
 
#define LGUSER 20
 
 
#define LGREP 256
 
 
#define PORT 6259  /* port du serveur */#define TRUE 1
 
 
 
 
main ()
 
 
{
 
 
int lg, port, sock, nsock, d; /* sock socket d'écoute, nsock socket de service */
 
 
struct sockaddr_in adr; /* adresse de la socket distante */
 
 
 
 
if ((sock = creesock(port,SOCK_STREAM)) == -1) {
 
 
    fprintf (stderr, "Echec creation/liaison de la socket\n");
 
 
    return;
 
 
}
 
 
 
 
/* Creation de la file des connexions pendantes */
 
 
listen(sock,5);
 
 
 
 
/* Boucle d'acceptation d'une connexion */
 
 
 
 
while (TRUE) { /* Attente de question sur la socket */
 
 
       lg = sizeof(adr);
 
 
       nsock = accept (sock,&adr,&lg);
 
 
       service(nsock);
 
 
       close(nsock);
 
 
}
 
 
}
 
 
 
 
************************************************
 
 
Fonction service
 
 
************************************************
 
 
 
 
int service (nsock)
 
 
int nsock;
 
 
{
 
 
int n;
 
 
char line[MAXLINE];
 
 
 
 
n = readline(nsock,line,MAXLINE);
 
 
if (n < 0) {
 
 
      perror ("Pb lecture de ligne");
 
 
return; }
 
 
fputs (line, stdout);
 
 
}
 
 
 
 
*********************************************************
 
 
Fonction readline : lit une ligne sur la socket fd caractère par caractère
 
 
*********************************************************
 
 
 
 
int readline(fd,ptr,maxlen)
 
 
int fd;
 
 
char *ptr;
 
 
int maxlen;
 
 
{
 
 
int n, rc;
 
 
char c;
 
 
   
 
 
 
 
for (n=1; n<maxlen; n++)
 
 
{
 
 
   rc = read(fd, &c, 1);
 
 
   *ptr++ = c;
 
 
   if (c == '\n')
 
 
   break;
 
 
}
 
 
*ptr = '\0';
 
 
return(n);
 
 
}
 
 
     
 
 
   
 
 
   
 
 
CLIENT
 
 
 
 
************************************************
 
 
Client : envoie une ligne à afficher au serveur
 
 
************************************************
 
 
  
 
 
#include <stdio.h>
 
 
#include <sys/types.h>
 
 
#include <sys/socket.h>
 
 
#include <netinet/in.h>
 
 
#include <netdb.h>
 
 
#define PORT 6259 /* port du serveur */
 
 
#define PORTC 6260 /* port du client */
 
 
#define TRUE 1
 
 
 
 
main ()
 
 
{
 
 
int sock;
 
 
struct sockaddr_in adr;
 
 
struct hostent *hp;
 
 
 
 
if ((sock = creesock(PORTC,SOCK_STREAM)) == -1) {
 
 
     fprintf (stderr, "Echec creation/liaison de la socket\n");
 
 
     return;
 
 
}
 
 
 
 
/* preparation de l'adresse de la socket destinataire */
 
 
 
 
if ((hp = gethostbyname("dirac.cnam.fr")) == NULL) {
 
 
    perror ("Nom de machine irrecuperable");
 
 
    return;
 
 
}
 
 
bzero ((char *)&adr,sizeof(adr));
 
 
adr.sin_port = htons(PORT);
 
 
adr.sin_family = AF_INET;
 
 
bcopy (hp->h_addr, &adr.sin_addr, hp->h_length);
 
 
connect (sock, (struct sockaddr *)&adr, sizeof(adr));
 
 
client(sock);
 
 
close(sock);
 
 
}
 
 
 
 
   
 
 
   
 
 
************************************************
 
 
Fonction client
 
 
************************************************
 
 
 
 
int client(sock)
 
 
int sock;
 
 
{
 
 
int n;
 
 
char line[MAXLINE];
 
 
 
 
printf (" Donnez une ligne : \n");
 
 
fgets (line, MAXLINE, stdin);
 
 
n = strlen (line);
 
 
if (write (sock, line, n) != n)
 
 
    perror ("Pb ecriture de ligne");
 
 
}
 
 

4 Un exemple de programmation TCP : réalisation d'un serveur parallèle

 
 
Le serveur donné en exemple dans le paragraphe précédent est un serveur itératif. Une fois que le serveur a accepté une connexion, de la part d'un client, les clients suivants sont mis en attente jusqu'à ce que le serveur ait fini de traiter le premier client et revienne sur l'accept pour accepter une nouvelle connexion. Durant tout le traitement d'un client connecté, le serveur travaille sur la socket de service et la socket d'écoute ne sert pas.
Il serait ici plus performant de créer un serveur parallèle tel que :
  • Le processus père accepte les connexions sur la socket d'écoute et crée une socket de service pour la connexion acceptée.
  • Pour chaque connexion acceptée, le processus père crée un processus fils. Ce fils hérite naturellement de la socket d'écoute et de la socket de service ouvertes par son père.
  • Le fils s'occupe de servir le client : il travaille donc avec la socket de service et ferme la socket d'écoute.
  • Le père est libéré du service du client réalisé par son fils. Il ferme la socket de service et retourne immédiatement accepter de nouvelles connexions sur la socket d'écoute. Dans ce cas de figure, on aura donc autant de fils crées que de connexions acceptées par le père.
 
 
Nous donnons ci-dessous le code correspondant pour le même exemple que précédemment. Seul le processus serveur est modifié.
 
 
SERVEUR
 
 
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <pwd.h> #include <sys/wait.h> #include <signal.h> #define LGUSER 20 #define LGREP 256 #define PORT 6259 #define TRUE 1 #define MAXLINE 512
 
 
main ()
 
 
{ int lg, port, sock, nsock, d, pid; struct sockaddr_in adr; /* adresse de la socket distante */
 
 
if ((sock = creesock(port,SOCK_STREAM)) == -1) {     fprintf (stderr, "Echec creation/liaison de la socket\n");     return; }
 
 
/* Creation de la file des connexions pendantes */ listen(sock,5);
 
 
/* Boucle d'acceptation d'une connexion */
 
 
while (TRUE) { /* Attente de question sur la socket */       lg = sizeof(adr);       nsock = accept (sock,etadr,etlg);
 
 
     /* creation d’un fils pour chaque connexion acceptée */
 
 
     pid = fork();      if (pid == -1) {          fprintf (stderr, "pb fork\n");          return;      }     else         if (pid == 0) { /* Je suis le fils */               close(sock);                service(nsock);               close(nsock);                exit();         }         else               close (nsock); }
 
 
}
 
 
On notera ici que les processus fils deviennent zombies car le processus père n'effectue pas de wait pour récupérer la mort de ses fils. Pour éviter cette situation, on peut ajouter la ligne suivante au code du serveur qui permet au père d'ignorer le signal mort du fils et de détruire automatiquement en conséquence les processus zombies : Signal (SIGCHLD, SIG_IGN);
Programmation socket La programmation en mode TCP