Autour du codeDevelopper toujours mieux
Posté le

Développer serveur asynchrone

Un serveur asynchrone est un serveur dont les entrées sont non bloquantes. Cela signifie que lorsque le serveur dialogue avec un client, il n'attend pas qu'un message arrive, si le message est là, il le traite sinon il va voir s'il a reçus le message d'un autre client. Ce type de serveur permet de gérer simultanément un grand nombre de connexions.

Pour développer un serveur, il nous faut des sockets

Socket est une API plutôt bas niveau qui permet de faire dialoguer plusieurs applications entre elles. Les applications peuvent se trouver sur des machines différentes la communication pourra alors se faire via les protocoles TCP ou UDP mais on peut également travailler au niveau de IP. Cette API est disponible en natif dans tout bon langage.

Choisir son protocole

Il existe plusieurs familles de protocoles, et on va s'intéresser à la famille IPv6. Avec IP, on a le choix entre TCP fiable et UDP non fiable TCP garantie que les paquets arriveront dans l'ordre d'envoie et qu'il n'y aura pas de perte, c'est un protocol connecté.Alors qu'UDP et non connecté il ne garantit rien mais est plus rapide. Un avantage d'UDP sur TCP et qu'il peut faire du multicaste (un seul envoie pour plusieurs destinataires), UDP et TCP utilise tout les deux le protocole IP pour faire leur besogne. Dans cet article, on va partir sur du TCP.

Pour créer une socket TCP/IPv6

int ma_socket = socket(AF_INET6, SOCK_STREAM, 0);

Pour UPD/IPv6

int ma_socket = socket(AF_INET6, SOCK_DGRAM, 0);

Créer un serveur TCP

Sur un serveur TCP, il y a une socket dédié à l'acceptation des connexions des clients.À chaque connexion acceptée, une socket est créer pour communiquer avec le client. Avant de pouvoir accepter des demandes de connexion, la socket doit être attachée à une adresse et un port. Faire un serveur TCP, demande 4 grandes étapes.

1 - Création de la socket du serveur

Le serveur doit avoir une socket dédiée à l'acceptation des connexions des clients.À chaque connexion acceptée, une autre socket sera créée pour communiquer avec le client.

2 - Lier la socket du serveur

Avant de pouvoir accepter des demandes de connexion, la socket doit être attachée à une adresse et un port. Cela se fait avec la fonction bind qui prend en paramètre une socket, et une structure contenant toutes les informations sur l'adresse.

3 - Marquer la socket pour accepter les demandes de connexions

Cette quatrième étape ce fait avec la fonction listen. Qui prend en paramètre la socket et le nombre maximum de sockets en attente d'acceptation cette valeur est limitée à 128. Depuis les noyaux linux 2.2 elle est fixé dans le fichier /proc/sys/net/core/somaxconn

4 - Accepter les connexions des clients

Cette dernière étape ce fait via la fonction accept. Elle prend en paramètre la socket puis à deux autres paramètres optionnels qui permettent de récupérer l'adresse de celui qui se connecte. La fonction accept va créer une nouvelle socket pour chaque connexion, on peut l'utiliser pour envoyer ou recevoir des messages via les fonctions send et recv. send prend en paramètre la socket nouvellement créée, une string contenant le message et le nombre d'octets à envoyer. recv quand à elle prend la socket, un tableau de caractère dans lequel copier les octets reçue et la taille de ce tableau.

La structure d'un serveur TCP est la suivante

L.1 Tant que le serveur tourne:
L.2     new_connect = accept(...)
L.3     requete = recv(new_connect, ...)
L.4     send(new_connect, "reponse")
L.5     close(new_connect, ...)

En temps normal, les instructions accepte et recv sont bloquante, cela signifie que l'on reste bloqué à la ligne 2 tant que l'on n'a pas de client qui se connecte. Dès qu'un client se connecte, on reste bloquer à la ligne 3 tant qu'il n'envoie rien et aucun autre client peut être traité.

Avec des socket non-bloquantes: accept retournera -1 s'il n'a pas reçu connexion et la variable errno sera mise à EAGAIN ou EWOULDBLOCK. Même chose pour recv. Pour avoir plusieurs connexions en simultanée, l'idée est la suivante. On créé un tableau de sockets. Lorsque accepte à créer une nouvelle connexion, on la met dans le tableau. On parcourt le tableau pour voir si un client à envoyé une requête, si oui, je lui renvoie une réponse. Après le close, je supprime la socket du tableau. Pour une gestion efficace du tableau de socket l'algo est le suivant. On garde dans une variable 'p' la position du dernier élément de notre tableau. Lors de l'ajout, on ajoute la socket à 'p' et on incrémente 'p', lors de la suppression, la socket à la position 'p' prend la place de la socket supprimer et 'p' est décrémenté de 1.

Un coup de pouce de la libraire poll

