4.C - Programmation Socket

Nous allons voir dans cette partie comment fonctionnent les sockets.

Les sockets sont la couche de programmation des réseaux qui permet aux applications d'Internet d'envoyer et de recevoir des données entre un client et un serveur. Nous allons illustrer cela avec du code en Python, mais cela fonctionne de la même manière avec d'autres langages de programmation (C, C++, Java, etc.).

Quelques Généralités

Mode non connecté vs mode connecté

Pour rappel, il y a deux types de communications dans le domaine des réseaux : celles qui sont en mode non connecté et celles qui sont en mode connecté. On va retrouver cette notion dans les sockets et leur programmation, puisqu'à l'ouverture de la socket, on va choisir un mode de communication qui est soit en utilisant TCP, auquel cas on sera dans le mode connecté et on pourra disposer de la fiabilité des transferts, c'est-à-dire faire en sorte que tout ce qui arrive à l'autre extrémité de la connexion corresponde exactement à ce qui avait été émis. L'autre possibilité est de choisir le mode non connecté, auquel cas, c'est le protocole UDP qui sera choisi.

Le mode non connecté, c'est donc le fait d'envoyer une requête aussitôt, sans attendre et sans demander au serveur s'il est d'accord pour recevoir cette requête. Le serveur, de son côté, quand il reçoit la requête, la traite et y répond de manière positive ou négative. La requête, bien sûr, peut être perdue, peut être erronée... Et donc, dans ce mode-là, il n'y a aucune garantie.

Mode non-connecté

A contrario, en mode connecté, avant d'échanger des données, le client et le serveur vont avoir une phase d'ouverture de connexion et, à la fin, une phase de fermeture de connexion. On va dès lors être sûr que les deux parties en présence sont bien d'accord pour échanger des données. On peut associer à une connexion un contexte à chaque extrémité, c'est-à-dire un espace mémoire qui permet de stocker des données relatives à la connexion. Bien sûr, à la fermeture de la connexion, le contexte sera libéré. On peut ici faire l'analogie avec le réseau téléphonique, puisque lorsqu'on passe une communication téléphonique, il y a d'abord une phase d'établissement de la communication qui va faire sonner le téléphone du destinataire. À ce moment-là, le destinataire a le choix : soit il accepte la demande de communication, soit il la refuse. Et dans le cas où il accepte, il y a bien des ressources qui sont réservées pour la communication. C'est ce qu'on appelle le circuit dans le réseau téléphonique et ce circuit va garantir que les données arrivent à la bonne vitesse.

Mode connecté

Le seul protocole d'Internet parmi tous ceux que l'on voit dans le cadre de ce cours qui soit connecté est le protocole TCP. Il a donc une phase d'ouverture de connexion et une phase de fermeture de connexion et le contexte associé à la communication repose sur des zones mémoire pour envoyer et recevoir des données de manière fiable.

Serveurs itératifs vs serveurs concurrents

On va distinguer plusieurs types de serveurs : les serveurs itératifs et les serveurs concurrents.

Un serveur itératif va traiter l'ensemble des requêtes qui arrivent de manière séquentielle, c'est-à-dire l'une après l'autre. Alors qu'un serveur concurrent va pouvoir accepter différentes requêtes en parallèle et créer un processus fils (ou un thread) qui va s'occuper du traitement d'une requête. Cela permet de paralléliser le traitement des requêtes et donc des réponses qui vont être faites par plusieurs processus concurrents côté serveur.

Quels sont les avantages et inconvénients des deux types de serveurs ? Le serveur itératif est généralement en mode non-connecté, ce qui permet une réponse plus rapide aux requêtes. Il est adapté au cas des requêtes qui sont simples à traiter.

