Communication avec un serveur d'images

Le but de ce TP est de programmer un client capable de se connecter à un serveur d'affichage d'images. Le client et le serveur communiquent grâce à des sockets.

Préliminaires

Le serveur

Le serveur est déjà programmé, vous n'aurez qu'à programmer divers clients. La première chose à faire est donc de télécharger les sources du serveur, les décompresser (tar xzf) et les compiler (make -f Makefile.sun sur les Sun et make -f Makefile.pc sur les PC).

Le serveur stocke un ensemble d'images qui lui sont envoyées par les clients. Il obéit également à quelques ordres simples du type: afficher telle image, détruire telle image, etc. À chaque image est associé un identificateur global (entre 0 et 65535) choisi par le client quand il définit l'image. Celui-ci permet aux clients de référencer l'image dans les requêtes suivantes (par exemple, pour l'afficher). Notez que l'identificateur est global au serveur. Ainsi, un client peut référencer une image définie par un autre client, si il connaît son identificateur. De plus, l'identificateur et l'image associée continuent d'exister même après que le client a fermé sa connection et a quitté.

Le serveur accepte des requêtes TCP et UDP sur le port PORT_IP, ainsi que sur la socket Unix de nom ADDR_UNIX (créée par le serveur). Les symboles PORT_IP et ADDR_UNIX sont définis dans le fichier protocol.h. Notez que le serveur accepte des requêtes simultanées de plusieurs clients.

On lancera le serveur ./gserver avec l'option -d pour lui faire afficher des informations de débogage bien utiles.

Le protocole

Notre protocole n'est pas réaliste: il s'agit d'un exemple académique développé pour ce TP. Il est toutefois inspiré du protocole de X Windows.

Le protocole est relativement simple: après avoir établi la connection, le client envoie une requête. Le serveur traite cette requête et renvoie une réponse. Le client peut alors envoyer une nouvelle requête sur la même connection ou bien clore la connection.

Le fichier protocol.h

Le protocole est binaire. Il s'agit d'envoyer et de recevoir des objets de type struct C. Tous les types et constantes dont le client aura besoin sont définis dans le fichier protocol.h inclus avec les sources du serveur. Ce fichier constitue également une documentation précise du protocole. Nous ne décrivons ici que ses principes de base et renvoyons le lecteur à protocol.h pour tout complément d'information.

Les requêtes

Les requêtes acceptables sont définies par des types struct ayant toutes pour préfixe req. Toutes les requêtes commencent par les deux champs suivant:

Les autres champs dépendent du type de la requête (voir protocol.h).

Les réponses

Le serveur renvoie toujours une structure de type ans. Le champ type contient soit un code d'erreur, soit ANS_OK qui indique que la requête a pu être traitée. Le champ data est utilisé pour les requêtes-questions qui attendent une valeur en retour (pour l'instant, uniquement reqQueryImage) et vaut toujours 0 dans les réponses aux autres requêtes.

Exercices

On ne cherchera pas à programmer toutes les requêtes dans chacun de nos clients. Chaque exercice n'en demandera qu'une ou deux.

Connection par socket Unix

1) Programmez un client qui se connecte au serveur par une socket de flux Unix. Dans un premier temps, on se contentera d'envoyer une simple requête reqSetup permettant de changer la taille de la fenêtre ouverte par le serveur. On rappelle qu'il faut d'abord créer la socket (socket avec pour domaine PF_UNIX et type SOCK_STREAM), puis construire une adresse Unix struct sockaddr_un en précisant le bon type (AF_UNIX) et le chemin du serveur (ADDR_UNIX), et enfin établir la connection (connect) à l'adresse construite. La socket peut alors être utilisée comme n'importe quel fichier ouvert en lecture et en écriture (full-duplex), et se ferme par close.

Correction) client1.c

2) Modifiez votre client pour qu'il prenne en argument le nom d'un fichier image PNG, l'envoie au serveur (requête reqDefImage) et l'affiche (requête reqShowImage). Dans une requête reqDefImage, le contenu du fichier image suit directement la structure reqDefImage. Le champ size indique la taille totale: structure plus taille de l'image. Vous pouvez ensuite améliorer le client pour qu'il envoie une liste d'images au serveur. Grâce aux champs delay et trans de reqShowImage, il est possible de démarrer un diaporama (qui continuera de tourner même après que le client a quitté).

