Client/serveur

Introduction

Dans cette article nous allons découvrir le concept de client-serveur. Client–serveur désigne un mode de transaction (souvent à travers un réseau) entre plusieurs programmes ou processus : l’un qualifié de client, envoie des requêtes ; l’autre, qualifié de serveur, attend les requêtes des clients et y répond(Wikipédia). Cette environnement qui reste pour une grande partie de monde la définition de l’internet car les gens ont tendance à croire que l’internet c’est un navigateur web qui répond à leur recherche. Cela reflète la place et l’importance qu’il a l’environnement client-serveur dans le domaine de l’informatique.

Pour comprendre ce qui est cet environnement prenons toujours l’exemple de navigateur web. Dans l’environnement client-serveur le navigateur web joue le rôle de client. Lorsque l’utilisateur ouvre son navigateur web à partir de son ordinateur ou smartphone (une machine dont moins performante que le serveur) et tape l’url d’un site web, le navigateur envoi des requêtes de demande de connexion à ce serveur web. Un serveur est une machine avec des capacités importantes au niveau des ressources comme RAM, CPU, stockage. Le serveur est toujours en écoute (Listen) sur sa carte réseau dans un port défini. Cette association d’adresse IP et le port s’appelle Socket. Autrement dit c’est un connecteur réseau qui permet d’établir une session (Accept) puis de recevoir et d’expédier des données grâce à elle. C’est donc ce qui se passe lorsque le serveur reçoit la requête de client (connect) il l’accepte et établie une connexion pour pouvoir échanger les données (Send/Recv). Une fois que les données sont échangées c’est à dire une fois que l’utilisateur a consulté la page et qu’il ferme cet onglet, il ferme le lien avec le serveur.

La figure suivant schématise l’explication qu’on vient de voir:

Figure1:Fonctionnement d’un environnement client-serveur

Pour bien comprendre le fonctionnement j’ai divisé cette article en plusieurs parties; nous allons créer et simuler un client-serveur dans différents langage de programmation pour pouvoir comprendre en détails son fonctionnement:

  • Pour cela nous verront dans un premier temps l’exécution d’un ensemble de code déjà faits (récupéré) permettant de monter un serveur web http en langage C.
  • Puis nous verrons comment créer un client-serveur dans langage PYTHON dans le quelle nous verrons dans un premier temps un simple serveur qui traite uniquement un seul client puis nous verrons comment modifier ce code dans le mode non bloquantes pour qu’il puisse traiter plusieurs clients.
  • Ensuite nous verrons comment faire la même chose en langage Go.C’est à dire créer un serveur qui gère plusieurs clients.
  • Puis nous allons voir comment mettre en place un serveur web multithrédé avec le langage Rust.
  • Finalement nous verrons l’utilisation d’un serveur de base donnée nommé Redis.

1.Programme tiny

Le but de cette premier partie est de compiler plusieurs programmes à l’aide de l’outil make afin de créer un exécutable qui va permet de simuler un serveur http implémenté avec un socket. Puis de le tester avec un client.

Pour cela nous possédons le dossier tiny qui contient un programme en .h ou nous avons créé le bibliothèque csapp.h dans le quel :

  • On définit la taille de buffer « RIO_BUFSIZE » c’est-à-dire la taille maximum de donné/fichier qu’on peut échanger en une envoie.
  • Une fonction permettant de créer notre socket avec l’instruction bind.
  • Les fonctions permettant d’afficher des messages d’erreurs.
  • Les fonctions permettant d’allouer et de libérer de la mémoire pour stocker les données reçu.
  • Les fonctions permettant de gérer les signaux entrant et sortant.

Puis nous avons un programme ou on fait appels aux fonctions qu’on a définie sur le bibliothèque csapp.h qui permet d’afficher les messages d’erreurs.