Plutot que de faire un recv et de tester la valeur de retour, les system POSIX.1-2001 mettent à disposition la libraire poll . Cette libraire contient une structure pollfd à laquelle on donne un socket et un événement sur lequel on veut être prévenue. Dans notre cas, on va utiliser l'événement POLLIN qui indique qu'il y a des données en attente de lecture. On constitue un tableau de structure puis on appelle la fonction poll sur ce tableau. La fonction poll va l'indiquer dans le champ revents de chaque structure si la socket a reçue un message. La fonction poll prend également en paramètre le nombre de milisecondes pendant lequel elle doit attendre qu'une socket reçoive. On peut mettre se paramètre à 0, mais donner un peu de temps peut permettre au CPU de se reposer.

Exemple

Le client va envoyer un premier mot attendre que le serveur lui envoie "thanx" puis renvoyer un deuxième mot et le serveur va renvoyer les 2 mots qu'il a reçu.

#include <sys/types.h>
#include <sys/socket.h> // socket, bind, listen, accept

// TCP/IP
#include <netinet/in.h>
#include <netinet/tcp.h>

// memset
#include <string.h>

// hton
#include <arpa/inet.h>

#include <unistd.h> // close, sleep
#include <fcntl.h>

#include <stdlib.h> // exit
#include <stdio.h>

#include <errno.h>

#include <poll.h> // Selection d'un fichier sur un evenement.

#define NB_CLIENT 64
int
main(int argc, char *argv[])
{

    int connection_socket;
    // Pour une socket non bloquante, on utilise SOCK_STREAM | SOCK_NONBLOCK depuis linux 2.6.27
    if ((connection_socket = socket(AF_INET6, SOCK_STREAM | SOCK_NONBLOCK , 0)) == -1) {
        perror(NULL);
    }

    // Creation d'une structure adresse pour lier la socket avec bind
    struct sockaddr_in6 my_addr, peer_addr;
    socklen_t peer_addr_size;

    memset(&my_addr, 0, sizeof(struct sockaddr_in6));
    my_addr.sin6_family = AF_INET6;   // Famille IPv6
    my_addr.sin6_addr = in6addr_any;  // Adresse IP
    my_addr.sin6_port = htons(10000); // Port
    my_addr.sin6_len = sizeof(my_addr);

    if (bind(connection_socket, (struct sockaddr *) &my_addr,
                                sizeof(my_addr)) == -1) {
        perror(NULL);
    }

    // Nb socket en attente d'acceptation
    if (listen(connection_socket, 5) == -1){
        perror(NULL);
    }

    struct pollfd peer_sockets[NB_CLIENT];
    for (int i=0; i < NB_CLIENT; ++i){
        peer_sockets[i].events = POLLIN ;
        peer_sockets[i].fd = -1;
    }
    int peer_socket;

    unsigned int i_last_socket = 0;
    char buffers[NB_CLIENT][64];
    int nb_char_receiveds[NB_CLIENT];


    while (1) {

        if ((peer_socket = accept(connection_socket,
                                   (struct sockaddr *) &peer_addr,
                                   &peer_addr_size)) == -1)
        {
            if (errno != EAGAIN && errno != EWOULDBLOCK)
            {
                perror(NULL);
                exit(1);
            }
         }
         else
         {
            // on rend la socket cliente non-bloquante
            // Puis on la stoque à la fin du tableau
            fcntl(peer_socket, F_SETFL, O_NONBLOCK);
            peer_sockets[i_last_socket++].fd = peer_socket;
         }

        int nb_events = poll(peer_sockets, i_last_socket, 0);
        for (int i=0; i < i_last_socket; ++i)
        {
            if (peer_sockets[i].revents & POLLIN)
            {
                if (nb_char_receiveds[i])
                {
                    nb_char_receiveds[i] += recv(peer_sockets[i].fd,
                                                 buffers[i] + nb_char_receiveds[i], 64, 0);
                    send(peer_sockets[i].fd, buffers[i], nb_char_receiveds[i], 0);
                    close(peer_sockets[i].fd);
                    if (i < --i_last_socket)
                    {
                        peer_sockets[i].fd = peer_sockets[i_last_socket].fd;
                        nb_char_receiveds[i] = nb_char_receiveds[i_last_socket];
                        memcpy(buffers[i], buffers[i_last_socket], nb_char_receiveds[i]);
                    }
                    else
                    {
                        peer_sockets[i].fd = -1;
                        nb_char_receiveds[i] = 0;
                    }
                }
                else
                {
                    nb_char_receiveds[i] = recv(peer_sockets[i].fd, buffers[i], 64, 0);
                    send(peer_sockets[i].fd, "thanx", 5, 0);
                }
            }
        }
    }
    close(connection_socket);
    return 0;
}

import socket
import errno
from time import time, sleep
HOST = "localhost"
PORT = 10000
socket_to_server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
socket_to_server.connect((HOST, PORT))
socket_to_server.send("Hello")
data = socket_to_server.recv(5) # Recevoir "thanx"
print(data)
socket_to_server.send(" Word")
data = socket_to_server.recv(64)
print(data)
socket_to_server.close()

Voilà ! Désolé si cet article est peut-être un peu moins fluide que les autres, j'espère que ça vous a plu quand même, n'hésitez pas si vous avez des remarques d'ici là a vos IDE et bon code ! laughing