Les sockets UNIX Sylvie Dupuy sdupuy@ccr.jussieu.fr année 2000
Protocoles de l’Internet : rappels Interface du service de transport APPLICATION APPLICATION send Num. port UDP ou TCP receive PHY 1 PHY 2 ARP MAC 1 MAC 2 Data Link 1 Data Link 2 ICMP IP TCP UDP Endsystem sur réseau 1 Endsystem sur réseau 2 Routeur IP Numéro de port = identifie l’application à laquelle le paquet IP est destiné
Notion de socket (1) Permet d’établir un canal de communication bidirectionnel entre deux applications (plus exactement entre deux TSAPs) Socket UDP : paquets de taille variable, transport non fiable (pertes, duplications, erreurs possibles) Socket TCP : paquets de taille variable délivrés dans l’ordre et sans erreurs => Choisir le protocole en fonction des besoins de l’application : données informatiques (FTP, Telnet) : TCP, video : UDP
Notion de socket (2) Sous Unix les applications sont dénommées processus Les processus actifs sont répertoriées dans une table du système et identifiés par un pid (cf. commande ps) Lorsqu’il est actif, un processus possède un contexte d’exécution => variables d’environnement, table de descripteurs de fichiers ouverts …
Notion de socket (3) Mécanisme de communication entre applications sous Unix : - pipe (ou tube nommé) -> fichier - mémoire partagée (system V) - file de messages - sockets Lorsqu’un objet de ce type est créé par une application, le système lui alloue un identifiant. L’identifiant d’un socket est un descripteur analogue aux descripteurs de fichiers.
Fork et héritage Unix est un système multitâches : => un processus en cours d’exécution peut se dupliquer dynamiquement : fork() => après un fork(), deux applications : le processus père et le processus fils s’exécutent simultanément (en temps partagé ou bien en parallèle sur un système multiprocesseurs) => le processus fils hérite du contexte d’exécution du processus père
Terminaison de processus Le processus père attend habituellement la terminaison du processus fils : => wait(), attente passive : reçoit le signal SIGCLD (cf. table des signaux /usr/include/signal.h) Lorsque le processus père se termine sans avoir fait un wait (), le processus fils est rattaché au processus de PID 1 (init)
Recouvrement Le code du processus fils peut être dynamiquement remplacé par celui d’un exécutable stocké sur disque : => exec () Possibilité de passage de paramètres (ligne de commande) et des variables d’environnement Après un exec() le processus conserve toutes ses caractéristiques : PID, priorité, propriétaire et groupe réels, catalogue courant, terminal associé, masque de création de fichiers, descripteurs de fichiers ouverts, signaux ignorés (les autres signaux reprennent leur comportement par défaut)
Redirection des entrées/sorties Les descripteurs de fichiers 0,1 et 2 correspondent respectivement à : - l’entrée standard (clavier), - la sortie standard (écran), - la sortie erreur standard (écran) Ces fichiers spéciaux (mode caractère) sont ouverts par défaut lors de la création du processus La primitive système dup() permet de dupliquer un descripteur (ie. Toutes les lectures/écritures effectuées sur l’un des fichiers le sont aussi sur l’autre) => par ce mécanime, les fichiers standards peuvent être redirigés
Modèle de communication Un processus (le serveur) est en attente (à l’écoute) sur un numéro de port => tous les processus (les clients) connaissant l’adresse IP du serveur et le numéro de port peuvent lui envoyer des messages (requêtes) Le serveur traîte les messages entrants puis renvoie une réponse au client en utilisant le numéro de port source contenu dans l’en-tête du message En mode connecté (TCP) il y a établissement préalable d’une connexion
Programmation des sockets Les sockets sont identifiés dans le système par des descripteurs de même nature que les descripteurs de fichiers => les fichiers d’entrées/sortie standard (desc. 0,1 et 2) peuvent être redirigés sur des sockets => il y a héritage des descripteurs de sockets après un fork() Le descripteur d’un socket est renvoyé par la primitive socket(). Le processus peut alors effectuer des opérations de lecture/écriture car la communication est bidirectionnelle (!= tube) Les frontières de messages sont conservées
Domaine d’un socket(1) Domaine UNIX : AF_UNIX <sys/un.h> Les sockets sont locales au système et fournissent un mécanisme d’IPC (InterProcessus Communication) struct sockaddr_un { short sun_family; /* AF_UNIX */ char sun_path [108] /* référence UNIX */ };
Domaine d’un socket(2) Domaine internet : AF_INET struct in_addr { u_long s_addr; }; struct sockaddr_in { short sin_family; /* AF_INET */ u_short sin_port; /* numéro de port */ struct sin_addr; /* adresse internet */ char sin_zero[8]; /* champ de 8 zeros] */
Types de sockets SOCK_DGRAM : transport de données en mode non connecté. Dans le domaine AF_INET, le protocole UDP est utilisé. Les frontières de messages sont préservées. SOCK_STREAM : transport de données en mode connecté. Dans le domaine AF_INET, utilisation de TCP. Frontières de messages préservées et possibilité d’envoi de messages urgents. SOCK_RAW : permet l’accès au service de niveau 3 (émission/réception de paquets IP) SOCK_SEQPACKET : comme le type SOCK_STREAM mais ne permettant pas l’envoi de messages urgents
Création d’un socket int socket (domaine, type, protocole) int domaine; /* AF_INET, … */ int type; /* SOCK_DGRAM, … */ int protocole; /* 0 : protocole par défaut attribué par le système*/ Structure de données associée à un socket : type, domaine et protocole du socket état (en état de recevoir ou d’émettre) adresse des buffers de réception et d’émission pointeurs sur les données en émission/réception groupe de processus pour la gestion des signaux SIGURG, SIGIO
Binding : association socket/adresse int bind (sock, p_adresse, lg) int sock; /* descripteur de socket */ struct sockaddr *p_addresse; /* pointeur sur l’adresse */ int lg; /* longueur de l’adresse */ Après sa création un socket est accessible par son descripteur. Seuls des processus fils peuvent en hériter. Bind fournit un procédé de nommage d’un socket permettant à d’autres processus d’y accèder. Cas d’erreurs : descripteur incorrect (EBADF,ENOTSOCK), adresse incorrecte, inaccessible ou déjà utilisée (EADDRNOTAVAIL, EADDRINUSE, EACCESS), socket déjà associé à une adresse (EINVAL).
Binding dans le domaine AF_INET(1) Dans le domaine internet, le champ sockaddr est une structure sockaddr_in => choisir une adresse internet quelconque (INADDR_ANY) ou spécifier l’adresse d’une interface IP (gethostname() et gethostbyname()) => choisir le numéro de port (getservbyname() dans le cas d’un service existant) ou bien fixer un numéro non réservé (>= IPPORT_RESERVED) ou bien laisser le système choisir un numéro de port (initialiser le champ sin_port à zéro)
Binding dans le domaine AF_INET(2) Pour accéder à l’adresse associée à un socket en connaissant seulement son descripteur : int getsockname (sock, p_adr, p_lg) int sock; /* descripteur du socket */ struct sockaddr_in *p_addr; /* pointeur sur l’adresse */ int *p_lg; /* pointeur sur la longueur de l’adresse */ A l’appel p_lg = sizeof(struct sockaddr_in), au retour p_lg = longueur effective de l’adresse
Emission de datagrammes(1) int sendto(sock, ms, lg, option, p_dest, lgdest) int sock; /* descripteur du socket */ char *msg; /* adresse du message à envoyer */ int lg; /* longueur du message */ int option; /* =0 pour le type SOCK_DGRAM */ struct sockaddr *p_dest; /* pointeur sur adresse socket dest */ int lgdest; /* longueur adresse socket dest */ => retour = nombre de caractères envoyés ou –1 en cas d’échec (descripteur ou adresse du socket de destination invalide, taille de message ne permettant pas l’envoi d’un seul datagramme)
Emission de datagrammes(2) Pour envoyer une série de messages : int sendmsg (sock, msg, option) int sock; /* descripteur du socket en émission */ struct msghdr msg[]; /* tableau des en-têtes de messages */ int option; => cette primitive d’envoi peut être utilisée sans appel préalable à la primitive bind(). Le système associe le socket à une adresse lors de l’envoi du premier message.
Emission de datagrammes(3) Structure des en-têtes de messages à émettre <sys/socket.h> struct msghdr { caddr_t msg_name; /* adresse optionnelle */ int msg_namelen; /* taille de l’adresse */ struct iovec *msg_iov; /* tableau de messages */ int msg_iovlen; /* nombre d’élements du tableau */ caddr_t msg_accrights; /* inutilisé pour les sockets */ int msg_accrightslen; /* inutilisé pour les sockets */ }; Structure iovec <sys/uio.h> struct iovec { caddr_t iov_base; /* adresse du message */ int iov_len; /* longueur du message */ };
Réception de datagrammes (1) int recvfrom ( sock, msg, option, p_exp, p_lgexp) int sock; /* descripteur du socket */ char *msg; /* adresse mémoire du message reçu */ int lg; /* taille du buffer contenant le message */ int option; /* 0 ou MSG_PEEK*/ struct sockaddr *p_exp; /* adresse de l’expediteur */ int *p_lgexp; /* sizeof(p_exp) et longueur du résultat */ Le paramètre lg doit correspondre à la taille du message reçu, sinon des caractères sont perdus (il faut connaître la taille du message envoyé) Retourne le nombre de caractères reçus ou –1 en cas d’erreur
Réception de datagrammes (2) L’appel recvfrom est bloquant. Pour éviter l’attente de réception d’un message, utiliser fcntl ( sock, F_SETFL,FNDELAY|fcntl (sock, F_GETFL,0)); Pour répondre, le processus extrait l’adresse et le numéro de port de la structure sockaddr (sockaddr_in dans le domaine AF_INET); L’option MSG_PEEK permet de consulter le message sans l’extraire Pour recevoir une série de messages : int recvmsg ( sock, msg, option) int sock; /* descripteur du socket de réception */ struct msghdr [ ]; /* tableau des en-têtes reçus */ int option;
Communication client/serveur(1) Sur le serveur : - créér un socket de type SOCK_DGRAM (donner un numéro de port) - détacher le serveur du terminal de lancement (pas d’interruptions clavier possibles) - boucler en attente des messages : les lire par recvfrom( ), y répondre par sendto( ) Sur le client : - récupérer l’adresse du serveur (gethostbyname( )) - créer un socket, initialiser les champs (numéro de port, adresse @IPdest) - envoyer une requête par sendto ( ), lire la réponse par recvfrom ( )
Communication client/serveur(2) Les fonctions suivantes sont utilisées pour la programmation des sockets : - bzero : initialiser à zéro les champs d’une structure sockaddr - bcopy : initialiser le champ adresse (sin_addr) d’une structure sockaddr - htons : initialiser le champ port de la structure (little != big endian) Pour éviter l’attente active du serveur (while(1)), le service associé au numéro de port choisi pour le serveur peut être enregistré dans le fichier /etc/services. En modifiant le fichier /etc/inetd.conf, le processus daemon inetd prendra en charge la création d’un processus serveur sur réception d’un message de requête contenant le numéro de port du service (attente active, select, fork()). Inetd relit le fichier inetd.conf sur réception d’un signal SIGHUP.
Communication en mode connecté(1) L’information est échangée après établissement d’une connexion. Contrairement à l’émission de datagrammes, les messages sont reçus en mode flot (pas de notion de frontière de message) Modèle de communication : - le serveur est en attente d’une demande de connexion sur un numéro de port TCP correspondant à un service via un socket. - lorsqu’une requête arrive le serveur reçoit un signal, il crée un nouveau socket puis se duplique (fork()). - le processus fils prend en charge l’échange de données avec le client (exec()), tandis que le processus père retourne attendre de nouvelles demandes de connexion sur le premier socket.
Communication en mode connecté(2) Primitives associèes au différentes étapes de la communication : Création et attachement du socket Socket/bind Ouverture du service listen Attente de la demande de connexion accept Création d’un processus fils fork Traitement de la demande exec processus père processus fils
Listen( ) Après avoir créé un socket de type SOCK_STREAM, le serveur indique au système qu’il est à l’écoute sur un port TCP socket (AF_INET, SOCK_STREAM, 0); int listen ( sock, nb ) int sock; /* descripteur du socket d’écoute */ int nb; /* nombre maxi de connexions */ => si le champ sin_port de la structure sockaddr_in n’a pas été initialisé, le numéro de port est attribué par le système. Le processus utilisera getsockname() pour le connaître
Accept() Accept() permet d’extraire une demande de connexion parmi l’ensemble des requêtes en attente (FIFO) La primitive crée un nouveau socket qui est associée à un nouveau numéro de port choisi par le système et retourne son descripteur Au retour de l’appel, le champ p_adr->sin_port contient le numéro de port du socket du client ayant demandé l’ouverture d’une connexion int accept ( sock, p_adr, p_lgadr ) int sock; /* descripteur du socket */ struct sockaddr *p_adr; /* adresse du socket */ int *p_lgadr; /* pointeur sur la longueur de l’adresse */ => s’il n’y a pas de requête en attente l’appel est bloquant sauf si le socket est en mode non bloquant (cf. fcntl). En ce cas, la valeur de retour vaut –1 et errno = EWOULDBLOCK
Communication en mode connecté : côté client Etapes d’une demande d’établissement de connexion : Création et attachement du socket Socket/bind Construction de l’adresse du serveur Demande de connexion connect Echec Dialogue avec le serveur
Connect( ) La primitive connect() réalise l’établissement d’une connexion bidirectionnelle de bout-en-bout entre le client et le serveur en associant socket serveur et socket client. int connect ( sock, p_adr, lgadr ) int sock; /* descripteur socket local */ struct sockaddr *p_adr; /* adresse socket distant */ int lgadr; /* longueur adresse distante */ => socket local et distant doivent être de même type et appartenir au même domaine. => le serveur doit avoir effectué un listen() et ne pas avoir atteint la file de demandes de connexion en attente ne doit pas être pleine.
Connect( ) : délai d’établissement d’une connexion Lorsque la file de demandes de connexions du serveur est pleine, le processus client est bloqué jusqu’à expiration du délai d’établissement d’une connexion. Le code de retour est alors –1 et errno = ETIMEDOUT En mode non bloquant, le retour est immédiat mais la tentative d’établissement de connexion est maintenue : - le client peut s’enquérir de l’établissement de la connexion avec select( ) en passant le descripteur du socket parmi les paramètres représentant les descripteurs testés en écriture. - s’il exécute à nouveau un connect avant expiration du délai d’établissement, le retour est immédiat et errno = EALREADY
Emission/Réception de messages TCP(1) Les primitives standard de lecture/écriture de fichiers permettent de lire/écrire dans les sockets, donc d’envoyer des messages TCP int write ( sock, msg, lg ) int sock; /* descripteur du socket local */ char *msg; /* adresse du message */ int lg; /* longueur du message */ int read ( sock, msg, lg ) char *msg; /* adresse de sauvegarde du message */ int lg; /* longueur de la zône */
Emission de messages TCP(2) La primitive send permet l’envoi de données urgentes (hors bande) int send (sock, msg, option) int sock; /* descripteur du socket local */ char *msg; /* adresse du message à envoyer */ int lg; /* longueur du message */ int option /* 0 ou MSG_OOB */ => retourne le nombre de caractères émis la lecture est bloquante si le buffer en émission (socket local) ou le buffer en réception (socket distant) est plein fcntl( ) permet les lectures non bloquantes
Réception de messages TCP(3) La primitive recv permet d’extraire des données urgentes et de lire des données sans les extraire du buffer de réception du socket int recv ( sock, msg, lg, option ) int sock; /* descripteur du socket local */ char *msg; /* adresse de sauvegarde du message */ int lg; /* longueur de la zone */ int option; /* 0 ou MSG_PEEK ou MSG_OOB */ => La lecture est par défaut bloquante. Le paramètre lg spécifie un nombre maximal de caractères à lire.
Terminaison d’une connexion Au lieu d’utiliser la primitive close sur le descripteur du fichier comme pour les sockets UDP, il faut utiliser la primitive shutdown qui assure le transfert des messages en attente d’émission dans les buffers locaux int shutdown ( desc, sens ) int desc; /* descripteur de socket */ int sens; /* 0, 1 ou 2 */ 0 indique que le processus ne veut plus recevoir, 1 qu’il ne veut plus émettre et 2 ni recevoir, ni émettre si un processus tente d’écrire sur un socket qui a été fermé en reception sur le site distant, il reçoit le signal SIGPIPE
Signaux SIGIO/SIGURG Le signal SIGIO peut être envoyé par le système à un ou plusieurs processus lorsqu’un message a été reçu sur un socket : - définir un handler du signal SIGIO (ie. fonction de traitement du signal) - demander l’envoi du signal SIGIO à l’arrivée d’un caractère (fcntl(), FASYNC) - attribuer un groupe propriétaire au socket (fcntl(sock), F_SETOWN, getpid()) De même le signal SIGURG peut être envoyé à un groupe de processus lors de la réception d’un caractère urgent (utiliser recv et l’option MSG_OOB); celui-ci est repéré dans le flot de données par une marque
Paramétrage des sockets (1) La primitive getsockopt fournit des informations sur le paramétrage int getsockopt ( desc, niveau, option, o_arg, lg) int desc; /* descripteur de la socket */ int niveau; /* niveau de l’option */ int option; /* option à appliquer */ char *p_arg; /* pointeur pour récupérer la valeur */ int *lg; /* à l’appel : lg réservée, au retour lg du résultat */ => Quand les options sont définies au niveau de l’interface des sockets, la valeur du paramètre option est SOL_SOCKET (pour que la prise en compte des options ait lieu a un autre niveau, utiliser getprotobyname())
Paramétrage des sockets (2) Modification de la valeur d’une option : int setsockopt (desc, niveau, option, p_arg,lg) int desc; /* descripteur du socket */ int niveau; /* niveau de l’option */ int option; /* option à appliquer */ char *p_arg; /* pointeur sur l’argument de l’option */ int lg; /* longueur de l’argument */
Paramétrage des sockets (3) Valeurs possibles de certaines options <sys/socket.h> SO_TYPE type de la socket (getsockopt seulement) SO_BROADCAST si *p_adr=1 diffusion (SOCK_DGRAM seulement) SO_SNDBUF *p_adr = taille du buffer d’émission SO_RCVBUF *p_adr = taille du buffer de réception
Source « La communication sous UNIX » Jean-Marie RIFFLET, édition Mc Graw Hill