Les sockets sont basés sur une architecture client/serveur: Le serveur décide d’accepter les demandes de connection sur un port particulier, tandis que le client demander une connexion sur le serveur.

Une fois la connexion établie, les deux programmes peuvent communiquer à l’aide de read et write, exactement de la même manière que lorsque l’on lit/écrit dans un fichier. Un exemple est fourni : le serveur Exemple-simple/socket_srv.c et le client Exemple-simple/socket_clt.c. Vous pourrez suivre les explications en lisant les sources fournis.

Exercice 1. Tester et comprendre chaque ligne de l’exemple.
socket_server.c
/* Serveur sockets TCP
 * affichage de ce qui arrive sur la socket
 *    socket_server port (port > 1024 sauf root)
 */
 
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
 
int main(int argc, char** argv )
{ char datas[] = "hello\n";
  int    sockfd,newsockfd,clilen,chilpid,ok,nleft,nbwriten;
  char c;
  struct sockaddr_in cli_addr,serv_addr;
 
 
  if (argc!=2) {printf ("usage: socket_server port\n");exit(0);}
 
  printf ("server starting...\n");  
 
  /* ouverture du socket */
  sockfd = socket (AF_INET,SOCK_STREAM,0);
  if (sockfd<0) {printf ("impossible d'ouvrir le socket\n");exit(0);}
 
  /* initialisation des parametres */
  bzero((char*) &serv_addr, sizeof(serv_addr));
  serv_addr.sin_family       = AF_INET;
  serv_addr.sin_addr.s_addr  = htonl(INADDR_ANY);
  serv_addr.sin_port         = htons(atoi(argv[1]));
 
  /* effecture le bind */
  if (bind(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))<0)
     {printf ("impossible de faire le bind\n");exit(0);}
 
  /* petit initialisation */
  listen(sockfd,1);
 
  /* attend la connection d'un client */
  clilen = sizeof (cli_addr);
  newsockfd = accept (sockfd,(struct sockaddr*) &cli_addr, &clilen);
  if (newsockfd<0) {printf ("accept error\n"); exit(0);}
  printf ("connection accepted\n");
 
 
  while (1)
   { while (read(newsockfd,&c,1)!=1);
     printf("%c",c); 
   }
 
   /*  attention il s'agit d'une boucle infinie 
    *  le socket nn'est jamais ferme !
    */
 
   return 1;
 }
socket_client.c
/* Client pour les sockets
 *    socket_client ip_server port
 */
 
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
 
 
int main(int argc, char** argv )
{ 
  int    sockfd,newsockfd,clilen,chilpid,ok,nleft,nbwriten;
  char c;
  struct sockaddr_in cli_addr,serv_addr;
 
  if (argc!=3) {printf ("usage  socket_client server port\n");exit(0);}
 
 
  /*
   *  partie client 
   */
  printf ("client starting\n");  
 
  /* initialise la structure de donnee */
  bzero((char*) &serv_addr, sizeof(serv_addr));
  serv_addr.sin_family       = AF_INET;
  serv_addr.sin_addr.s_addr  = inet_addr(argv[1]);
  serv_addr.sin_port         = htons(atoi(argv[2]));
 
  /* ouvre le socket */
  if ((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
    {printf("socket error\n");exit(0);}
 
  /* effectue la connection */
  if (connect(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))<0)
    {printf("socket error\n");exit(0);}
 
 
  /* repete dans le socket tout ce qu'il entend */
  while (1) {c=getchar();write (sockfd,&c,1);}
 
 
  /*  attention il s'agit d'une boucle infinie 
   *  le socket n'est jamais ferme !
   */
 
   return 1;
 
}
Les exemples peuvent être testés en exécutant le client et le serveur sur la même machine, et surtout sur des machines différentes : dans ce cas les ports utilisés doivent être différents pour chaque serveur. Pour éviter les problèmes vous pouvez exécuter le client et le serveur sur deux machines virtuelles.

Pour tester l’exemple :

  1. compiler socket_srv.c et socket_clt.c :
  gcc socket_client.c -o socket_client
  gcc socket_server.c -o socket_server
  1. exécuter le serveur sur une machine :
  ./socket_srv 9999
  1. exécuter le client sur une autre machine et ouvrir une connexion sur le serveur:
 ./socket_clt <ip du serveur> 9999 

Avant toute chose, de la même manière que lorsque l’on ouvre un fichier, le serveur se doit d’obtenir un identificateur de socket avec la fonction socket() (man 2 socket).

int socket(int family, int type, int protocol);

Cette fonction prend trois paramètres qui décrivent les protocoles utilisés.

Le paramètre family (AF_UNIX, AF_INET, AF_INET6, AF_IPX, AF_NETLINK, AF_X25, AF_AX25, AF_ATMPVC, AF_APPLETALK, AF_PACKET) que nous fixons à AF_INET pour choisir IP.

Le paramètre type précise la couche transport. le couple family/type correspond généralement à un couple de protocoles reseau/transport (TCP/IP ou UDP/IP). Dans cet exemple c’est TCP qui est utilisé (SOCK_STREAM pour TCP et SOCK_DGRAM).

Le dernier paramètre protocole, est un paramètre additionnel, que nous mettrons à 0. Socket renvoie alors un identificateur de socket que l’on utilisera par la suite.

int bind(int sockfd, struct sockaddr *myaddr, int addrlen);

Le bind sert a indiquer au système la façon dont il doit accepter les connections de la part d’éventuels clients, il prend comme paramètre l’identificateur de socket sockfd, obtenu lors de l’ouverture du socket, ainsi qu’une structure de données myaddr spécifique au protocole utilisé contenant les informations nécessaire, ainsi que la taille addrlen de cette structure. Le bind revoie une valeur n ́gative s’il n’a pas pu s’effectuer correctement. Dans le cas de TCP il faut remplir une structure de type sockaddr in, en initialisant les champs sin_family, sin_addr.s_addr et sin_port * sin_family est la famille utilisée, ici AF_INET * sin_addr.s_addr indique les adresses d’où peuvent être acceptées les requêtes de connexion, dans notre cas on pourra utiliser la valeur INADDR_ANY. Cependant cette doit être convertie au bon format grâce à la fonction htonl().

  • sin_port est le numéro de port où doit être adressé la connexion, il suffit de choisir un nombre arbitraire plus grand que 1024 (les ports avec des numéros plus petits sont réservés pour des fonctions système). Cette valeur doit être également convertie au bon format grâce à la fonction htons().

Les autre champs de la structure doivent être initialisés à zéro (utiliser memset() ou bzero()).

bzero((char*)&serv_addr,sizeof(serv_addr));
serv_addr.sinfamily = AF_INET; 
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_TCP_PORT);
int listen(int sockfd, int backlog);

