Linux/Unix : Programmer un service réseau en C

27 février 2014 rdorigny 0 commentaires

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 .
struct sockaddr_un{ short sun_familly; //domaine Unix: PF_UNIX char sun_path[108]; //référence du fichier }

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.
struct sockaddr_in{ short sin_familly; //domaine Unix: PF_INET ou AF_INET unsigned short sin_port; //le numéro de port struct in_addr sin_addr; //adresse IP char sin_zero[8]; //champ de huit zero }; struct in_addr{ unsigned long s_addr; };

Et pour l'IPV6:
struct sockaddr_in6{ u_int16_t sin6_familly; //AF_INET6 u_int16_t sin6_port; //numéro de port u_int32_t sin6_flowinfo; struct in6_addr sin6_addr; //adresse IPV6 u_int32_t sin6_scope_id; }; struct in6_addr{ unsigned char s6_addr[16]; };

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

#include <sys/types.h> #include <sys/socket.h> int socket(int domaine,int type,int protocole);

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.
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

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.
int socketpair(int dom,int typ,int prot,int *ptr_dec);

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.

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> int main(void) { char *sock_name="/tmp/reference"; int sd; struct sockaddr_un socket_listen; sd=socket(PF_UNIX,SOCK_STREAM,0); //Pour ĂŞtre certain que le fichier n'existe pas unlink(sock_name); socket_listen.sun_family=PF_UNIX; strcpy(socket_listen.sun_path,sock_name); bind(sd,(struct sockaddr *)&socket_listen,sizeof(struct sockaddr_un)); system("netstat --unix|grep "reference" "); close(sd); exit(0); }

Voyons comment faire pour le domaine Internet:
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.

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main() { int desc; struct sockaddr_in adresse; int longueur=sizeof(struct sockaddr_in); desc=socket(PF_INET,SOCK_STREAM,0); adresse.sin_family=PF_INET; adresse.sin_addr.s_addr=INADDR_ANY; adresse.sin_port=htons(61234); bind(desc,(struct sockaddr *)&adresse,longueur); printf("RSTREAM:: assigned port number %dn", ntohs(adresse.sin_port)); //system("netstat -an"); exit(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:
  • htons():entier court local vers entier court rĂ©seau,
  • htonl():entier long local vers entier long rĂ©seau,
  • ntohs():entier court rĂ©seau vers entier court local,
  • ntohl():entier long rĂ©seau vers entier court local.
  • 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.

    #include <sys/socket.h> int getsockname(int s, struct sockaddr *name,socklen_t *namelen);

    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.

    #include <arpa/inet.h> //transforme un entier long en chaîne de caractères const char *inet_ntop(int af,const void *src,char *dst,socklen_t cnt); //Opération inverse, transforme l'adresse IP en entier long int inet_pton(int af,const char * src,void *dst);

    Les arguments :
  • af: famille d'adresse (AF_INET ou AF_INET6 pour l'IPV6),
  • src: un pointeur sur la structure d'adresse,
  • dst: une chaĂ®ne de caractères pouvant accueillir le rĂ©sultat,
  • cnt: la taille du buffer prĂ©cĂ©dent.

  • La fonction inet_ntop() retourne un pointeur sur la chaĂ®ne de caractère.

    Voici un exemple d'utilisation de ces fonctions:
    #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main() { int s; char buffer[1024]; struct in_addr adresse; adresse.s_addr=htonl(INADDR_LOOPBACK); if (inet_ntop(AF_INET,&adresse,buffer,1024)==NULL) { perror("inet..."); exit(1); } printf("inet_top : %sn",buffer); exit(0); }

    3.2)RĂ©solution du nom par le DNS