À l'inverse, le serveur concurrent qui crée un fils pour chaque requête sera utile dès lors que le traitement de la requête est important, car le fait de créer le processus fils et de mettre en place le contexte de traitement de la requête prend du temps et nécessite des ressources au niveau du système d'exploitation. Généralement, le serveur concurrent est en mode connecté. Par exemple, pour le transfert de fichiers, on essaie de paralléliser le transfert de plusieurs fichiers en même temps et donc le traitement de plusieurs requêtes en parallèle.

Tampons d'émission et de réception

Dans le cadre de la programmation des sockets, on utilise le paradigme de communication du passage de messages (ou send/recv), ce qui pose un certain nombre de problèmes liés aux écritures simultanées dans des zones mémoires pour émettre ou recevoir des données (au niveau du client ou du serveur). Et évidemment, côté serveur, la problématique est également, comme on vient de le voir, le traitement en parallèle de plusieurs requêtes issues de différents clients.

La socket peut être vue comme un fichier virtuel auquel est associée une zone d'émission qu'on va appeler le tampon d'émission et une zone de réception, le tampon de réception.

Faut-il des zones d'émission et des zones de réception distinctes pour chaque processus ou bien peut-on mutualiser les zones mémoires ?

S'il n'y a qu'une seule zone d'émission/réception qui est commune au niveau du processus P1 (comme sur le schéma ci-dessous), un premier problème survient car il n'est pas possible de distinguer les écritures concurrentes dans cette zone, qui peuvent venir soit du processus P1 directement quand il veut envoyer à P2, soit du processus P2 lorsqu'il envoie des données à P1. Il est donc nécessaire de distinguer le tampon d'émission du tampon de réception.

Tampons d'émission et de réception

Dans le schéma ci-dessous, on a trois processus qui communiquent entre eux : P1, P2 et P3. On a maintenant bien séparé la zone d'émission (en haut) et la zone de réception (en bas) au niveau de chaque processus. Cependant, dans ce cas, un deuxième problème survient si les processus P2 et P3 décident au même moment d'envoyer un message à P1. Dans ce cas, la zone de réception de P1 va se retrouver à traiter des écritures concurrentes dans sa zone de réception qui est mutualisée pour P2 et P3. Ainsi, il apparaît important de bien séparer la zone de réception pour P2 et P3.

Séparation des tampons

En conclusion, il faut disposer d'une zone d'émission et d'une zone de réception distincte pour chaque paire de processus qui communiquent.

Par ailleurs, il faudra veiller à ce que deux écritures successives ne s'écrasent pas et soient bien réalisées dans des zones mémoires différentes au sein même du buffer de réception (ajout à la fin). Pour finir, il faudra vérifier que la capacité du récepteur est suffisante pour recevoir les données, c'est-à-dire qu'il y a assez de place en réception pour recevoir le message envoyé par le processus émetteur. Et là, c'est bien le contrôle de flux qui doit intervenir, le contrôle de flux qui vise à limiter les émissions en fonction de la capacité de réception du récepteur. S'il n'y a plus de place dans le tampon de réception, il faut bloquer l'émission (envoi bloquant) jusqu'à ce que le récepteur libère les données dans le tampon en réalisant une opération de lecture. Ce faisant l'opération d'envoi se débloque.

Fichier virtuel socket

Au niveau des sockets, nous avons donc deux processus qui communiquent entre eux, le client et le serveur, soit en mode connecté, soit en mode non connecté, et chaque extrémité de la communication (d'un côté le client, de l'autre le serveur) va avoir sa propre socket, c'est-à-dire un fichier virtuel auquel sera associé une zone d'émission et une zone de réception, comme on vient de le voir précédemment.

La socket va donc être identifiée de manière unique par l'adresse IP de la machine qui héberge le client ou le serveur et un numéro de port. La socket est donc l'objet qui va utiliser la zone d'émission et la zone de réception. Ainsi l'adresse de transport, qui est le couple adresse IP/numéro de port, va identifier un point de communication, c'est-à-dire une socket sur une machine pour une application donnée.