le listen sert à spécifier le nombre de connections simultanées que le serveur peut accepter, la valeur la plus communément employée est 5 qui est la plus grande autorisée. listen revoie une valeur négative en cas de problème.

int accept(int sockfd, struct sockaddr *peer, int *addrlen);

Le accept() est un fonction bloquante qui attend qu’une demande de connection arrive, lorsque c’est le cas, une copie du socket est effectuée et son identificateur est renvoyé. Cette manière de faire (copie du socket existant) peut paraître curieuse au premier abord mais elle est très utilise pratique lorsque l’on écrit un serveur capable d’accepter plusieurs connections simultanées.

Les coordonnées de l’auteur de la requête se trouvent dans la structure peer au retour de la fonction accept, de même addrlen permet de connaitre la longueur de la structure en question. Noter que les transactions read/write devront se faire en utilisant l’identificateur de socket renvoyé par accept() et non pas celui renvoyé par socket()

Les read/write permettent ensuite de lire/écrire dans le socket. Ils s’utilisent exactement comme ceux des fichiers binaire, si ce n’est qu’ils utilisent un identificateur de socket au lieu d’un identificateur de fichier.

int close(int sockfd);

Permet de fermer un socket. Il est conseillé de fermer un socket que l’on utilise plus.

La partie client est la partie qui effectue demande de connection, elle se programme en ouvrant tout d’abord un socket avec socket() de la même manière que le serveur, il s’agit ensuite d’établir la connection avec connect().

int connect(int sockfd, struct sockaddr *servaddr, int addrlen);

connect() prend en paramètres, outre l’ID du socket, une structure qui lui indique où il doit essayer de se connecter, il y a trois valeurs à initialiser dans cette structure de type sockaddr (les autres doivent être mises à zéro).

  • sin_family est la famille utilisée, ici AF_INET
  • sin_addr.s_addr contient indique l’adresse du serveur, cette adresse est généralement connue sous forme texte (exemple: 10.9.185.203) et doit être convertie en un entier grâce à la fonction inet_addr().
  • sin_port est le numéro de port du serveur où doit aboutir la connection. Cette valeur doit être également convertie au bon format grace à la fonction htons().

Une fois la connection acceptée, le client n’a plus qu’à lire/écrire dedans et l’aide des fonctions read() et write() puis à le fermer à l’aide de close().

Exercice 2. En vous inspirant de l’exemple précédent écrire deux clients pour les protocoles :
  • Time (RFC 868)
  • Daytime (RFC 867) (optionnel)

Un serveur pour ces protocoles écoute sur la machine lsis.univ-tln.fr.

