Application serveur
Une application serveur est une application offrant un service à d’autres applications (locales ou distantes) appelées applications clientes.
Aperçu
L’application serveur est un programme à l’écoute de demandes de connexion provenant souvent de clients connectés au réseau. Cette application écoute sur une ou plusieurs adresses IP et un numéro de port.
- Adresses IP d’écoute
-
L’adresse IP identifie un point de connexion sur le réseau. Un serveur peut écouter n’importe lesquelles de ses adresses. Il existe des IP particulières :
-
127.x.y.zqui sont des adresses accessibles uniquement en local : une application écoutant seulement sur127.0.0.1ne peut être sollicitée que par des clients résidant sur la même machine -
0.0.0.0qui représente toutes les adresses du serveur.
-
- Port d’écoute
-
Le port est un entier identifiant le service. Les ports <1024 sont réservés à des services internet bien connus (well-known ports). Ils sont définis dans le fichier
/etc/services. Une application sans droit particulier ne peut pas écouter un port <1024. Pour le faire, elle doit disposer des droits particuliers. C’est pourquoi ces ports sont appelés ports privilégiés.
Le couple (IP,port) s’appelle un network socket.
C’est un point d’entrée de service unique sur le réseau.
Implémentation
Lorsqu’une demande de connexion arrive au serveur, celui choisit ou non de l’accepter. S’il l’accepte, alors s’établit une session au cours de laquelle se déroule un dialogue entre le serveur et le client.
Un serveur digne de ce nom ne doit pas faire attendre ses clients. Il doit donc être capable de gérer le dialogue avec plusieurs clients simultanément. De plus, il doit faire attention à ne pas être monopolisé par un client. Or la cadence de chaque dialogue dépend de la réactivité du client, ainsi que de la qualité, des capacités et de l’encombrement du réseau. Donc pour avoir un service fluide, chaque session doit être traitée en asynchrone vis à vis des autres, afin de ne pas introduire de temps d’attente superflus : chaque session doit être gérée à son rythme, indépendamment des autres.
Squelette d’une application serveur
Le serveur se compose d’une première coroutine qui lance l’écoute sur le réseau et l’attente des requêtes :
async def main(addr,port):
server = await asyncio.start_server(handle,addr,port) (1)
async with server:
await server.serve_forever() (2)
| 1 | lancement de l’écoute sur le socket (addr,port) et mémorisation de la fonction à exécuter à chaque connexion |
| 2 | boucle d’attente perpétuelle |
A chaque connexion, une coroutine (nommée dans l’exemple ci-dessus handle) sera lancée.
Ci-dessous un exemple simpliste d’une telle coroutine qui simplement sort à l’écran la requête reçue et la réponse qui lui est faite :
async def handle(reader, writer):
data = await reader.read(100) (1)
print(f"réception de : {data.decode()}") (2)
response = "je suis le serveur et voici ma reponse"
writer.write(response.encode()) (3)
await writer.drain() (3)
print(f"envoi de : {response}")
writer.close()
| 1 | récupération de la requête (sous forme de bytes) |
| 2 | conversion en string et affichage |
| 3 | envoi de la réponse (sous forme de bytes) |
Squelette d’un client
Le client dispose d’une coroutine réalisant la demande de connexion vers le serveur, l’envoi de la requête, l’attente et la récupération de la réponse.
|
L’usage de coroutines par le client n’est pas indispensable, mais cela lui permet de récupérer sans blocage plusieurs ressources en même temps (par exemple les CSS, images, etc. contenus dans une page web) ou bien de faire du scraping. |
async def send(message,addr,port):
reader, writer = await asyncio.open_connection(addr,port)
print(f'envoi de : {message}')
writer.write(message.encode()) (1)
data = await reader.read(100) (2)
print(f'réception de : {data.decode()}') (3)
writer.close()
| 1 | envoi d’une requete (sous forme de bytes) |
| 2 | attente et lecture de la réponse (sous forme de bytes) |
| 3 | sortie à l’écran de la string correspondante |
Problème
Il s’agit de développer une application client/serveur élémentaire.
Question 1
En assemblant les codes ci-dessus, concevoir un serveur à l’écoute sur 127.0.0.1 sur le port non privilégié 1234, qui attend l’arrivée d’un message 1, et renvoie au client un message 2, en loguant le tout à l’écran.
Concevoir le client envoyant un message au serveur, en loguant à l’écran la requête et la réponse.
Question 2
Modifier le client de façon à ce que l’adresse IP du serveur sollcité soit variable (fournie en argument de ligne de commande).
Le client peut-il se connecter à un serveur distant ?
Question 3
Modifier le serveur pour qu’il écoute sur toutes ses IP. Vérifier que cela permet à une autre machine de l’interroger.
Question 4
On définit le protocole de communication suivant :
-
si le client envoie la chaine
"où?", le serveur renvoie son nom (indice :socket.getfqdn), -
sinon si le client envoie la chaine
"qui?", le serveur renvoie le nom de l’utilisateur qui a lancé le serveur (indice :os.getlogin), -
sinon, si le client envoie la chaine
"quand?", le serveur renvoie la date de dernière modification de son code source (indice :sys.arg[0],os.lstat,time.ctime) , -
sinon, si le client envoie la chaine
"comment?", le serveur renvoie son code source, -
sinon, le serveur renvoie
"pas compris!".
Le serveur ferme la session aussitôt après avoir répondu.
Implémenter ce protocole dans le serveur, et adapter le client pour qu’il puisse dialoguer avec ce nouveau serveur.
Question 5
On modifie le protocole, en ajoutant une requête "quit!".
Comme précédement, le client envoie une chaine de caractères au serveur, qui lui répond par une chaine de caractères.
Mais maintenant, la connexion entre le serveur et ce client reste ouverte
(tant que celui-ci ne lui a pas envoyé "quit!")
, ce qui permet au client d’envoyer d’autres requêtes dans la foulée (sans ré-établir une nouvelle connexion).
A la réception de "quit!", le serveur répond "bye!" et ferme la connexion.
Modifier le client et le serveur en conséquence. Vérifier que plusieurs clients peuvent tenir une conversation avec le serveur en même temps.