Les numéros de port sont donc le moyen de désigner l'application émettrice ou destinataire avec un numéro qui se trouve codé sur deux octets, donc qui varie entre 0 et 65535. Les numéros inférieurs à 1024 ont été fixés et ils sont réservés aux applications système. Les numéros supérieurs à 1024 sont réservés aux applications utilisateurs. Ces derniers sont choisis de manière unique par le système d'exploitation. L'unicité ici concerne le fait que sur une même machine, deux applications ne peuvent pas utiliser le même numéro de port afin de bien distinguer les différentes sockets utilisées par chacune des applications.

En pratique, une socket est un fichier virtuel. Elle dispose des opérations sur les fichiers : l'ouverture, la fermeture, la lecture ou l'écriture. Ce fichier est virtuel dans le sens où il a une existence uniquement dans la mémoire du système d'exploitation, pas sur le disque dur ou sur un système de stockage. Pour accéder à ces fichiers virtuels, on utilise des opérations particulières appelées appels systèmes (comme pour les fichiers réguliers). Ces opérations se retrouvent dans la bibliothèque socket. Cette bibliothèque socket est donc une interface de programmation (API) avec un certain nombre de fonctions qu'on peut appeler pour communiquer entre le processus du client (ou du serveur) et le système d'exploitation.

On distingue trois types de socket qui sont définis lors de sa création :

  • Le type SOCK_STREAM qui correspond aux communications en mode connecté avec TCP.
  • Le type SOCK_DGRAM qui correspond à l'utilisation d'UDP, donc en mode non connecté.
  • Le type SOCK_RAW qui utilise directement IP (ou ICMP). Il est en particulier utilisé pour faire les ping ou les traceroute.

D'un point de vue système, la socket est un fichier (virtuel) qui est identifié dans le système d'exploitation par un numéro de fichier, appelé descripteur de fichier. Ainsi, lorsque la socket est créée par le système, deux zones mémoires sont automatiquement allouées pour l'émission et la réception.

Socket en mode connecté (TCP)

Le schéma ci-dessous montre une communication TCP entre un navigateur web (client, port 5004) et un serveur web (port 80). Chaque processus a sa propre socket, et ses tampons de communication associés, qui sont nommés le TCP send buffer pour l'émission et le TCP recv buffer pour la réception.

Connexion TCP

Lorsque le navigateur envoie une requête en écrivant dans la socket, les données sont déposées dans le TCP send buffer et transmises au serveur via le protocole TCP de manière fiable. Le serveur peut alors lire dans le TCP recv buffer les requêtes du client.

En mode connecté, pour que le client puisse contacter le serveur, ce dernier doit avoir été préalablement démarré et en attente de connexions.

  • Le client crée alors une socket locale et précise l'adresse IP et le numéro de port du serveur pour lui envoyer sa demande de connexion.
  • Si le serveur accepte, il crée une nouvelle socket pour gérer le dialogue avec ce client. Ainsi, le serveur peut disposer de plusieurs sockets, une pour chaque client, et dialoguer avec plusieurs clients en parallèle, dans le cas d'un serveur concurrent.

Concrètement, voici comment se passe l'établissement de la connexion en mode connecté avec l'API socket :

Dialogue en mode connecté avec l'api socket

Côté client :

  • La primitive socket() permet d'ouvrir la socket et de la créer ensuite.
  • La primitive connect() permet d'envoyer la demande de connexion au serveur.
  • Et ensuite, une fois que la connexion est ouverte, le client va faire des write() pour envoyer une requête et des read() pour lire la réponse.

Côté serveur :

  • La création de la socket au lancement du processus serveur se fait avec la primitive socket().
  • La primitive bind() permet de rattacher cette socket à un port particulier (par exemple 80 pour un serveur web).
  • La primitive listen() permet de mettre le serveur en attente de réception des demandes de connexion.
  • La primitive accept() permet d'accepter la connexion et de créer une nouvelle socket pour dialoguer avec le client.
  • Si la connexion est acceptée, la connexion est alors ouverte et le serveur peut ensuite recevoir les requêtes et y répondre à l'aide des primitives read() et write().

