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.
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.
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.
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.
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 lesping
ou lestraceroute
.
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.
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 :
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 desread()
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()
etwrite()
.
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.
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).