Attention, il y a plusieurs difficultés :

  • ce protocole retourne maintenant le temps sur 4octets mais sur les machines 64bits les fonctions de conversions prennent en paramètre des entiers sur 8octets. Les données reçues du serveur doivent donc être décodées et stockées dans des variables de type unsigned long int.
  • pour assurer la portabilité de la représentation des nombres, un standard est fixé sur le réseau, il faut donc faire des conversions avant écriture et après lecture (cf. man ntohl).
  • il y a plusieurs habitudes différentes pour indiquer une date (en secondes depuis 1er janvier 1970, depuis le 1er janvier 1900 à minuit UTC, … Pour information, il y a 2208988800s entre les deux).
  • on peut utiliser la fonction ctime pour afficher la date. Attention, elle prend un paramètre de type time_t (codé sur 8 octets). Pour l’afficher avec printf, vous pouvez utiliser un cast vers un unsigned long ou bien le masque “%zu”.

Dans l’exercice précédent votre programme traite les clients les uns après les autres même si ces derniers se sont connectés au même moment au serveur. Généralement quand on développe un serveur on souhaite qu’il puisse dialoguer en simultané avec plusieurs clients. Pour cela après l’exécution de la primitive accept on utilise la primitive fork qui créé un processus fils qui se chargera de la communication pendant que le père pourra retourner en attente sur accept. La syntaxe générale d’un serveur est donc :

while (1)
{
 scomm = accept(sd, NULL,NULL);
 pid = fork();
 if (pid == 0) /* c’est le fils */
 {
  close(sd); /* socket inutile pour le fils */
  ...
  /* traiter la communication */
  ...
  close(scomm);
  exit(0); /* on force la terminaison du fils */
 }
 else /* c’est le pere */
 {
  close(scomm); /* socket inutile pour le pere */
  ....
 }
}
close(sd);

Remarque: Les processus fils qui se terminent deviennent zombies si dans le code du processus père on ne rajoute pas des instructions pour lui indiquer de lire le code retour de ses fils. Pour éviter cette situation vous rajouterez ici (avant la boucle) l’instruction signal(SIGCHLD,SIG_IGN) qui a pour effet sous Linux d’éliminer directement les processus qui se terminent sans les laisser dans l’état zombie (notez que cette façon de traiter la fin des processus fils est déconseillée par la norme POSIX).

Exercice 3. Pour les parcours informatique uniquement. Réécrivez le programme exemple (socket_server et socket_client) de façon à ce que les clients soient traités en parallèle par le serveur.
Exercice 4. Ecrire un programme qui lit le contenu d’une page web (avec http il suffit d’écrire GET URL sur une ligne). Un défi : dans la page suivante extraire les éléments title.
./rss_client 10.1.65.61 80 http://www.univ-tln.fr/backend-breves.php3
Exercice 5. Ecrire un programme qui renvoie la liste des ports TCP ouverts sur une machine dont on passera en paramètre, soit le nom (cf. man gethostbyname), soit l’adresse IP.
maitinfo1:~> ./scan sinfo1
Le port 22 est ouvert
Le port 80 est ouvert
Le port 111 est ouvert
Le port 199 est ouvert
Le port 763 est ouvert
Le port 2049 est ouvert
Le port 4001 est ouvert
Le port 5432 est ouvert
maitinfo1:~> ./scan 10.9.185.1
Le port 22 est ouvert
Le port 80 est ouvert
Le port 111 est ouvert
Le port 199 est ouvert
Le port 763 est ouvert
Le port 2049 est ouvert
Le port 4001 est ouvert
Le port 5432 est ouvert
Ecrire un programme de bataille navale en réseau. Le programme sera tour à tour client puis serveur. Le programme prendra trois paramètres : le numéro du joueur (1 ou 2), n le nombre de bateaux et le port du serveur. Les bateaux sont tous de taille 1 et la grille est de taille n*n. Le fonctionnement est le suivant :
  • les deux joueurs remplissent leur grille (des tableaux de caractères) avec '.’. Les n bateaux sont disposés aléatoirement (attention, à ne pas mettre deux bateaux au même endroit). Chaque joueur affiche sa grille.
  • le joueur 1 est le serveur, le joueur 2 est le client.
  • Le joueur 1 écoute le tir du joueur 2 (deux entiers) et répond avec le nombre de bateaux restants (1 entier).
  • Les rôles sont inversés, jusqu’à ce que le nombre de bateau restants de l’un des joueurs soit 0.
  • Chaque joueurs a donc successivement la fonction écouter et parler.

~~DISCUSSION~~

—- dataentry page —- type : TP enseignement_tags : S52 technologies_tags : Socket, TCP themes_tags : Réseaux