Premier exemple : client echo

Pour illustrer cela, prenons un premier exemple de code Python qui envoie une requête HTTP vers le serveur web www.perdu.com et affiche la réponse :

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM )
s.connect(('www.perdu.com' , 80))
s.send(b'GET / HTTP/1.1 \r\nHost: www.perdu.com \r\nConnection: close\r\n\r\n')
data = s.recv(1024)
s.close()
print (data)

Dans la version Python de l'API, les méthodes write() et read() sont remplacées par les méthodes send() et recv(), mais le fonctionnement est le même. La création de la socket se fait avec la méthode socket() qui prend en argument AF_INET pour indiquer que l'on utilise l'adresse IP et SOCK_STREAM pour indiquer que l'on utilise le protocole TCP. La méthode connect() prend en argument l'adresse IP du serveur et le numéro de port du serveur. La méthode send() prend en argument la requête HTTP à envoyer au serveur. La méthode recv() prend en argument la taille du buffer de réception. La méthode close() permet de fermer la socket. Remarquez que la méthode send() prend en argument un objet de type bytes (tableau d'octets) et non une chaîne de caractères. C'est pour cela que nous avons utilisé le préfixe b pour indiquer que la chaîne de caractères est de type bytes.

Serveur echo en mode itératif

Prenons maintenant l'exemple d'un serveur "echo" qui va simplement renvoyer au client la requête qu'il lui a envoyée.

import socket

sserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sserver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sserver.bind(('', 7777))
sserver.listen(1)

while True:
    sclient , addr = sserver.accept()
    print('Connected by' , addr)
    while True:
        data = sclient.recv(1500)
        if data == b'' or data == b'\n':  # Fermeture de la connexion par le client
            break
        print(data)
        sclient.send(data)
    print('Disconnected by' , addr)
    sclient.close()

sserver.close()

La première (vraie) instruction crée la socket du serveur, comme dans l'exemple du client. La méthode bind() permet de rattacher cette socket à un port particulier (ici le port 7777). Remarquez que le nom de l'hôte est vide, cela signifie que le serveur va écouter sur toutes les interfaces réseau. L'instruction listen(1) permet de mettre le serveur en attente de réception des demandes de connexion. Le paramètre (ici 1) indique la longueur de la file d'attente, c'est-à-dire le nombre de demandes de connexion en attente d'ouverture par le serveur.

Là où les choses deviennent intéressantes, c'est la première boucle while True: qui permet de traiter de manière séquentielle les demandes de connexion. Pour chaque demande de connexion, on va créer une nouvelle socket sclient qui sera utilisée pour dialoguer avec le client. Remarquez le rôle très différent entre la socket sserver et la socket sclient : la première est utilisée par le serveur pour écouter les demandes de connexion, la seconde est utilisée par le serveur pour dialoguer avec un client spécifique.

Une fois la connexion établie, on entre dans une deuxième boucle while True: qui permet de traiter les requêtes du client. Ici on va simplement afficher la requête à l'écran et renvoyer la même requête au client. Lorsque le client ferme la connexion, on sort de la boucle while True et on ferme la socket sclient et on attend une nouvelle connexion.

On peut remarquer dans cet exemple que le serveur ne gère qu'une seule connexion à la fois. En effet, la méthode recv() est bloquante, c'est-à-dire que le serveur va attendre que le client envoie une requête avant de continuer son exécution. Si le client ne répond pas, le serveur va rester bloqué dans la méthode recv() et ne pourra pas traiter d'autres demandes de connexion.

Serveur echo en mode concurrent (avec threads)

