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 nest 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 dune 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 dun 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
|