Ensuite nous avons le programme tiny.c qui permet d’activer le socket ainsi que d’écouter sur le socket, attendre et accepter une connexion d’un client puis génère une réponse avec le fichier home.html et l’image godzilla.jpg/GIF et transmet au programme CGI. On a aussi quelques fonctions qui permettent d’afficher des messages erreur lié aux accès et lecture de fichier home.html ainsi que des erreurs liées au traitement des requêtes http.

Finalement nous avons un dossier ou nous avons un programme CGI qui permet de récupérer la réponse généré par le programme tiny.c et d’afficher cette réponse avec des phrases «Welcome to add.com: » et « Thanks for visiting! » sur le navigateur de client.

Voici un schéma représentant le fonctionnement ces programmes:

Figure 2:fonctionnement d’un serveur http avec CGI

Pour tester ce code téléchargez le fichier archive via ce lien : https://lipn.univ-paris13.fr/~cerin/LPASUR/tiny.tar

Puis décompressez en utilisant cette commande :

tar xvf tiny.tar

Ensuite il faut se rendre dans le chemin de dossier :

cd le_chemin_de_dossier_tiny

Puis lancer la commande make pour exécuter le fichier makefille que contient le dossier tiny :

make

Cela va nous créer un fichier output exécutable nommé tiny.Il suffit en suite de lancer cette exécutable en précisant le port dans la quel on veut que le serveur écoute :

./tiny 8000

Allons vérifier le fonctionnement de notre serveur http sur le navigateur web :

localhost:8000

Voici l’aperçu de l’affichage lorsqu’on tape localhost:8000.Comme le figure suivant illustre on voit bien que le serveur fonctionne bien.

Figure 3:Aperçu de résultat

2.Programmation client serveur dans langage python

Voici un exemple d’un programme d’un serveur en python :

 #!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 8080        # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

Nous avons besoin d’importer le module socket affin de créer notre socket serveur(connecteur).« With » est une instruction dans python permet d’exécuter un groupe d’instruction suivant un contexte. On crée un objet socket en utilisant l’instruction socket.Socket().ici on utilise le protocole TCP comme type de socket pour éviter de s’inquiéter de la perte des paquets et désordonnément des paquets. Ensuite nous lions à ce socket les informations de paramètres réseau tel que son adresse IP ainsi que le port sur lequel il va écouter.

Un socket d’un serveur écoute et attend qu’un client se connecte à son socket s.listen().Lorsqu’un client demande d’établir une connexion avec le serveur, le serveur l’accepte et établi la connexion. Le client et le serveur établie une connexion avec la méthode de hanshake. A partir de moment ou un client est connecté après l’exécution de accept() un nouveau objet socket conn diffèrent de socket d’écoute est créé qui va être servi pour la communication entre le serveur et le client.

En se servant de ce nouveau objet socket le serveur et le client s’échangent avec des méthodes send() recv().

Conn.sendall(data) permet renvoyer toutes les données reçu par client. Cela peut servir pour s’assurer le client en terme de perte de donnés.

Et une fois que l’échange est terminé le client coupe la connexion close().

Coté client :

#!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # The server's hostname or IP address
PORT = 8080       # The port used by the server

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)

print('Received', repr(data))

Du coté client on crée le socket et on connecte à notre socket serveur. Autrement dit à l’adresse IP et le port de serveur et nous envoyons et recevons des données.L’instruction print(‘Received’, repr(data)) permet d’afficher les données que le serveur a envoyé.

L’exécution de ces deux codes nous donne les résultats suivant :

Figure 4 : Résultats des exécution des deux codes.

Du coté serveur nous avons l’adresse IP de client et le port avec lequel il est connecté au serveur.Du coté client on aura les données que le serveur a envoyé à savoir que dans ces code d’exemples c’est les même données que le client a envoyé au serveur comme expliqué précédemment.

Nous pouvons observer le statut de notre socket après lancement de programme serveur en exécutant la commande suivant :