Pour mettre en oeuvre un serveur en mode concurrent, il faut déléguer les traitements associés à chaque connexion cliente à un processus fils, le processus père s'occupant essentiellement d'attendre les nouvelles demandes de connexions et de les accepter. Une solution élégante pour programmer ce serveur consiste à utiliser des processus légers, appelés thread.

Voici le code qui illustre cette solution :

import socket
import threading

sserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sserver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sserver.bind(('', 7777))
sserver.listen(1)

def handle(sclient):
    while True:
        msg = sclient.recv(1500)
        if len(msg) == 0:
            print("client disconnected")
            sclient.close()
            break
        sclient.sendall(msg)
    sclient.close()


while True:
    sclient, a = sserver.accept()
    print("new client:", a)
    t = threading.Thread(None, handle, None, (sclient,))
    t.start()

Dans ce code, un thread t est créé pour chaque demande de connexion acceptée (accept()). Au démarrage du thread (t.start()), la fonction handle() est appelée pour traiter le dialogue avec le client correspondant à la socket sclient. Ainsi, plusieurs clients peuvent être traités concurremment, chacun exécutant sa propre instance de la fonction handle() dans un thread particulier. Le thread se terminera quand la fonction handle() se terminera elle-même.

Serveur echo en mode concurrent (avec select)

Une alternative pour la programmation du serveur echo en mode concurrent consiste à utiliser l'opération select. Cette opération va permettre de surveiller simultanément plusieurs sockets au sein d'un seul processus (pas de threads). Voici le code qui illustre cette solution :

import socket
import select

sserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sserver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sserver.bind(('', 7777))
sserver.listen(1)
lst_sclients = []

while True:
    lst_ready_sockets, _, _ = select.select(lst_sclients + [sserver], [], [])
    for s in lst_ready_sockets:
        if s == sserver:
            sclient, addr = sserver.accept()
            print("new client:", addr)
            lst_sclients.append(sclient)
        else:
            data = s.recv(1500)
            if data == b'' or data == b'\n':
                s.close()
                lst_sclients.remove(s)
            else:
                s.send(data)

sserver.close()

La méthode select() prend en paramètre 3 listes de sockets (ou plus généralement de descripteurs de fichiers). La première liste contient les sockets surveiller en lecture, pour écouter les demandes de connexion de nouveaux clients, ou pour lire les données envoyées par les clients. La deuxième liste contient les sockets à surveiller en écriture. La troisième liste contient les sockets à surveiller pour les exceptions. Dans notre cas, seule la première liste est utile.

La méthode select() va renvoyer une liste de sockets sur lesquelles des données sont disponibles. On va donc parcourir cette liste et traiter les données sur chaque socket. Le traitement sera différent selon que la socket est la socket du serveur (sserver) ou une socket associée à un client. Si la socket est la socket du serveur, cela signifie qu'un nouveau client vient de se connecter. Dans ce cas, on va créer avec accept() une nouvelle socket pour dialoguer avec ce client et on va ajouter cette socket à la liste des sockets à écouter. Si la socket est une socket d'un client, cela signifie que le client a envoyé une requête. Dans ce cas, on va traiter la requête et renvoyer la réponse au client. Si la socket est une socket d'un client et que le client a fermé la connexion, on va supprimer la socket de la liste des sockets à écouter.

Socket en mode non-connecté (UDP)

En mode non connecté, le client va transmettre un message au serveur sans lui demander son autorisation à l'avance. Il va préciser pour chaque requête l'adresse de la socket destination, c'est-à-dire l'adresse IP du serveur et le port du serveur. Et lorsqu'il envoie sa requête, il va mettre dans l'entête UDP son numéro de port et son adresse IP afin que le serveur puisse lui répondre. Le serveur va traiter la requête et lui répondre. Et ensuite cette communication sera terminée, sachant que le client peut éventuellement faire plusieurs requêtes successives.

Dialogue en mode non-connecté avec l'API socket