    #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *node, const char *service,const struct addrinfo *hints,struct addrinfo **res);

    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 :
    struct addrinfo{ int ai_flags; int ai_familly; int ai_socktype; int ai_protocol; size_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; }

    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():
  • ai_flags : Options supplĂ©mentaires, 0 pour aucune,
  • ai_family : AF_INET (IP V4), AF_INET6 (IP V6) ou AF_UNSPEC (les deux piles),
  • ai_socktype : SOCK_STREAM (TCP), SOCK_DGRAM (UDP) et 0 pour tous,
  • ai_protocol : 0 pour laisser celui le plus adaptĂ©.

  • Les autres champs:
  • ai_addrlen : longueur de la structure d'adresse ai_addr,
  • ai_addr : pointeur sur la structure d'adresse,
  • ai_canonname : nom de l'hĂ´te si demandĂ© par les options,
  • ai_next : pointeur sur l'enregistrement suivant.

  • La variable ai_flags peut prendre une combinaison de valeurs suivantes:
  • AI_NUMERICHOST : ne pas effectuer de rĂ©solution, node sera une adresse IP,
  • AI_PASSIVE : retourne les adresses locales si node est NULL,
  • AI_NUMERICSERV : ne cherche pas Ă  rĂ©soudre le service, le champ service doit alors contenir un chiffre correspondant au numĂ©ro de port et non le nom du service.

  • Exemple de code pour rĂ©aliser une rĂ©solution DNS:
    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main() { int s; struct sockaddr_in * ptr_IP4; struct addrinfo hints; struct addrinfo *result, *rp; char buffer[1024]; memset(&hints,0,sizeof(struct addrinfo)); hints.ai_family=AF_UNSPEC; hints.ai_socktype=SOCK_STREAM; s=getaddrinfo("www.google.fr",NULL,&hints,&result); if (s==-1) { perror("Erreur de getaddrinfo..."); exit(1); } rp=result; while (rp!=NULL){ if (rp->ai_family==AF_INET){ ptr_IP4=((struct sock_in *) rp->ai_addr); if (inet_ntop(AF_INET,&(ptr_IP4->sin_addr),buffer,1024)==NULL){ perror("Erreur de getaddrinfo..."); exit(1); } printf("%sn",buffer); } rp=rp->ai_next; } exit(0); }

    3.3)Options de socket

    Il est possible de consulter et de modifier les options de sockets créés:
    #include <sys/types.h> #include <sys/socket.h> //retourne 0 en cas de réussite et -1 sinon. int getsockopt(int s,int level,int optname,void *optval,socklent_t *optlen); int setsockopt(int s,int level,int optname,cons void *optval,socklent_t *optlen);