ss -antp
Figure 5 : Statut de socket

Nous pouvons voir que le serveur est en écoute sur le port 8080 comme on a défini dans le socket et sur son adresse de boucle.

Maintenant nous allons voir comment un exemple d’un code serveur qui gère plusieurs clients :

#!/usr/bin/env python3

import sys
import socket
import selectors
import types

sel = selectors.DefaultSelector()


def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print("accepted connection from", addr)
    conn.setblocking(False)
    data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)


def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            data.outb += recv_data
        else:
            print("closing connection to", data.addr)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            print("echoing", repr(data.outb), "to", data.addr)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]

if len(sys.argv) != 3:
    print("usage:", sys.argv[0], "<host> <port>")
    sys.exit(1)

host, port = sys.argv[1], int(sys.argv[2])
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print("listening on", (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)

try:
    while True:
        events = sel.select(timeout=None)
        for key, mask in events:
            if key.data is None:
                accept_wrapper(key.fileobj)
            else:
                service_connection(key, mask)
except KeyboardInterrupt:
    print("caught keyboard interrupt, exiting")
finally:
    sel.close()

Pour créer un serveur qui est capable de traiter plusieurs clients à la fois la méthode traditionnelle est de programmer un serveur multithread. Un thread est une séquence de telles instructions dans un programme qui peut être exécutée indépendamment d’un autre code. Un programme multithread contient deux ou plusieurs parties qui peuvent s’exécuter simultanément. Chaque partie d’un tel programme est appelée un thread, et chaque thread définit un chemin d’exécution distinct. La programmation de socket multithread décrit qu’un serveur de socket multithread peut communiquer avec plusieurs clients en même temps dans le même réseau.

Dans notre cas pour rester simple nous allons utiliser la fonction select() et nous allons créer des sockets non bloquantes en servant de fonction setblocking() en lui donnant le paramètre FALSE. C’est à dire dans le cas précédent à partir de moment un client se connecte à notre serveur, le serveur reste bloqué/occupé avec cette client. Tant que ce client ne coupe pas la connexion un autre client ne peut pas se connecter au serveur car le serveur sera BLOQUÉ avec le client qui est actuellement connecté. Or avec la fonction select() on peut vérifier plusieurs socket c’est-à-dire vérifier si un client est connecté et qu’il est prêt pour l’échange et la fonction setblocking() évitequ’on soit bloqué avec un seul client jusqu’à qu’il parte. L’idée est donc d’accepter un certain nombre de connexion (sockets) avec client est de voir qui est prêt pour l’échange et de prendre un par un pour l’échange.Ainsi on sert tout le monde sans être bloqué.

Nous pouvons remarquer que nous désactivons le mode bloquante avec lsock.setblocking(False).Puis dans un premier temps nous enregistrons le socket qu’on veut vérifier avec sel.register() en le repérant avec selectors.EVENT_READ.

Ensuite nous allons créer une boucle avec with dans lequel nous allons récupérer les sockets qui sont disponible et retourné par events = sel.select(timeout=None) et ensuite de les traiter un par un. L’instruction précédant donne deux informations intéressantes sur les sockets disponibles. L’un c’est le « key » c’est l’identifiant de socket en quelque sorte et il contient aussi les informations sur les données que client veut envoyer. L’autre est « mask » qui donne les informations sur les opérations (écriture et lecture) qui sont prêt.

Ensuite nous avons une condition if qui traite deux cas :

  • nous avons le cas où le client se présente dans le socket d’écoute donc logiquement il n’aura pas de données à émettre alors on fait appel à la fonction accept_wrapper() pour considérer la demande de client. Cette fonction est assez similaire au programme serveur qu’on a vu avant. Seulement on ajoute conn.setblocking(False) pour le serveur ne soit pas bloqué avec un seul client et on crée un objet pour inclure les données qu’on veut envoyer utilisant types.SimpleNamespace. Et on ajoute events = selectors.EVENT_READ | selectors.EVENT_WRITE pour savoir si le client est prêt pour lecture ou écriture. Et la dernière ligne permet de enregistrer cette client qui est prêt pour l’échange.
  • Nous avons le cas le client est déjà connecté en fonction de des contenue des variable key et mask nous allons traiter deux cas avec une condition if  permettent de faire lecture et ecriture.Juste une diference dans le bloc de lecture c’est que si le client n’est pas prêt pour effectuer une lecture cela veut dire qu’il n’est plus présent donc le serveur ferme la connexion.

Du coté client :

#!/usr/bin/env python3

import sys
import socket
import selectors
import types
import os

sel = selectors.DefaultSelector()
messages = [b"Message 1 from client.", b"Message 2 from client.",b"Message 3 from client."]


def start_connections(host, port, num_conns):
    server_addr = (host, port)
    for i in range(0, num_conns):
        connid = i + 1
        print("starting connection", connid, "to", server_addr)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setblocking(False)
        sock.connect_ex(server_addr)
        events = selectors.EVENT_READ | selectors.EVENT_WRITE
        data = types.SimpleNamespace(
            connid=connid,
            msg_total=sum(len(m) for m in messages),
            recv_total=0,
            messages=list(messages),
            outb=b"",
        )
        sel.register(sock, events, data=data)


def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = None
        recv_data = sock.recv(22)  # Should be ready to read
        if recv_data:
            print("received", repr(recv_data), "from connection", data.connid)
            data.recv_total += len(recv_data)
            #file_obj = os.fdopen(sock.fileno())
            #file_obj.flush()
            # print('data.recv_total:',data.recv_total)
        if not recv_data or data.recv_total == data.msg_total:
            print("closing connection", data.connid)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if not data.outb and data.messages:
            data.outb = data.messages.pop(0)
        if data.outb:
            print("sending", repr(data.outb), "to connection", data.connid)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]