Pour envoyer une requête dans l'API socket, c'est la primitive sendto() qui est utilisée.

Voici un exemple de client echo en mode non connecté :

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'hello', ('localhost', 7777))
data, addr = s.recvfrom(1024)
print(data)
s.close()

Comme dans le cas du mode connecté, cela commence par la création de la socket. Par contre, ici, cette socket est de type SOCK_DGRAM et non SOCK_STREAM. Ensuite, on envoie la requête avec la primitive sendto(). Cette primitive prend en paramètre le message à envoyer et un tuple contenant l'adresse IP et le port du serveur. Ensuite, on attend la réponse avec la primitive recv_from(). Cette primitive prend en paramètre la taille de la zone de réception. Et à la sortie de cette primitive, on récupère le message reçu et l'adresse de la socket source.

Voici ce que cela donne en mode non-connecté côté serveur :

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('', 7777))

while True:
    data, addr = s.recvfrom(1024)
    s.sendto(data, addr)

Là encore, on crée une socket de type SOCK_DGRAM. Comme dans le cas connecté, on commence à s'attacher à un port (UDP ce coup-ci) à l'aide de la primitive bind(). La suite est beaucoup plus simple : on boucle sur la primitive recvfrom() qui va attendre une requête et renvoyer le message reçu et l'adresse de la socket source. Ensuite, on renvoie la réponse avec la primitive sendto().

Annexes

Les messages en Python

Dans l'API socket en Python, les messages envoyés ou reçus via les appels send()/recv() sont de type bytes, c'est-à-dire des tableaux d'octets. Cependant, il est souvent pratique de manipuler des chaînes de caractères (type str) dans son programme. Il est alors nécessaire de procéder des conversions du type bytes vers str, et réciproquement.

>>> s = "hello"           # type str
>>> c = s.encode("ascii") # c = b"hello" (type bytes)

Et inversement :

>>> c = b"hello"          # type bytes
>>> s = c.decode("ascii") # s = "hello" (type str)

Accessoirement, il est souvent plus lisible d'afficher une variable de type bytes au format hexadecimal, en utilisant la fonction hex() comme ceci :

>>> c = b'\x00\x00\x00\x14'
>>> c.hex()
'00000014'

Un peu d'aide sur les types standard en Python3 : https://docs.python.org/3/library/stdtypes.html

Primitives bloquantes et non bloquantes

Lorsqu'on appelle une primitive d'une socket, il est important de comprendre quand l'appel est bloquant et quand il est non bloquant. Nous allons détailler cela maintenant.

L'appel à la primitive send() (ou sendto()) est non-bloquant. Cela signifie qu'on poursuit dans notre programme notre fil d'exécution, sans attendre que le message soit reçu à l'autre extrémité. Plus précisément, la primitive send() place le message à envoyer dans le tampon d'émission et rend la main ensuite. Si le tampon d'émission est plein, la primitive send() va bloquer jusqu'à ce qu'il y ait de la place dans le tampon d'émission.

L'appel à la primitive recv() (ou recvfrom()) est bloquant. Cela signifie que l'appel à la primitive recv() va bloquer jusqu'à ce qu'un message soit reçu ou qu'une demande de fermeture de connexion soit reçue.

Cependant, afin de ne pas bloquer indéfiniment l'exécution du programme sur l'attente d'un recv(), vous pouvez mettre en place un mécanisme de timeout (en utilisant la fonction socket.settimeout()), qui produit une erreur après un délai de 2 secondes (valeur par défaut).

Les appels aux primitives connect() et accept() sont bloquants. Cela signifie que le fil d'exécution sera bloqué jusqu'à la finalisation de l'ouverture d'une nouvelle connexion ou jusqu'à l'échec de l'ouverture d'une nouvelle connexion.

Par ailleurs, il est possible de paramétrer les sockets à leur ouverture pour indiquer que les appels send()/recv() soient non bloquants. Pour cela, il faut utiliser la primitive socket.setblocking(False).