Correction) client2.c

Connection TCP/IP

3) Modifiez votre client pour qu'il utilise une socket TCP Internet (socket de domaine PF_INET et de type SOCK_STREAM). Le client prendra en argument le nom de l'ordinateur à contacter. On pourra utiliser gethostbyname pour la conversion nom vers IP.

Vérifiez que vous pouvez lancer un client depuis un ordinateur distant sur un serveur local. Dans un premier temps, choisissez deux machines de même type (Sun ou PC) comme client et serveur.

Si le client et le serveur sont des machines de type différent, l'encodage des types C n'est pas forcément le même. En particulier, il existe deux conventions pour l'ordre des octets dans un entier: big endian (octet de poids fort en premier) et little endian (octet de poids faible en premier). Si les machines utilisent des conventions différentes, il sera nécessaire à l'une d'entre elles de retourner les octets dans tous les champs des requêtes et des réponses. Dans notre cas, le serveur peut faire cette transformation à condition qu'on lui envoie une requête reqSetEndianess.

Correction) client3.c

4) Modifiez votre client pour commencer chaque connection par une requête reqSetEndianess afin d'imposer au serveur l'ordre des octets utilisé par le client. Essayez, si possible, de déterminer cet ordre par un test dynamique plutôt que d'utiliser une constante en dur. La requête affecte uniquement les messages sur la socket où elle a été émise.

Correction) client4.c

Remarque: Nous avons pris deux mesures pour améliorer la portabilité: d'abord en utilisant des entiers de taille fixée indépendemment de la machine (uint8_t, uint16_t et uint32_t), et ensuite en permettant au serveur et au client de se mettre d'accord sur un ordre des octets. En théorie, ce n'est pas suffisant. D'autres indéterminées concernant le codage bas-niveau des types C peuvent poser problème. Par exemple, le compilateur insère des blancs (padding) entre les champs d'une structure afin d'aligner leur adresse en mémoire, selon des règles qui dépendent du processeur, du système d'exploitation, des options de compilation, etc. Pour ces raisons, on évite généralement d'écrire des structures C complètes dans des fichiers ou des sockets. À la place, on définit chaque message du protocole au niveau de l'octet et on lit et écrit les champs un par un, octet par octet. Cette opération assez pénible peut être automatisée grâce à des langages d'IDL (Interface Definition Language), par exemple celui des RPC de Sun. L'alignement standard des champs étant compatible entre Sun et PC, nous ne prenons pas ces précautions dans ce TP...

Segments de mémoire partagée

Quand le serveur et le client tournent sur la même machine, on peut éviter de passer par la socket pour transférer l'image en utilisant, à la place, un segment de mémoire partagée. Ceci se traduit par un gain de vitesse. Pour cela, le client doit:

5) Remplacez dans un des clients précédents les requêtes reqDefImage par reqDefImageSHM.

Correction) client5.c

Connection UDP

On s'intéresse maintenant à des clients qui communiquent au serveur par paquets UDP (sockets de domaine PF_INET et de type SOCK_DGRAM). On pourra utiliser au choix les appels systèmes read et write ou bien recvfrom et sendto.

Attention: quand on communique par UDP, le serveur ignore les requêtes reqSetEndianess. L'ordre des octets est supposé toujours être big endian (ordre 'réseau'). Un client sur une machine little endian doit donc retourner lui-même les champs de type uint16_t et uint32_t avant d'envoyer une requête (htons & co.).

6) Programmez un client qui affiche à l'écran la liste des identificateurs des images actuellement connues par le serveur. On utilisera pour cela plusieurs requêtes reqQueryImage. Est-il possible que la liste renvoyée soit incohérente (i.e., ne corresponde pas à l'état courant du serveur, ni même à un état passé) ?

Correction) client6.c

7) Programmez un client qui envoie une requête reqDefImage. Que se passe-t-il si l'image est trop grosse?

Correction) client7.c


Antoine Miné