if len(sys.argv) != 4:
    print("usage:", sys.argv[0], "<host> <port> <num_connections>")
    sys.exit(1)

host, port, num_conns = sys.argv[1:4]
start_connections(host, int(port), int(num_conns))

try:
    while True:
        events = sel.select(timeout=1)
        if events:
            for key, mask in events:
                service_connection(key, mask)
        # Check for a socket being monitored to continue.
        if not sel.get_map():
            break
except KeyboardInterrupt:
    print("caught keyboard interrupt, exiting")
finally:
    sel.close()

Le programme coté client est un peu similaire que le serveur sauf au lieu d’écouter et attendre des connexions il commence par tenter une connexion via start_connections():

Il a deux fonctions start_connections() qui permet d’établir une connexion et service_connection() qui permet d’échanger.

Voici le résultat de l’exécution de ces programmes :

Figure 6 : résultat de l’exécution

Nous pouvons voir trois connexions client est on peut aussi voir que les message ont bien été reçu par le serveur.

3.Mise en place d’un client-serveur avec le langage GO

Voici programme d’un serveur qui communique avec plusieurs client au même temps :

package main

import (
        "bufio"
        "fmt"
        "net"
        "os"
        "strconv"
        "strings"
)

var count = 0

func handleConnection(c net.Conn) {
        fmt.Print(".")
        for {
                netData, err := bufio.NewReader(c).ReadString('\n')
                if err != nil {
                        fmt.Println(err)
                        return
                }

                temp := strings.TrimSpace(string(netData))
                if temp == "STOP" {
                        break
                }
                fmt.Println(temp)
                counter := strconv.Itoa(count) + "\n"
                c.Write([]byte(string(counter)))
        }
        c.Close()
}