Plus généralement, les primitives socket.setsockopt() et socket.getsockopt() permettent de paramétrer les sockets. Par exemple, on peut paramétrer la taille de la zone d'émission et de la zone de réception. Ce qui est utile pour des raisons de performance, puisque la fenêtre d'émission et la fenêtre de réception sont des paramètres importants pour la performance d'une connexion TCP. Par défaut, la taille de la fenêtre d'émission et de réception est de 64 kilo-octets. En particulier, l'option tcpnodelay permet de forcer le protocole TCP à envoyer les données dès qu'elles sont disponibles. C'est utile pour les applications de type connexion à distance où il faut que les données soient envoyées le plus rapidement possible.

Serveurs multi-protocoles et serveurs multi-services

Un serveur multi-protocoles est un serveur qui écoute sur une socket TCP et une socket UDP. C'est généralement le cas des serveurs daytime et des serveurs DNS.

Un serveur multi-services est un serveur qui peut répondre à plusieurs services simultanément (par exemple : web, daytime, ...). L'intérêt du serveur multi-services, c'est qu'il y a un seul serveur qui tourne et qui est donc en permanence en attente de requêtes, mais sur plusieurs sockets simultanément. Cela permet de lancer le service demandé à la demande et uniquement à la demande. Typiquement, le serveur web ne va être lancé que lorsqu'une requête à destination du serveur web arrivera sur le serveur multi-services.

Le serveur multi-services va écouter sur plusieurs sockets (une socket par service). Lorsqu'une demande de connexion va arriver, il pourra créer un processus fils pour prendre en compte l'ouverture de la connexion. Chaque processus fils va donc se retrouver à gérer la socket correspondant au service proposé, et une socket pour chaque client qui a fait une demande de connexion.

Les serveurs sont des processus qui tournent en permanence, appelés démons, et qui consomment beaucoup de ressources. Par exemple, le serveur FTP, le serveur web et le serveur SSH sont des processus qui tournent en permanence et qui consomment du processeur. C'est pourquoi il est intéressant de créer des serveurs multi-services qui lanceront les processus serveurs à la demande, lorsqu'ils seront sollicités.

Sous Unix, le processus inetd est un "super serveur" qui est à la fois multi-services et multi-protocoles. Il va centraliser toutes les requêtes qui arrivent sur la machine et activer les services à la demande au fur et à mesure de l'arrivée des requêtes. Autrement dit, le serveur web ou le serveur FTP ne seront lancés que lorsqu'ils auront été sollicités. Pour réaliser cela, il y a un fichier de configuration inetd.conf qui permet de préciser les différents services qui sont scrutés par le serveur inetd.

Voici un exemple de fichier de configuration inetd.conf :

#  Internet services syntax :
#  <service_name> <socket_type> <proto> <flags> <user> <server_pathname> <args>
#  wait : pour un service donné, un seul serveur peut exister à un instant donné
# donc le serveur traite l'ensemble des requêtes à ce service
# stream --> nowait : un serveur par connexion
ftp   stream  tcp nowait root /etc/ ftpd ftpd -l
tftp  dgram   udp wait   root /etc/tftpd    tftpd
shell stream  tcp nowait root /etc/rshd            rshd
pop3  stream tcp  nowait root /usr/local/lib/popper  popper -s -d -t /var/log/poplog
# internal services :
# => service réalisé par inetd directement
time  stream  tcp nowait  root internal
time  dgram   udp nowait  root internal

On retrouve ftp, tftp, pop3 (pour la réception des mails) ainsi que le processus timing pour envoyer la date et l'heure.

L'administrateur de la machine peut préciser les services qu'il propose et comment ils sont lancés (avec quels paramètres, en tant quel utilisateur, etc.). Il peut également préciser pour chaque service, s'il est proposé en mode connecté (tcp) ou en mode non connecté (udp).