Articles récents
Linux/Unix : Programmer un service réseau en C
Voilà un chapitre passionnant! Quoi de plus motivant que celui de programmer un code qui discute en réseau? En effet, communiquer est primordial de nos jours. Si tu ne communiques pas, t'est mort!
Il n'y a pas 36 méthodes pour communiquer, soit on fait de l'ajax, soit on fait des web services ou soit on fait une programmation de type SOCKET. Nous allons nous intéresser à la programmation socket en C système Linux, qui présente l'avantage d'être native dans le monde UNIX.
1)Les sockets
Un socket est point de communication par lequel un processus transmet et reçoit des informations. Les services Unix ou Linux sont de gros consommateurs de sockets, la commande netstat -a vous permettra de les lister facilement.Pour un processus, un socket est identifié par un descripteur (au même titre qu'un fichier), ce qui permet de lui appliquer les primitives des fichiers comme read, write, .... Ce fonctionnement favorise la mise en réseau des applications standards et permet l'héritage des sockets lors d'un fork. Les différentes constantes sont macro-définies dans les fichiers sys/types.h et sys/socket.h .
1.1)Les sockets Unix
Un socket peut-être local au système, il possède alors une existence dans l’arborescence. La commande ls -l affiche un s en début de ligne pour ce type de fichier. Les sockets Unix sont IPC interne au système.La structure d'une adresse de socket est définie dans le fichier sys/un.h .
1.2)Les sockets du domaine Internet
Un socket du domaine Interne est IPC externe qui permet à deux processus sur des hôtes distants de communiquer. La structure d'une addresse est définie dans le fichier netinet/in.h à inclure.Et pour l'IPV6:
A noter que pour mémoire, le mode non connecté est dédié à l'envoi de datagramme de taille limitée en utilisant le protocole UDP. Le type flot est utilisée au mode connecté et au protocole TCP.
2)Les primitives générales de manipulation
2.1)Création et suppression d'un socket
La primitive socket permet de créer un socket en précisant le domaine (PF_UNIX : IPC local ou PF_INET : IPC externe), son type (SOCK_STREAM en mode connecté et SOCK_DGRAM en mode datagramme). Pour le protocole, il est préférable de laisser à 0, ainsi le système choisira le bon protocole en fonction du type choisi. La primitive renvoie -1 en cas d'erreur.
Pour supprimer un socket, il suffit d'appeler la primitive close avec le descripteur en paramètre. Toutes les ressources seront alors libérées.
2.2)Attachement
Cette fonction sert à attacher notre socket au système, cela revient à le déclarer et le faire prendre en compte par l'OS.Les paramètres du bind sont le sockfd comme descripteur du socket, addr l'adresse IP et addrlen la taille de l'adresse. La fonction retourne 0 en cas de succès et -1 sinon.
Spécificité Unix:
Nous avons vu qu'il était indispensable d'initialiser la structure sockaddr_un. En général on créé deux objets sockets, un pour lire et l'autre pour écrire.
Cette primitive est utilisable pour les types SOCK_STREAM et SOCK_DGRAM. Le paramètre ptr_desc est un tableau de deux entiers dans lequel on récupère les deux descripteurs permettant l'accès aux deux sockets.
Pour initialiser la structure sockaddr_in, on utilise la valeur INADDR_ANY qui représente toutes les adresses IP de la machine. Si on veut que le système choisisse un port automatiquement, on initialise sin_port à 0.
2.3)Gérer le problème endianness
Selon l'architecture du processeur la manière d'organiser les octets est différentes. Il faut prendre en compte ce phénomène et utiliser des primitives adaptée entre la catégorie Big endian et Little endian. Il existe des primitives qui permettent de réaliser un code standard indépendant de la plateforme.Les primitives pour la programmation réseau:
2.4)La fonction getsockname()
La fonction getsockname() retourne l'adresse d'attachement d'un socket, ce qui permet de le connaitre si on a laissé le noyau choisir le numéro de port.3)Les fonctions de résolution et les fichiers administratifs
3.1)Format réseau et format affichable
Les adresses IP qui sont manipulées sont des entiers long, et si on les affichent, ils ne correspondent pas à grand chose. Il existe une fonction de conversion de cet entier en chaîne caractères.Les arguments :
La fonction inet_ntop() retourne un pointeur sur la chaîne de caractère.
Voici un exemple d'utilisation de ces fonctions:
3.2)RĂ©solution du nom par le DNS
node représente le nom DNS ou l'adresse recherchée sous forme de chaîne de caractères, service est le service recherché (le port du service, comme 80 ou http pour un service web), NULL pour tous, hints est une structure définissant les critères de recherche, NULL pour aucun, res représente le résultat. La fonction retourne 0 en cas de réussite, et -1 sinon.
Avec :
Lorsque cette structure est utilisée dans une fonction, tous les champs ne sont pas obligatoires, comme pour la fonction getaddrinfo() il tout de même les initialiser à 0 ou NULL.
La définition des champs suivants est obligatoire pour le paramètres hints de la fonction getaddrinfo():
Les autres champs:
La variable ai_flags peut prendre une combinaison de valeurs suivantes:
Exemple de code pour réaliser une résolution DNS:
3.3)Options de socket
Il est possible de consulter et de modifier les options de sockets créés:Les paramètres de ces deux fonctions:
4) La communication par datagrammes
Il s'agit ici des protocoles non-connectés comme UDP.Un processus souhaitant communiquer par l'intermédiaire d'une socket du type SOCK_DGRAM doit réaliser les opérations suivantes:4.1) Envoi de messages
Cette fonction retourne le nombre de caractères effectivement envoyés en cas de réussite, dans le cas contraire elle retourne -1. Attention, il est à noter que si l'attachement n'est pas effectué avant le premier envoi utilisant cette socket (client), l'attachement est effectué automatiquement sur un port quelconque de la machine locale.
4.2) RĂ©ception de messages
L'adresse de l'émetteur du message sera récupérée à l'adresse ptr_adresse. La fonction retourne le nombre de caractère effectivement reçus sis réception et -1 sinon.
Il existe un mode pseudo connecté en UDP en utilisant les fonction read() et write().En fait la connexion n'est pas vraiment réalisée, mais l'adresse de destination est mémorisée pour ne pas avoir à le préciser à chaque envois/réception de message. Ceci se fait grâce à la primitive connect().
4.3) Exemple
Nous allons réaliser une application client serveur qui fait l'écho de ce qui lui est transmis. Pour le serveur:Pour le client:
Ce qui donne:
4.4) Le mode broadcast
Ce mode permet d'envoyer des paquets sur un ensemble d'adresses, mais reste limité au segment local. Le client doit modifier les caractéristiques de son socket et signaler le mode SO_BROADCAST par l'intermédiaire de la fonction setsockopt(), l'adresse de diffusion sera INADDR_BROADCAST. Nous allons reprendre l'exemple précédent en réalisant un client echo UDP qui fonctionne en broadcast.4.5) Le mode multicast
Le multicast permet d'envoyer un paquet à plusieurs destinataires sans polluer le reste du réseau. Pour cela, il est nécessaire de s'abonner à la classe IP multicast associée.Pour cela, il y a la structure ip_mreq:
Pour réaliser un programme permettant l'écoute d'un flux multicast, il est nécessaire de spécifier une structure de ce type comme valeur de l'option IP_ADD_MENBERSHIP, du paramètre de niveau IP (IPPROTO_IP). Cette étape est réalisée en utilisant un appel à la fonction setsochopt(), après avoir créé un socket classique attaché localement.
Le fonctionnement client/serveur multicast est inversé par rapport au fonctionnement classique:
A noter que le serveur multicast se réalise en s’attachant à l'adresse IP multicast souhaitée.
Pour le serveur:
Pour le client:
5)La communication connectée (TCP)
C'est le mode de communication TCP dit SOCK_STREAM, le client est plus simple à réaliser que le serveur.Il suffit en général de remplacer le couple sendto/recevfrom par le trio connect/write/read, ce qui marque de manière forte la connexion. Pour le serveur TCP, on remplace recvfrom/sendto par accept/read/write.Ci-dessous le schéma de principe du client TCP. N'oubliez pas que le bind associe l’adresse IP au socket.
5.1)La primitive connect()
Cette fonction prend paramètre le sockfd descripteur de socket existant, serv_addr le pointeur sur une structure contenant l'adresse du serveur et addrlen la longueur de la structure. Elle retourne 0 en cas de réussite et -1 sinon.
5.2)Transmission de donnée write()
Cette fonction prend en paramètre fd le descripteur du socket, buf le pointeur sur une zone de mémoire contenant des données et count la taille des données.
5.3)Réception de donnée read()
Cette fonction prend en paramètre fd le descripteur du socket, buf le pointeur sur une zone de mémoire contenant des données et count la taille des données. Attention, cette fonction est bloquante, elle attend une réponse.
5.4)Exemple de client TCP
6)Le serveur TCP
Le serveur a un rôle passif dans l'établissement d'une connexion, aussi après avoir avisé (appel de la fonction listen()), le serveur se met en attente des requêtes clientes. Le serveur dispose pour cela d'un socket d'écoute attaché au port correspondant au service et connu des clients. Lorsqu'une requête arrive, il créé un nouveau socket dédiée à cette nouvelle connexion, il s'agit du socket de service.Le serveur prend alors connaissance de l'existence d'une nouvelle connexion avec la primitive accept(), et au retour de cet appel, le processus reçoit un descripteur pour accéder au socket de service.
Les connexions acceptées au niveau TCP mais non encore prises en compte par le processus sont dites pendantes. Une fois prise en compte par le serveur (par un appel à la primitive accept), une connexion devient effective et est enlevée de la liste des connexions pendantes.
6.1)La primitive listen()
Cette fonction prend en paramètre un descripteur sockfd et la taille de la file d'attente (nombres de connexions pendantes) backlog.
6.2)La primitive accept()
Cette fonction bloquante prend en paramètre un descripteur socket sockfd, l'adresse de la structure à initialiser et la longueur de la structure.
6.3)Exemple de serveur TCP
Et si on teste avec la version cliente, on obtient un echo du chaîne de caractères.
7)Particularités des systèmes Unix
7.1)Les zombies
Un serveur UDP traite directement les requêtes des clients. Pour le serveur TCP en posture no wait, il va se cloner et traiter les requêtes par ses fils. Il est donc nécessaire de vérifier l'élimination des zombies (clone inutile) et éventuellement réaliser une élimination du zombie par une primitive sigaction ou un signal.7.2)Les démons
Pour créer un démon, il faut le détacher du terminal avec la fonction setsid() qui crée une nouvelle session dont le processus sera leader.Un démon peut être programmé pour lire un fichier de configuration lors de son lancement. Mais il peut être important de l'obliger à relire ce fichier sans l'arrêter.Ceci est réalisé par l'envoi d'un signal SIGHUP et son déroutement dans le code du serveur par le biais de la primitive sigaction. En règle générale, le fichier de configuration se termine par l'extension ".conf".
7.3)Problème du ré-attachement de socket TCP
Il peut arriver que lorsque un serveur TCP décide de redémarrer, le socket ne soit pas disponible parce qu'une connexion existe encore ou que le système ne le libère pas immédiatement le port. Ceci peut être résolu en positionnant le paramètre SO_REUSEADDR du socket à l'aide de la primitive setsockopt().7.4)Journaliser les logs d'un démon
Première méthode: écrire sur la consoleIl suffit d'ouvrir le fichier /dev/console et d'envoyer les messages de trace sur ce fichier en se servant éventuellement de la primitive dup pour rediriger stdout et stderr (les messages ne seront pas enregistrés).
Seconde méthode: écrire dans un fichier spécifique
Troisième méthode: envoyer les messages de trace au démon syslogd
Le démon syslogd est chargé de gérer l'historique du système.Initialement il est nécessaire d'ouvrir une connexion avec syslog.
Avec, ident comme message ajouté au log (généralement le nom du démon). Les options pour openlog (cumulables avec un OU) sont:
Pour envoyer une information Ă syslog:
Il est nécessaire de fermer la connexion:
Donc globalement, cela ressemblera Ă :
8)Socket au format brut (raw)
8.1)Principe
La primitive socket() permet de créer un socket au niveau de la couche transport du modèle ISO, c'est à dire en utilisant SOCK_DGRAM pour UDP ou SOCK_STREAM pour TCP. Mais il est possible de réaliser l'opération au niveau 3 (IP), et donc de forger ses paquets! Pour cela, il vous faudra utiliser le SOCK_RAW.Attention, si vous utilisez ce type de socket, il y a pleins de choses à faire en plus! Par exemple, il faudra préciser le protocole utilisé, aussi il y a un champ supplémentaire pour préciser le protocole :IPPROTO_IP, IPPROTO_ICMP,IPPROTO_UDP,IPPROTO_TCP,...
A noter que pour l'émission d'un paquet, le système construit l'entête IP et laisse à la charge du développeur le soin de programmer la couche du niveau supérieur. Pour la réception, c'est différent, le paquet IP est fournit en intégralité donc avec l'entête.
Attention, ce type de programme nécessite des droits root pour fonctionner.
Il est possible de réaliser des paquets de niveau 2, il faudra utiliser alors la famille de socket PF_PACKET et non plus AF_INET.
8.2)Exemple
Nous allons prendre pour exemple le ping. Pour cela, il sera nécessaire de créer un request ICMP.Ce qui donne:
Et si on lance wireshark on observe les trames echo request et echo reply du ping.
Conclusion
Voila un bon gros chapitre, mais vous avez les bases pour programmer en C et réaliser vos applications réseaux. Bon courage! :-)
© 2024 www.doritique.fr par Robert DORIGNY