func main() {
        arguments := os.Args
        if len(arguments) == 1 {
                fmt.Println("Please provide a port number!")
                return
        }

        PORT := ":" + arguments[1]
        l, err := net.Listen("tcp4", PORT)
        if err != nil {
                fmt.Println(err)
                return
        }
        defer l.Close()

        for {
                c, err := l.Accept()
                if err != nil {
                        fmt.Println(err)
                        return
                }
                go handleConnection(c)
                count++
        }
}

Dans la fonction main() on commence par vérifier si utilisateur a fourni un port sur lequel le serveur doit écouter.si l’utilisateur n’a pas fourni cette information on affiche une message d’erreur. Puis nous allons créer un listener avec la fonction net.Listen() qui prend en paramètre deux variable dont l’un est « tcp » qui est l’adresse IP ou le nom de serveur(cette variable récupère par défaut tous seul l’adresse IP ou le nom de serveur) et la deuxième est le port sur lequel il va écouter. En gros on ouvre un canal sur lequel on sera en écoute. Puis nous avons une boucle for qui permet à chaque fois qu’un client se présente de l’accepter c’est-à-dire établir une connexion avec lui. Ensuite on fait appel à notre fonction handleConnection() qui permet de communiquer avec notre client. Finalement c’est cette simple boucle for qui nous permet de traiter plusieurs clients.

Et notre fonction handleConnection() qui prend en paramètre une connexion client commence par un boucle for où nous avons deux conditions if. L’un vérifie si ’il y a quelque chose dans le buffer et s’il y a rien affiche un message d’erreur. L’autre vérifie si le client envoie le mot STOP afin de couper la connexion en cours et si c’est le cas le serveur coupe la connexion avec ce dernier.

Puis on a un bloc d’instruction qui permet d’écrire des réponses/donnés à notre client.

Du coté client :

package main

import (
        "bufio"
        "fmt"
        "net"
        "os"
        "strings"
)

func main() {
        arguments := os.Args
        if len(arguments) == 1 {
                fmt.Println("Please provide host:port.")
                return
        }

        CONNECT := arguments[1]
        c, err := net.Dial("tcp", CONNECT)
        if err != nil {
                fmt.Println(err)
                return
        }

        for {
                reader := bufio.NewReader(os.Stdin)
                fmt.Print(">> ")
                text, _ := reader.ReadString('\n')
                fmt.Fprintf(c, text+"\n")

                message, _ := bufio.NewReader(c).ReadString('\n')
                fmt.Print("->: " + message)
                if strings.TrimSpace(string(text)) == "STOP" {
                        fmt.Println("TCP client exiting...")
                        return
                }
        }
}

Du coté client nous avons dans notre fonction main un bloc d’instructions accompagné d’une condition if permet de contrôler grâce au variable arguments si l’utilisateur fourni bien ou pas le nom/adresse IP de serveur et le port sur lequel il écoute afin d’établir une connexion avec lui.si ce n’est pas le cas nous avons un message d’erreur qui affiche demandant de renseigner ces paramètres.

Puis on a un deuxième bloc qui permet d’établir la connexion avec notre serveur grâce au variable CONNECT qui récupère le contenue de argument[1] dont l’adresse IP et le port saisi par l’utilisateur et appelle le serveur et établie la connexion au serveur avec net.Dial().

Ensuite nous avons une boucle for qui a un premier bloc qui permet de lire ce que le client dont l’utilisateur saisi avec bufio.NewReader(os.Stdin) et ReadString().Et la foction Fprintf() permet d’envoyer ce que l’utilisateur a saisi au serveur.

Et finalement on le deuxième bloc d’instructions qui permet de lire et afficher la réponse de serveur.Et le boucle if dans ce bloc d’instruction permet de notifier avec la phrase « « TCP client exiting… » client» que qu’il a décidé de coupé la connexion avec le serveur parce qu’il a envoyé le mot clé « STOP ».

Voici ce que donne l’exécution de ces programmes :

Figure 7: l’exécution des programmes serveur/client GO

Nous pouvons constater que coté serveur il y a deux client sont connecté grâce au deux point et qu’il reçoit bien les messages de client 1 et deux.