    Les paramètres de ces deux fonctions:
  • s : le descripteur de socket,
  • level : le niveau, SOLSOCKET ou le numĂ©ro du protocole (IPPROTO_IP),
  • optname : le nom de l'option,
  • optval : la valeur de l'option,
  • oplen : la taille de la donnĂ©e associĂ©e Ă  l'option.
  • 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:
  • 1) demander la crĂ©ation d'un socket,
  • 2) demander Ă©ventuellement l'attachement du socket sur le port convenu ou un port quelconque selon qu'il joue le serveur ou le client,
  • 3) construire l'adresse de son interlocuteur en mĂ©moire : tout client dĂ©sirant s’adresser Ă  un serveur doit en connaitre l'adresse,
  • 4) procĂ©der Ă  des Ă©missions et des rĂ©ceptions de messages.
  • 4.1) Envoi de messages

    int sendto( int descripteur //renvoyé par la primitive socket void *message, //message à envoyer int longueur, //longueur du message int option, //0 struct sockaddr *ptr_adresse, //adresse de dest. int longueur_adresse //longueur de l'adresse dest

    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

    int recvfrom( int descripteur //renvoyé par la primitive socket void *message, //adresse du buffer de réception int longueur, //taille du buffer de réception int option, //0 struct sockaddr *ptr_adresse, //adresse de l'émetteur int *ptr_longueur_adresse //pointeur sur la longueur de l'adresse

    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:
    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(){ int desc,taille,longueur; char chaine[1024]; struct sockaddr_in adr1,adr2; if ((desc=socket(AF_INET, SOCK_DGRAM,0))==-1){ perror("PB de socket."); exit(1); } adr1.sin_family=AF_INET; adr1.sin_addr.s_addr=htonl(INADDR_ANY); adr1.sin_port=htons(3570); longueur=sizeof(struct sockaddr_in); if (bind(desc,(struct sockaddr *)&adr1,longueur)==-1){ perror("PB avec le bind."); exit(1); } do{ //Attend un paquet taille=recvfrom(desc, chaine,1024,0,(struct sockaddr *)&adr2,&longueur); //Affiche le paquet printf("Packet reçu : %sn",chaine); //Retourne le paquet sendto(desc,chaine,taille,0,(struct sockaddr *)&adr2,longueur); } while (taille>2); //tant que la chaine reçue n'est pas vide exit(0); }

    Pour le client:
    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(){ int desc,taille,longueur=sizeof(struct sockaddr_in); char chaine[1024],chaine2[1024]; struct sockaddr_in adr1,adr2,adr3; if ((desc=socket(AF_INET, SOCK_DGRAM,0))==-1){ perror("PB de socket."); exit(1); } adr1.sin_family=AF_INET; adr1.sin_addr.s_addr=htonl(INADDR_ANY); adr1.sin_port=htons(0); if (bind(desc,(struct sockaddr *)&adr1,longueur)==-1){ perror("PB avec le bind."); exit(1); } adr2.sin_family=AF_INET; adr2.sin_addr.s_addr=htonl(INADDR_LOOPBACK); adr2.sin_port=htons(3570); do{ printf("Message à envoyer : "); fgets(chaine,1024,stdin); sendto(desc,chaine,strlen(chaine)+1,0,(struct sockaddr *)&adr2,longueur); taille=recvfrom(desc, chaine2,1024,0,(struct sockaddr *)&adr3,&longueur); printf("Message reçu : %sn",chaine2); } while (taille>2); //tant que la chaine reçue n'est pas vide exit(0); }

    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.

    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(){ int desc,taille,longueur=sizeof(struct sockaddr_in); char chaine[1024],chaine2[1024]; struct sockaddr_in adr1,adr2,adr3; int bcast=1; if ((desc=socket(AF_INET, SOCK_DGRAM,0))==-1){ perror("PB de socket."); exit(1); } //Le bind est bind est automatique en broadcast puisque les paquets sont pour tout le monde adr2.sin_family=AF_INET; adr2.sin_addr.s_addr=htonl(INADDR_BROADCAST); adr2.sin_port=htons(3570); if (setsockopt(desc,SOL_SOCKET,SO_BROADCAST,&bcast,sizeof(bcast))<0){ perror("Erreur de setsockopt"); } do{ printf("Message à envoyer : "); fgets(chaine,1024,stdin); if (sendto(desc,chaine,strlen(chaine)+1,0,(struct sockaddr *)&adr2,longueur)==-1){ perror("PB avec sendto"); exit(1); } taille=recvfrom(desc, chaine2,1024,0,(struct sockaddr *)&adr3,&longueur); printf("Message reçu : %sn",chaine2); } while (taille>2); //tant que la chaine reçue n'est pas vide exit(0); }

    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:
    struct ip_mreq{ //adresse IP du groupe multicast struct in_addr imr_multiaddr; //adresse IP locale de l'interface struct in_addr imr_interface; };

    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:
  • Le serveur va envoyer des donnĂ©es sans attendre de requĂŞtes,
  • Le client lui,se met en Ă©coute.

  • A noter que le serveur multicast se rĂ©alise en s’attachant Ă  l'adresse IP multicast souhaitĂ©e.

    Pour le serveur:
    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(){ int desc,taille,longueur=sizeof(struct sockaddr_in); char chaine[1024]; struct sockaddr_in adr1,adr2; if ((desc=socket(AF_INET, SOCK_DGRAM,0))==-1){ perror("PB de socket."); exit(1); } adr2.sin_family=AF_INET; inet_pton(AF_INET,"224.0.0.10",&adr2.sin_addr.s_addr); adr2.sin_port=htons(2013); do{ printf("Message à envoyer : "); fgets(chaine,1024,stdin); if (sendto(desc,chaine,strlen(chaine)+1,0,(struct sockaddr *)&adr2,longueur)==-1){ perror("PB avec sendto."); exit(1); } } while (1); //tant que la chaine reçue n'est pas vide exit(0); }

    Pour le client:
    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(){ int desc,taille,longueur=sizeof(struct sockaddr_in); char chaine[1024]; struct sockaddr_in adr1,adr2; struct ip_mreq mreq; if ((desc=socket(AF_INET, SOCK_DGRAM,0))==-1){ perror("PB de socket."); exit(1); } adr1.sin_family=AF_INET; adr1.sin_addr.s_addr=htonl(INADDR_ANY); adr1.sin_port=htons(2013); if (bind(desc,(struct sockaddr *)&adr1,longueur)==-1){ perror("PB avec le bind."); exit(1); } //Joindre le groupe multicast inet_pton(AF_INET,"224.0.0.10",&mreq.imr_multiaddr.s_addr); mreq.imr_interface.s_addr=htonl(INADDR_ANY); if (setsockopt(desc,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq))==-1){ perror("PB avec setsockopt."); exit(1); } do{ taille=recvfrom(desc, chaine,1024,0,(struct sockaddr *)&adr2,&longueur); printf("Message reçu : %sn",chaine); } while (taille>2); //tant que la chaine reçue n'est pas vide exit(0); }

    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()

    #include <sys/types.h> #include <sys/socket.h> int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen);

    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()

    #include <unistd.h> ssize_t write(int fd,const void *buf,size_t count);

    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()

    #include <unistd.h> ssize_t read(int fd,const void *buf,size_t count);

    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

    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(){ int desc,desc2,taille,longueur=sizeof(struct sockaddr_in); char chaine[1024],chaine2[1024]; struct sockaddr_in adr1,adr2,adr3; //Phase 1 if ((desc=socket(AF_INET, SOCK_STREAM,0))==-1){ perror("PB de socket."); exit(1); } adr1.sin_family=AF_INET; adr1.sin_addr.s_addr=htonl(INADDR_ANY); adr1.sin_port=htons(0); //Phase 2 if (bind(desc,(struct sockaddr *)&adr1,longueur)==-1){ perror("PB avec le bind."); exit(1); } //Phase 3 adr2.sin_family=AF_INET; adr2.sin_addr.s_addr=htonl(INADDR_LOOPBACK); adr2.sin_port=htons(19200); printf("Message à envoyer : "); fgets(chaine,1024,stdin); if (connect(desc,(struct sockaddr *)&adr2,longueur)==-1){ perror("PB avec connect()"); exit(1); } write(desc,chaine,strlen(chaine)+1); taille=read(desc,chaine2,1024); printf("Message reçu : %sn",chaine2); close(desc); exit(0); }

    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()

    #include <sys/types.h> #include <sys/socket.h> int listen(int sockfd,int backlog);

    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()

    #include <sys/types.h> #include <sys/socket.h> int accept(int sockfd,struct sockaddr *adresse,socklen_t *longueur);

    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

    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(){ int desc,desc2,taille; int longueur=sizeof(struct sockaddr_in); char chaine[1024]; struct sockaddr_in adr1,adr2; if ((desc=socket(AF_INET, SOCK_STREAM,0))==-1){ perror("PB de socket."); exit(1); } adr1.sin_family=AF_INET; adr1.sin_addr.s_addr=htonl(INADDR_ANY); adr1.sin_port=htons(19200); if (bind(desc,(struct sockaddr *)&adr1,longueur)==-1){ perror("PB avec le bind."); exit(1); } if (listen(desc,10)==-1){ perror("PB avec listen()."); exit(1); } do{ if ((desc2=accept(desc,(struct sockaddr *)&adr2,&longueur))==-1){ perror("PB avec accept."); exit(1); } taille=read(desc2,chaine,1024); printf("Paquet reçu : %sn",chaine); write(desc2,chaine,taille); close(desc2); } while (taille>2); //tant que la chaine reçue n'est pas vide exit(0); }

    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.
    if (fork()!=0) //Clone et test si c le processus père exit(0); // si c'est le processus père, on le kill setsid(); //ici c'est forcément le fils, on le détache de la console et il devient démon.

    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().
    int valeur=1; if(setsockopt(desc,SOL_SOCKET,SO_REUSEADDR,&valeur,sizeof(valeur))<0){ perror("PB avec setsockopt."); exit(1); }

    7.4)Journaliser les logs d'un démon

    Première méthode: écrire sur la console
    Il 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
    FILE *f; f=fopen("trace.log","w"); fprintf(f,"%s",data);


    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.
    #include <syslog.h> void openlog(char *ident,int option, int facility);


    Avec, ident comme message ajouté au log (généralement le nom du démon). Les options pour openlog (cumulables avec un OU) sont:
  • LOG_CONS: Ă©crit directement sur la console système,
  • LOG_NDELAY: ouvre la connexion immĂ©diatement (normalement la connexion n'est ouverte qu'au moment de l'Ă©criture du message),
  • LOG_PERROR: envoie le message sur stderr,
  • LOG_PID: insère le numĂ©ro du PID avec chaque message.
  • facility reprĂ©sente le niveau d'importance du message envoyĂ© au syslog.

    Pour envoyer une information Ă  syslog:
    #include <syslog.h> void syslog(int priority,char *format,...);

    Il est nécessaire de fermer la connexion:
    #include <syslog.h> void closelog(void);

    Donc globalement, cela ressemblera Ă :
    openlog("Mon programme",LOG_PID,LOG_DAEMON); syslog(LOG_EMERG,"Mon message"); syslog(LOG_WARNING,"Mon message %d",i); closelog();

    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.
    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> #include <string.h> int main(int argn,char * argv[]){ struct sockaddr_in dest,source; int i,recu,s; u_char packetIN[128]; u_char packetOUT[64]; u_char cksum; int sourcelong=sizeof(source); int packetOUTlong=sizeof(packetOUT); int packetINlong=sizeof(packetIN); if(argn!=2){ printf("Syntaxe: %s <IP DESTINATION>n",argv[0]); exit(1); } //Remplissage de la structure dest.sin_family=AF_INET; inet_pton(AF_INET,argv[1],&dest.sin_addr.s_addr); //Ouverture du socket if ((s=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP))<0){ perror("PB avec socket."); exit(1); } printf("Envoi du paquet ICMP request sur %sn",argv[1]); //nettoyage du paquet memset(packetOUT,0,packetOUTlong); //DĂ©finition du paquet ICMP packetOUT[0]=(u_char)8; //Type ICMP 8,demande d'echo packetOUT[1]=(u_char)0; //code 0 //Checksum ~(8+0+0)=0xf7ff packetOUT[2]=(u_char)0xf7; //partie haute packetOUT[3]=(u_char)0xff; //partie basse //boucle infinie for(;;){ sleep(1); //envoi paquet i=sendto(s,(char *)packetOUT,packetOUTlong,0,(struct sockaddr *) &dest, sizeof(struct sockaddr)); recu=recvfrom(s,(char *)packetIN,packetINlong,0,(struct sockaddr *)&source, (socklen_t *)&sourcelong); if (recu==-1){ perror("PB avec recvfrom."); exit(1); } else { for (i=0;i<28;i++){ printf("%d",packetIN[i]); if ((i%4)==3) printf("n"); } } } exit(0); }

    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! :-)







    Pseudonyme (obligatoire) :
    Adresse mail (obligatoire) :
    Site web :




    © 2024 www.doritique.fr par Robert DORIGNY