4.Mise en place un serveur web multithrédé avec le langage Rust

Pour pouvoir créer un serveur web qui peut traiter plusieurs requêtes dont plusieurs client à la fois nous allons utiliser des threads.Un thread ou fil d’exécution ou tâche est similaire à un processus car tous deux représentent l’exécution d’un ensemble d’instructions du langage machine d’un processeur. Du point de vue de l’utilisateur, ces exécutions semblent se dérouler en parallèle. Toutefois, là où chaque processus possède sa propre mémoire virtuelle, les threads d’un même processus se partagent sa mémoire virtuelle. Par contre, tous les threads possèdent leur propre pile d’exécution.

Du coup nous devons créer un groupe de processus qui vont attendre une tache et vont exécuter les taches par processus une tache qui leur ont été confié. En gros nous allons créer un certain nombre de threads et imaginons qu’on crée 5.On peut donc traiter 5 requêtes clients au même temps.Donc il faut créer notre module threads dans un premier temps afin de pouvoir s’en servir dans le programme principale de serveur.

Voici le programme permettant de créer le module threads(src/lib.rs) :

use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

enum Message {
    NewJob(Job),
    Terminate,
}

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(Message::NewJob(job)).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        println!("Sending terminate message to all workers.");

        for _ in &self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }

        println!("Shutting down all workers.");

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv().unwrap();

            match message {
                Message::NewJob(job) => {
                    println!("Worker {} got a job; executing.", id);

                    job();
                }
                Message::Terminate => {
                    println!("Worker {} was told to terminate.", id);

                    break;
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

Puis nous allons créer notre programme principale de serveur(src/main.rs) :

use hello::ThreadPool;
use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!("{}{}", status_line, contents);

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Dans ce programme nous avons let listener = TcpListener::bind(« 127.0.0.1:7878 »).unwrap();qui permet de créer un listener (qu’on peut comparer à un socket) qui permet d’écouter/attendre pour une connexion TCP. Puis la boucle for permet d’ouvrir une connexion avec le client. « unwrap() » est une fonction similaire à null dans linux.Ensuite on appelle la fonction handle_connection().

La fonction handle_connection permet d’initier une connexion. Cette fonction prend en paramètre TcpStream qui permet de lire ce que le client envoie comme requête afin de pouvoir lui écrire la réponse adapté par le serveur. Dans cette fonction nous déclarons un buffer permettant de définir la taille de donné qu’on peut envoyer en une envoie. Puis on lit ce qu’il y a dans le buffer dont la requête de client.

Puis nous déclarons deux variable qui ont pour contenue le type de requête :

let get = b »GET / HTTP/1.1\r\n »; ⇒ Requête normal

let sleep = b »GET /sleep HTTP/1.1\r\n »; ⇒ Requête avec un délai d’attente.

Puis nous formulons en fonction de type de requête de client une réponse qui contient la réponse html et le fichier html qui va avec la réponse. Puis nous procédons à l’envoie de réponse. On converti la réponse (message + fichier html) en binaire et on envoie dans le buffer. Puis on vide le buffer.

5.Découverte de redis en tant que serveur de base de donnée

Redis est un système de stockage de données clé-valeur en mémoire, open source et rapide, pour une utilisation en tant que base de données, de cache, de courtier de messages et de file d’attente. Il stocke les données en mémoire dans un tableau comme un moteur NoSQL contrairement aux autres bases de données qui stockent sous forme de tables. C’est cette façon de stocker lui rend rapide.

Voici un test d’échange avec un serveur redis :

Figure 8 : Test d’un serveur Redis

Voici une utilisation possible de redis en tant qu’un serveur de base de données :

Nous allons utiliser la structure HASH. Ce sont des structures plus complexes permettant de stocker sous une même clé des noms de champs associés à des valeurs. Cela permet donc de stocker des objets simples. Ici dans l’exemple je vais stocker des informations d’une personne.

Figure 9 : Exemple d’utilisation

Je vais maintenant essayer de récupérer son nom :

Figure 10 : Exemple d’utilisation 2

Maintenant nous allons voir l’opération atomique dans redis qui permet garantir un accès exclusif à une ressource partagée.

La notion d’atomicité est une série d’opérations qu’on réalise sur une base de données qui ne peuvent pas être interrompues avant la fin de leur déroulement. Ce concept s’applique par exemple à une partie d’un programme dont le processus ou le thread qui la gère ne cédera pas le monopole sur certaines données à un autre processus pendant tout le déroulement de cette partie. Cela garantit la sécurité car les opérations réalisé par un client x ne peut pas être interrompue et vu par un autre client y.

Voici quelques exemples des opérations atomique que redis nous propose :

Ajouter une chaîne de caractère contiguë :

Figure 11 : Ajouter une chaîne de caractère contiguë

Incrémenter des valeurs avec la structure HASH :

Figure 12 :Incrémenter des valeurs avec la structure HASH

Créer une liste :

Figure 12 :Créer une liste

Les transactions :

Les transactions se font en utilisant les commandes MULTI, EXEC, DISCARD et WATCH. Elles permettent d’exécuter plusieurs commandes au même temps en série. Le fait d’exécuter sous forme de série évite qu’une transaction se fasse interrompre par une action d’un autre client.

On réalise une transaction en commençant par la commande MULTI et on termine/exécute la transaction avec la commande EXEC. Tant que l’utilisateur n’a pas saisi la commande EXEC à la fin de ces autres commandes la transaction ne s’effectuera pas. Cela évite qu’une transaction se déclenche sans qu’un client n’a pas saisi toutes les commande qu’il veut saisir. On peut imaginer cette situation lors d’une coupure de connexion internet chez ce client.

Tout de même une exécution d’une transaction peut retourner des erreurs pour des raisons d’erreur de syntaxe ou erreur d’utilisation de type de variable. Mais même lorsqu’une commande échoue, toutes les autres commandes de la file d’attente sont traitées. Cela peut paraître bizarre mais la raison c’est comme il peut y avoir que des erreurs de syntaxe qui peut donner lieu à une erreur de transaction et on suppose que les développeurs ne feront pas cette erreur dans une production.

La commande DISCARD est utilisé pour annuler une transaction par exemple si au lieu de EXEC on lance la commande DISCARD toutes les commande dans la file vont être annulé.

Finalement nous avons la commande WATCH qui permet d’éviter qu’il y est un conflit. C’est à dire imaginons qu’un client a et un client b essaye de changer la valeur d’une même clé. S’ils font leur modification avec la commande WATCH. Le contenue de variable ne changera pas et les deux clients obtiendrons une erreur. Cela permet de leur faire savoir qu’ils sont tous les deux entrains d’essayer de modifier une clé au même temps.

Conclusion

A travers ces différentes exemples nous avons pu découvrir et voir l’importance de l’environnement client-serveur. Nous avons plusieurs langages puissants qui permettent de programmer un serveur avec différentes fonctions. Nous avons pu voir le mode bloquante dans le cas d’un serveur (synchrone) qui traite un seul client à la fois et le mode non bloquante dans le cas d’un serveur (asynchrone) qui traite plusieurs clients à la fois. Nous avons vu les notions de socket/connecteur et listener qui permettent d’écouter puis d’établir une connexion entre un client et serveur. Nous avons également pu étudier un serveur de base de donnés nommé Redis qui est un serveur puisant, rapide et open source.On peut par exemple avec l’un des langages qu’on a étudié dans cette article, créer un serveur web qui peut traiter plusieurs clients et offrir des service http et de base de donnés avec redis.Donc tous ces pratiques permet de comprendre comment un serveur est construit et serve les clients.