Mis à jour le mercredi 16 mars 2016
  • 2 heures
  • Moyenne
Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Introduction du cours

Bonjour à tous, et bienvenue dans mon premier cours.

Nous allons voir les aspects techniques et quelques informations sur une librairie gratuite et open source sous licence MIT : LibNet.

Nous verrons le développement réseau via cette librairie en plusieurs sous-parties, ce qui nous permettra de mieux comprendre son fonctionnement et son utilité. À la fin de ce cours, vous saurez par exemple créer un tchat avec cette librairie !

Qu'est-ce que la librairie LibNet ?

Un peu d'histoire

LibNet est une librairie réseau développée par AlexMog dans le but de simplifier et de démocratiser la programmation réseau dans divers langages (principalement en C++, la seule version stable à l'heure où j'écris cet article, mais aussi en Java et en C#). La librairie est conçue pour être simple à utiliser, légère, stable, portable et rapide. Elle est pensée et développée dans un contexte ultra-objet (à la façon de Java) dans le but d'en simplifier au maximum l'utilisation. LibNet est conçue pour être la plus optimisée possible !

LibNet dispose d'un Objet Packet, qui permet de créer simplement un package de données à envoyer en réseau.

Features

LibNet est simple à installer (en utilisant CMake) et est compatible avec Windows, Mac OS X et Linux. Elle est aussi simple à utiliser et une documentation complète est disponible pour vous y aider. Les caractéristiques de LibNet sont les suivantes :

  • Disponibilité en plusieurs langages (C++ pour l'instant, mais un client – en Binary mode – existe en C#)

  • Utilisation de la norme C++99 pour une meilleure portabilité

  • Proximité des objets avec la JavaNet library (la librairie réseau de Java)

  • Gestion des sockets en TCP et UDP

  • Fournie avec un serveur TCP Asynchrone

  • Support pour une gestion de n'importe quel type de protocole (que ce soit texte ou binaire)

  • Gestion de package de données avec l'objet "Packet"

  • Disponibilité de plusieurs objets utilitaires (Mutex, CondVar, Thread, Selector...)

Description de quelques objets de la librairie

Nous allons voir ensemble les différents objets dont dispose la librairie pour créer un programme réseau, sans rentrer dans les détails pour l'instant : nous y reviendrons par la suite !

Objets les plus utilisés dans la librairie

Nom de l'objet

Fonction

CondVar

Encapsulation des variables conditionnelles.

Mutex

Encapsulation des mutex.

Packet

Permet la gestion du packing/depacking de données.

Singleton

Permet la génération de singleton simplifiée.

TcpASIOListener

Thread d'écoute du serveur ASIO.

TcpASIOWriter

Thread d'écriture du serveur ASIO.

TcpASIOServer

Permet la création d'un serveur ASIO.

TcpSocket

Adaptation de Socket en TCP.

Thread

Encapsulation des threads.

ITcpASIOListernerHandler

Interface permettant l'écoute du TcpASIOListener.

TcpServerSocket

 Adaptation de TcpSocket pour créer serveur.

Objets à utiliser en cas de besoin

Nom de l'objet

Fonction

IpAddress

Permet la conversion simple d'une adresse IP en int.

IRunnable

Interface permettant de créer un exécuteur pour les threads (Java-like).

LibNetworkException

Exception renvoyée par la lib en cas d'erreur.

Selector

Encapsulation du Select.

ThreadException

Exception renvoyée en cas d'erreur dans un thread.

OsSocket

Encapsulation des sockets en fonction du système d'exploitation.

La documentation

Pour finir cette première section, voici le lien de la documentation, qui va sûrement beaucoup vous servir ! :)

http://alexmog.labs-epimars.eu/projets/mognetwork-doc/doc/html/

Téléchargez et installez la librairie

Installer la librairie sur Linux

Commencez par aller récupérer les sources sur le GitHub du projet.

git clone https://github.com/AlexMog/LibNet.git

Il faut installer CMake pour que la compilation fonctionne.

sudo apt-get update
sudo apt-get install cmake

Ensuite, vous pouvez passer à l'installation de la librairie en elle-même ! Allez dans le dossier généré via Git, et créez un dossier build.

cd LibNet
mkdir build

Rendez-vous dans le dossier build que vous venez de créer, et lancez les commandes suivantes.

cd build
cmake ..

Vous venez de générer le Makefile que nous allons utiliser pour compiler et installer la librairie ! C'est bow ! 

make && sudo make install

Pouf ! Vous venez d'installer la librairie ! Bravo à vous.

Installer la librairie sur Windows

Pour Windows, c'est un peu plus complexe. Avant tout, il va nous falloir télécharger quelques dépendances.

  • Commencez par télécharger CMake pour Windows.

  • Téléchargez également MinGW, qui va nous permettre de compiler le tout (choisissez la version normale – au moment où j'écris cet article, c'est la version mingw-12.2).

  • Normalement, vous allez installer Git avec MinGW, ce qui est parfait !

Choisissez à présent un dossier temporaire pour télécharger les sources (où un dossier définitif si vous voulez les garder). Dans ce dossier, lancez Git Bash (clic droit > Git Bash).

Téléchargez la librairie.

git clone https://github.com/AlexMog/LibNet.git

Une fois la librairie téléchargée, dirigez-vous dans le dossier qui vient d'être créé et créez-y un dossier build.

cd LibNet && mkdir build && cd build

Créez le Makefile en "Unix Makefiles" grâce à CMake.

cmake .. -G "Unix Makefiles"

Compilez le tout !

make

Dans notre dossier build, nous avons à présent le fichier "libmognetwork.dll" et le fichier "libmognetwork-static.a".

Mais pourquoi la librairie s'appelle "lobmognetwork.dll" au lieu de "libnet.dll" ?

C'est simple, la librairie "libnet.dll" existe déjà sur certains Windows... il a fallu s'adapter !

J'ai une erreur sur CMake "PATH not set", que faire ?

Ajoutez le chemin vers le dossier bin de votre installation de MinGW dans la variable d'environnement PATH (Google vous aidera !).

Pour enfin pouvoir utiliser notre librairie complètement, il va falloir aussi récupérer les fichiers "Headers". Pour ce faire, on va se créer un petit "package" que nous pourrons utiliser pour tous nos projets.

Créez un nouveau dossier dans le dossier "LibNet".

cd .. && mkdir lib-package && cd lib-package && mkdir lib

Et copiez tout ce dont on a besoin pour développer sur la librairie, dans ce dossier.

cp ../include/ . -r && cp ../build/*.{dll,a} ./lib/

Et voilà, nous avons créé un package de développement ! Ce package est la base principale nous permettant d'utiliser la librairie. Nous allons l'installer dans MinGW pour pouvoir l'utiliser plus tard.

Pour cela, copiez le contenu du dossier "include" de notre package dans le dossier "{MINGW}\include\c++\{Version_de_C++}\" où {MINGW} représente le dossier d'installation de MinGW. Normalement, vous devriez avoir le dossier "mognetwork" dans le dossier "{MINGW}\include\c++\{Version_de_C++\" après cette manipulation.

Enfin, installez les exécutables de notre librairie qui se trouvent dans notre dossier "lib".

Copiez les fichier du dossier "lib" de notre package dans le dossier "{MINGW}\lib\". Normalement, nous devrions avoir "libmognetwork.dll" et "libmognetwork-static.a" dans le dossier "{MINGW}\lib\" après cette manipulation.

La librairie est maintenant prête à être utilisée !

Amusez-vous bien ! :)

Exemple : un serveur bloquant

Côté serveur

Nous allons créer un serveur bloquant qui affichera "Nouveau connecté" lorsque quelqu'un se connecte, et "déconnecté" lorsque quelqu'un se déconnecte.

Comme c'est un programme simple, nous n'utiliserons qu'un main.

Commençons par créer une ServerSocket, et la faire écouter sur un port.

#include <mognetwork/TcpServerSocket.hh>
#include <iostream>
#include <exception>

int main(void)
{
    // On va créer la socket serveur, et lui définir pour port d'écoute
    // Le port 4242
    mognetwork::TcpSocketServer server();
    
    try {
        server.bind(4242); // On bind la socket
        server.listen(); // On écoute sur la socket
    } catch (const std::exception& e) { // On capture et affiche les erreurs s'il y en a
        std::cerr << e.what() << std::endl;
        return (1);
    }
    
    return (0);
}

Nous allons utiliser ce code, le compiler, et voir ce qu'il se passe.

Pour le compiler, rien de plus simple :

g++ -o server_test main.cpp -lpthread -lmognetwork

Si vous souhaitez utiliser un Makefile, utilisez la méthode suivante.

NAME=	server_test

SRC=	main.cpp

OBJ=	$(SRC:.cpp=.o)

RM=		rm -rf

CXX=	g++

CXXFLAGS=	-Wall -Werror -W -Wextra #codons proprement!

LDFLAGS=	-lmognetwork

all:	$(NAME)

$(NAME):	$(OBJ)
			$(CXX) -o $(NAME) $(OBJ) $(LDFLAGS)

clean:
			$(RM) $(OBJ)

fclean:		clean
			$(RM) $(NAME)

re:			fclean all

.PHONY		re fclean clean all

Si vous lancez votre programme... rien ne se passe ! C'est normal, nous avons juste demandé au programme d'écouter sur le port et de s'arrêter.

Maintenant, attendons la connexion d'un client et fermons le serveur juste après :

#include <mognetwork/TcpServerSocket.hh>
#include <mognetwork/Selector.hh>
#include <iostream>
#include <exception>

int main(void)
{
    // On va créer la socket serveur, et lui définir pour port d'écoute
    // Le port 4242
    mognetwork::TcpSocketServer server(4242);
    // On crée un Selector pour savoir si nos clients se connectent/déconnectent
    mognetwork::Selector selector;
    
    std::cout << "Lancement du serveur sur le port 4242..." << std::endl;
    try {
        server.bind(); // On bind la socket
        server.listen(); // On écoute sur la socket
        // On ajoute la socket serveur au selector
        selector.addFdToRead(server.getSocketFD());
        // On attend que le selector repère la modification d'une socket
        selector.waitForTrigger();
        // Le select a sauté, il y a donc une nouvelle connexion !
        // On accepte le client
        TcpSocket* client = server.accept();
        if (client != NULL) // On vérifie qu'il n'y a pas eu d'erreur d'accept
        {
            std::cout << "Nouveau client connecté !" << std::endl;
            // On déconnecte le client
            client->disconnect();
            delete client;
        }
    } catch (const std::exception& e) { // On capture et affiche les erreurs s'il y en a
        std::cerr << e.what() << std::endl;
        return (1);
    }
    std::cout << "Serveur stoppé." << std::endl;
    return (0);
}

Très bien ! Maintenant, envoyons "ping" au premier client qui se connecte.

#include <mognetwork/TcpServerSocket.hh>
#include <mognetwork/Selector.hh>
#include <iostream>
#include <exception>

int main(void)
{
    // On va créer la socket serveur, et lui définir pour port d'écoute
    // Le port 4242
    mognetwork::TcpSocketServer server(4242);
    // On crée un Selector pour savoir si nos clients se connectent/déconnectent
    mognetwork::Selector selector;
    
    std::cout << "Lancement du serveur sur le port 4242..." << std::endl;
    try {
        server.bind(); // On bind la socket
        server.listen(); // On écoute sur la socket
        // On ajoute la socket serveur au selector
        selector.addFdToRead(server.getSocketFD());
        // On attend que le selector repère la modification d'une socket
        selector.waitForTrigger();
        // Le select a sauté, il y a donc une nouvelle connexion !
        // On accepte le client
        TcpSocket* client = server.accept();
        if (client != NULL) // On vérifie qu'il n'y a pas eu d'erreur d'accept
        {
            std::cout << "Nouveau client connecté !" << std::endl;
            // On envoie "ping" au client (j'insiste sur le \0 pour la taille des données)
            // Il faut que le \0 sois compris dans la taille
            client->send("ping\0", 4 + 1); // dans le cas présent, 4 = taille "ping" + 1 du \0
            // On déclare l'objet qui contiendra les données reçues (std::vector<char>)
            mognetwork::TcpSocket::Data data;
            // On attend la réponse du client
            client->receiveAll(data);
            // On affiche la réponse client
            std::cout << "Client : " << &(*data)[0] << std::endl; // oui, cette ligne est un peu barbare
            // On déconnecte le client
            client->disconnect();
            delete client;
        }
    } catch (const std::exception& e) { // On capture et affiche les erreur s'il y en a
        std::cerr << e.what() << std::endl;
        return (1);
    }
    std::cout << "Serveur stoppé." << std::endl;
    return (0);
}

Vous avez fait un serveur de ping ! Bravo ! :D

Côté client

Passons maintenant au client !

Petit makefile pour pouvoir travailler rapidement :

NAME=	client_test

SRC=	main.cpp

OBJ=	$(SRC:.cpp=.o)

RM=		rm -rf

CXX=	g++

CXXFLAGS=	-Wall -Werror -W -Wextra #codons proprement!

LDFLAGS=	-lmognetwork

all:	$(NAME)

$(NAME):	$(OBJ)
			$(CXX) -o $(NAME) $(OBJ) $(LDFLAGS)

clean:
			$(RM) $(OBJ)

fclean:		clean
			$(RM) $(NAME)

re:			fclean all

.PHONY		re fclean clean all

Passons donc au main :

#include <mognetwork/TcpSocket.hh>
#include <mognetwork/IpAddress.hh>
#include <iostream>
#include <exception>

int main(void)
{
    // On crée la socket client
    mognetwork::TcpSocket socket;
    // On crée l'adresse ip associée
    mognetwork::IpAddress ip("127.0.0.1");
    
    std::cout << "Connexion au serveur 127.0.0.1 sur le port 4242..." << std::endl;
    try {
        // On se connecte au serveur
        socket.connect(ip, 4242);
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return (1);
    }
    return (0);
}

Nous sommes connectés au serveur ! YOUHOU !

Maintenant, attendons qu'il nous envoie un message.

#include <mognetwork/TcpSocket.hh>
#include <mognetwork/IpAddress.hh>
#include <iostream>
#include <exception>

int main(void)
{
    // On crée la socket client
    mognetwork::TcpSocket socket;
    // On crée l'adresse IP associée
    mognetwork::IpAddress ip("127.0.0.1");
    
    std::cout << "Connexion au serveur 127.0.0.1 sur le port 4242..." << std::endl;
    try {
        // On se connecte au serveur
        socket.connect(ip, 4242);
        
        // On déclare de quoi stocker les données
        mognetwork::TcpSocket::Data data;
        // On attend un message du serveur
        socket.receiveAll(data);
        // On affiche ce message
        std::cout << "Le message m'a envoyé: '" << &(*data)[0] << std::endl;
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return (1);
    }
    return (0);
}

Parfait, nous allons répondre au serveur par "pong" à présent !

#include <mognetwork/TcpSocket.hh>
#include <mognetwork/IpAddress.hh>
#include <iostream>
#include <exception>

int main(void)
{
    // On crée la socket client
    mognetwork::TcpSocket socket;
    // On crée l'adresse IP associée
    mognetwork::IpAddress ip("127.0.0.1");
    
    std::cout << "Connexion au serveur 127.0.0.1 sur le port 4242..." << std::endl;
    try {
        // On se connecte au serveur
        socket.connect(ip, 4242);
        
        // On déclare de quoi stocker les données
        mognetwork::TcpSocket::Data data;
        // On attend un message du serveur
        socket.receiveAll(data);
        // On affiche ce message
        std::cout << "Le message m'a envoyé: '" << &(*data)[0] << std::endl;
        std::cout << "J'envois au serveur 'PONG'" << std::endl;
        // On envoie au serveur 'PONG'
        socket.send("PONG\0", 4 + 1); // Encore une fois, j'insiste sur le \0
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return (1);
    }
    return (0);
}

Et hop ! Notre client va bien répondre à notre serveur !

Accepter plusieurs clients sur le serveur

Reprenons notre serveur précédent : nous allons faire en sorte d'accepter plus de clients, et de laisser le serveur tourner constamment.

#include <mognetwork/TcpServerSocket.hh>
#include <mognetwork/Selector.hh>
#include <iostream>
#include <exception>

int main(void)
{
    // On va créer la socket serveur, et lui définir pour port d'écoute
    // Le port 4242
    mognetwork::TcpSocketServer server(4242);
    // On crée un Selector pour permettre de savoir si nos clients se connectent/déconnectent
    mognetwork::Selector selector;
    
    std::cout << "Lancement du serveur sur le port 4242..." << std::endl;
    try {
        server.bind(); // On bind la socket
        server.listen(); // On écoute sur la socket
        // On ajoute la socket serveur au selector
        selector.addFdToRead(server.getSocketFD());
        // On ajoute une boucle pour accepter plusieurs clients
        while (1)
        {
            // On attend que le selector repère la modification d'une socket
            selector.waitForTrigger();
            // Le select a sauté, il y a donc une nouvelle connexion !
            // On accepte le client
            TcpSocket* client = server.accept();
            if (client != NULL) // On vérifie qu'il n'y a pas eu d'erreur d'accept
            {
                std::cout << "Nouveau client connecté!" << std::endl;
                // On envoie "ping" au client (j'insiste sur le \0 pour la taille des données)
                // Il faut que le \0 soit compris dans la taille
                client->send("ping\0", 4 + 1); // dans le cas présent, 4 = taille "ping" + 1 du \0
                // On déclare l'objet qui contiendra les données reçues (std::vector<char>)
                mognetwork::TcpSocket::Data data;
                // On attend la réponse du client
                client->receiveAll(data);
                // On affiche la réponse client
                std::cout << "Client: " << $(*data)[0] << std::endl; // oui, cette ligne est un peu barbare
                // On déconnecte le client
                client->disconnect();
                delete client;
            }
        }
    } catch (const std::exception& e) { // On capture et affiche les erreurs si il y en a
        std::cerr << e.what() << std::endl;
        return (1);
    }
    std::cout << "Serveur stoppé." << std::endl;
    return (0);
}

De cette manière, nous acceptons un client à chaque nouvelle connexion. Néanmoins, c'est assez gênant de ne pas laisser au client le temps de faire des choses. Nous allons donc attendre que le client réponde par lui-même, sans couper l'accès aux autres clients.

#include <mognetwork/TcpServerSocket.hh>
#include <mognetwork/Selector.hh>
#include <iostream>
#include <exception>
#include <map>
#include <list>

int main(void)
{
    // On va créer la socket serveur, et lui définir pour port d'écoute
    // le port 4242
    mognetwork::TcpSocketServer server(4242);
    // On crée un Selector pour permettre de savoir si nos clients se connectent/déconnectent
    mognetwork::Selector selector;
    // On prépare une map pour stocker les TcpSocket de nos clients
    mognetwork std::map<SocketFD, mognetwork::TcpSocket*> clients;
    
    std::cout << "Lancement du serveur sur le port 4242..." << std::endl;
    try {
        server.bind(); // On bind la socket
        server.listen(); // On écoute sur la socket
        // On ajoute la socket serveur au selector
        selector.addFdToRead(server.getSocketFD());
        // On ajoute une boucle pour accepter plusieurs clients
        while (1)
        {
            // On attend que le selector repère la modification d'une socket
            selector.waitForTrigger();
            // Le select a sauté, il y a donc une modification de nos sockets !
            // On récupère la liste des sockets modifiées
            std::list<SocketFD> list = selector.getReadingTriggeredSocket();
            // On parcourt les sockets qui ont été modifiées
            for (std::list<SocketFD>::iterator it = list.begin();
                    it != list.end();)
                {
                    // On vérifie si c'est un nouveau client qui se connecte
                    if (*it == server.getSocketFD())
                    {
                        // On accepte le client
                        TcpSocket* client = server.accept();
                        if (client != NULL) // On vérifie qu'il n'y a pas eu d'erreur d'accept
                        {
                            std::cout << "Nouveau client connecté!" << std::endl;
                            // On envoie "ping" au client (j'insiste sur le \0 pour la taille des données)
                            // Il faut que le \0 soit compris dans la taille
                            client->send("ping\0", 4 + 1); // dans le cas présent, 4 = taille "ping" + 1 du \0
                            // On ajoute le client à la liste des sockets surveillées
                            selector.addFdToRead(client->getSocketFD());
                            clients[client->getSocketFD] = client;
                        }
                    }
                    else // C'est une des sockets client qui parle !
                    {
                        // On déclare l'objet qui contiendra les données reçues (std::vector<char>)
                        mognetwork::TcpSocket::Data data;
                        // On attend la réponse du client
                        (*it)->receiveAll(data);
                        // On affiche la réponse client
                        std::cout << "Client: " << $(*data)[0] << std::endl; // oui, cette ligne est un peu barbare
                        // On déconnecte le client
                        (*it)->disconnect();
                        // On le supprime des listes
                        selector.remFdToRead((*it)->getSocketFD());
                        clients.erase(*it);
                        delete *it;
                    }
                }
        }
    } catch (const std::exception& e) { // On capture et affiche les erreurs si il y en a
        std::cerr << e.what() << std::endl;
        return (1);
    }
    std::cout << "Serveur stoppé." << std::endl;
    return (0);
}

Et hop ! Voilà qui est fait. :)

Exemple : un serveur en ASIO

Définition de l'ASIO

L'ASynchrone Input/Output (AS-IO) est un modèle qui permet d'envoyer et de recevoir des données de manière non bloquante. Pour plus d'informations, je vous renvoie à la définition du terme sur Wikipédia (en anglais).

C'est la méthode la plus utilisée au niveau des serveurs.

Pourquoi ?

Vous allez voir, c'est logique ! Dans les cas que nous avons vu précédemment, nous récupérions des données via nos clients de manière simple, en lisant jusqu'à ce que les données soient reçues entièrement.

Le problème, c'est que lorsqu'un client ralentit pendant l'envoi de ces données, on peut rester longtemps à attendre la réception desdites données... et pendant ce temps, le serveur est bloqué ! Ainsi, si un autre client veut envoyer des données, il devra attendre son tour.

Pour pallier cela, il existe plusieurs méthodes mono-threadées et multi-threadées.

Dans un souci d'optimisation et de simplicité, LibNet utilise deux threads dans ses serveurs ASIO :

  • un thread d'écoute (TcpASIOListener) ;

  • et un thread d'envoi (TcpASIOWriter).

Nous allons donc voir comment créer un serveur ASIO qui fonctionnera très bien avec le client précédent.

C'est parti pour le serveur ASIO !

Nous devons créer notre main, et une seconde classe (que je mettrais dans le même fichier dans un souci de simplicité) qui sera notre listener.

#include <mognetwork/TcpASIOServer.hh>
#include <mognetwork/TcpASIOWriter.hh>
#include <stdio.h>
#include <iostream>
#include <exception>
#include <signal.h>

mognetwork::TcpASIOServer* server; // Oui, c'pas beau

// On définit un handler pour un arret avec CTRL+C pour fermer les Fds restés ouverts
void shandler(int)
{
  std::cout << "Stopping server..." << std::endl;
  server.stop();
}


// Notre listener !
class Listener : public mognetwork::ITcpASIOListenerHandler
{
public:
    Listener(mognetwork::TcpASIOWriter* writer) : m_writer(writer) {}
    void onConnect(mognetwork::TcpSocket& client)
    {
        // Un nouveau client se connecte !
        std::cout << "New client connected." << std::endl;
        // On lui envoie un petit ping de bienvenue !
        client.asyncSend("PING\0", 4 + 1); // encore une fois, j'insiste sur le \0
        // On prévient le thread d'envoi qu'il a de nouvelles données à envoyer !
        m_writer->triggerData();
    }
    
    void onReceivedData(mognetwork::TcpSocket& client)
    {
        // On a reçu de nouvelles données!
        // On récupère les données reçues
        mognetwork::TcpSocket::Data* data = client.getDatasReaded();
        // On les affiche
        std::cout << "RECEIVED: '" << &(*data)[0] << "'" << std::endl;
        // On supprime les données temporaires
        delete packet;
    }

    void onDisconnect(mognetwork::TcpSocket& client)
    {
        // Un client s'est déconnecté !
        std::cout << "Client disconnected." << std::endl;
    }
  
private:
  mognetwork::TcpASIOWriter* m_writer; // On garde un pointeur sur notre thread d'écriture
};

int main(void)
{
    // On initialise le serveur en lui donnant pour port découte 4242
    // Et en définissant un envoi en mode Binary
    // (voir la documentation pour plus d'informations)
    mognetwork::TcpServer server_init(4242, mognetwork::TcpServer::Binary);
    server = &server_init;
    // On initialise notre listener avec le serverWriter du serveur
    Listener l(server->getServerWriter());

    // On capture les CTRL+C
    signal(SIGINT, shandler);
    std::cout << "Starting server..." << std::endl;
    try {
        // On ajoute le listener au serveur
        server->addListener(&l);
        // On allume le serveur !
        server->start();
        std::cout << "Server ended." << std::endl;
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return (1);
    }
  std::cout << "Finish." <<std::endl;
  return (0);
}

Et voilà notre serveur ASIO de ping pong !

TP : un serveur de tchat

Maintenant que nous savons comment fonctionne la librairie, nous allons nous lancer dans la création d'un tchat !

Instructions

Le but de ce TP est de créer un serveur de tchat simple. Voici son fonctionnement basique : un client se connecte → envoie une chaîne de caractères contenant son pseudo → peut commencer à envoyer des chaînes de caractères représentant les messages. 

Vous allez devoir trouver une méthode simple pour le tchat en utilisant les possibilités de la librairie. Envoyez les chaînes de caractères via un serveur ASIO.

Le client

Pour le client, on va faire un client très simple qui prend 3 arguments. Voici l'usage : ./client <server_ip> <server_port> <username>.

Le client doit ensuite écouter le serveur pour recevoir les messages de celui-ci, tout en écoutant l'entrée standard pour savoir si l'utilisateur envoie un message.

Évitez de créer 2 threads, et utilisez l'objet Selector, qui est parfait pour ça !

Le serveur

Le serveur est très simple, puisqu'il est basé sur la classe TcpASIOServer.

Au final, il faudra juste gérer une liste d'utilisateurs, et renvoyer à tous les utilisateurs les messages reçus par le serveur.

Voici l'usage : ./server <port_d_ecoute>.

À vos lignes de code ! 

Correction du TP

Alors, ça y est ? Vous avez terminé votre serveur de tchat ? Je vais vous montrer mon code. :)

Le serveur

La conception

Pour le serveur, j'ai choisi d'être très simpliste: celui-ci ne fera aucune vérification, il se contentera de renvoyer le message de quelqu'un à tout le monde.

Voici les fichiers que j'ai utilisé pour ce TP :

  • Server.cpp et Server.hh : ils permettent la définition et l'exécution du core du serveur. Le serveur va gérer les clients ainsi que l'envoi et la réception des informations.

  • Client.cpp et Client.hh : ils permettent la définition des informations client. Notez que le core contient une liste de clients.

Passons au code, et tout d'abord à la définition de nos .hh.

Server.hh

#include <mognetwork/TcpASIOServer.hh>
#include <mognetwork/ITcpASIOListenerHandler.hh>
#include <list>
#include "Client.hh"

class Server : public mognetwork::TcpASIOServer, public mognetwork::ITcpASIOListenerHandler
{
public:
  Server(int port);

private:
  /* Permet d'envoyer un packet à tous les clients connectés */
  void sendDatasToAllClients(mognetwork::Packet& datas);

public:
  /* Lorsqu'un client se connecte... */
  void onConnect(mognetwork::TcpSocket& client);
  /* Lorsqu'un client envoie des données... */
  void onReceivedData(mognetwork::TcpSocket& client);
  /* Lorsqu'un client se déconnecte... */
  void onDisconnect(mognetwork::TcpSocket& client);

private:
  std::list<Client*> m_socketList; // liste pour garder les clients connectés
};

Client.hh

#include <mognetwork/TcpSocket.hh>
#include <string>

class Client : public mognetwork::TcpSocket
{
public:
  Client(mognetwork::TcpSocket& client);

public:
  /* Permet de savoir si le client a été authentifié */
  bool isAuthed() const;
  /* Récupère le pseudo du client */
  const std::string& getUsername() const;

public:
  /* Définit le pseudo du client */
  void setUsername(char* username);

private:
  /* On garde le pseudo du client */
  std::string m_username;
};

Voilà, nous savons ce que nous voulons dans nos classes de gestion.

Il faut maintenant créer un main basique pour tester tout ça.

#include <iostream>
#include <exception>
#include <cstdlib>
#include <signal.h>
#include "Server.hh"

Server* server = NULL;

// Fonction utilisée par le signal pour clean le serveur
static void shandler()
{
    if (server != NULL) {
        server->stop();
        delete server;
    }
}

int main(int ac, char **av)
{
  if (ac < 2) // On vérifie le nombre d'arguments
    {
      std::cout << "Usage: " << av[0] << " <port>" << std::endl;
      return 0;
    }
    // On capture les CTRL+C histoire de nettoyer le serveur
  signal(SIGINT, shandler);
  server = new Server(std::atoi(av[1])); // On crée le serveur avec le port d'écoute

  try {
    std::cout << "Ecoute du serveur..." << std::endl;
    server.start(); // On lance le serveur...
  } catch (const std::exception& e) { // On récupère les erreurs sous forme d'exception
    std::cerr << e.what() << std::endl;
    return (1);
  } catch (...) { // On récupère les autres types d'erreurs (non exception) (ce cas est très rare)
    std::cerr << "Error but no exception found?!" << std::endl;
    return (2);
  }
  return (0);
}
Le code

Et maintenant, passons aux fichiers .cpp ! 

Server.cpp

#include <iostream>
#include <mognetwork/Packet.hh>
#include "Server.hh"

Server::Server(int port) : mognetwork::TcpASIOServer(port)
{
  std::cout << "Initialisation du serveur sur le port " << port << "..." << std::endl;
  this->addListener(this); // Oui, le serveur est son propre listener :)
}

void Server::onConnect(mognetwork::TcpSocket& client)
{
  std::cout << "Nouveau client connecté!" << std::endl;
  m_socketList.push_back(new Client(client)); // On ajoute le nouveau client dans la liste
}

void Server::onReceivedData(mognetwork::TcpSocket& client)
{
    // On cherche quel client enregistré a envoyé le message
  for (std::list<Client*>::iterator it = m_socketList.begin();
       it != m_socketList.end(); ++it)
    {
      if (client.getSocketFD() == (*it)->getSocketFD())
    {
      // On récupère les données lues
      mognetwork::Packet* packet = client.getPacketReaded();
	  char buffer[512];

      // On sait que la première donnée est un char*, on la récupère
	  (*packet) >> buffer;
	  std::cout << "Un client a envoyé: '" << buffer << "'" << std::endl;
	  if ((*it)->isAuthed()) // On vérifie que le client est authentifié
	    {
	      std::cout << "\tLe client était authentifié. Envois aux autres clients..." << std::endl;
	      packet->clear(); // On vide le packet dans le but de le re-remplir
	      (*packet) << (*it)->getUsername().c_str(); // On ajoute le pseudo en première donnée
	      (*packet) << buffer; // On ajoute le texte en seconde donnée
	      sendDatasToAllClients(*packet); // On envoie le packet à tout le monde
	    }
	  else
	    {
	      std::cout << "\tLe client n'était pas authentifié. Utilisation comme pseudo..." << std::endl;
	      (*it)->setUsername(buffer);
	    }
	  delete packet; // On vide le cache des données
	  break;
	}
    }
}

void Server::onDisconnect(mognetwork::TcpSocket& client)
{
  std::cout << "Un client c'est déconnecté." << std::endl;
// On supprime le client de la liste des clients connectés
  for (std::list<Client*>::iterator it = m_socketList.begin();
       it != m_socketList.end();)
    {
      if (client.getSocketFD() == (*it)->getSocketFD())
	{
	  delete *it;
	  it = m_socketList.erase(it);
	}
      else
	++it;
    }
}

void Server::sendDatasToAllClients(mognetwork::Packet& datas)
{
    // On parcourt la liste pour envoyer les données à tous les clients
  for (std::list<Client*>::iterator it = m_socketList.begin();
       it != m_socketList.end(); ++it)
    (*it)->asyncSend(static_cast<const char*>(datas.getData()), datas.getDataSize());
  sendPendingDatas(); // On réveille le thread d'envoi
}

Client.cpp

Au vu de sa simplicité, je ne me suis pas attardé sur les commentaires. :)

#include <iostream>
#include "Client.hh"

Client::Client(mognetwork::TcpSocket& client) : mognetwork::TcpSocket(client)
{
  std::cout << "Création du nouveau client" << std::endl;
}

bool Client::isAuthed() const
{
  return !m_username.empty();
}

const std::string& Client::getUsername() const
{
  return m_username;
}

void Client::setUsername(char* username)
{
  m_username = username;
}

Voilà ! Nous avons notre serveur, il nous reste à le compiler:

g++ *.cpp -lmognetwork -lpthread

Passons au client !

Le client

Introduction

Bon, c'est bien beau d'avoir créé un serveur en ASIO ! Il est temps de discuter avec lui via un client !

Pour ce client, nous allons utiliser la classe TcpSocket pour l'envoi et la réception des données, ainsi que la classe Selector.

Le client ne sera pas en ASIO. Néanmoins, il est possible d'utiliser TcpASIOListener et TcpASIOWriter de manière à écouter sur une TcpSocket classique.

Bon ! Passons à l'explication !

Comment le client va marcher ?

Pour le fonctionnement du client, j'ai choisi le mono-threading, et le format synchrone.

Pourquoi ?

Eh bien, nous allons faire un client de tchat en console, le transfert des données sera rapide, et un simple tchat dans ce genre n'a pas forcément besoin d'être ASIO.

Pourquoi le serveur est ASIO dans ce cas ?

Les deux cas sont très différents. Par définition, un serveur doit pouvoir gérer plusieurs clients : si un des clients plante, et que le serveur n'est pas en ASIO, il attendra la fin de l'envoi du message à ce client, or le client le fera attendre indéfiniment, ou encore ralentir ! Et comme tout le monde le sait, la règle d'or, c'est que le serveur (ou le service) ne doit pas planter ! Ainsi, le choix de l'ASIO s'impose dans pratiquement tous les cas pour un serveur.

Bon, faisons bref : le tout, pour éviter le multi-threading, c'est d'utiliser l'objet Selector pour le client.

Ainsi, pour savoir qui discute avec nous, nous utiliserons les FD. 11

Vous avez compris ? Go go go! Let's rock! :soleil:

Correction du client

C'est parti pour la correction du client !

Le code ci-dessous est très basique, mais largement fonctionnel pour ce qu'on souhaite faire.

#include <mognetwork/TcpSocket.hh>
#include <stdlib.h>
#include <exception>
#include <list>
#include <iostream>

int main(int argc, char **argv) {
    if (argc < 4) {
        std::cout << "Usage: " << argv[0] << " <server> <port> <username>"
            << std::endl;
        return 1;
    }
    
    mognetwork::TcpSocket socket;
    mognetwork::IpAddress ip(av[1]);
    mognetwork::Selector selector();
    
    // On surveille l'entrée standard.
    selector.addFdToRead(0);
    // On surveille la socket principale.
    selector.addFdToRead(socket.getSocketFD());
    
    try { 
        socket.connect(ip, atoi(av[2]));
        while (1) {
            // On attend qu'un des FD qu'on surveille saute.
            selector.waitForTrigger();
            // On récupère les FD modifiés.
            std::list<SocketFD> triggeredFds = selector
                                                .getReadingTriggeredSockets();
            // On parcours les FD modifiés
            for (std::list<SocketFD>::iterator it = triggeredFds.begin();
                it != triggeredFds.end(); ++it) {
                    char message[512];
                    // On vérifie si c'est l'entrée standard qui a été modifiée
                    if ((*it)->getSocketFD() == 0) {
                        // Dans le cas présent, on lit l'entrée standard,
                        // et on envoie le message.
                        
                        // On lit l'entrée standard
                        std::cin >> message;
                        // On construit le packet
                        mognetwork::Packet packet();
                        packet << message;
                        // On envoie les données
                        socket.sendDatas((char*)packet.getData(),
                            packet.getDataSize());
                    } else {
                        // Sinon, c'est la socket, donc nouveau message!
                        socket.receiveAll(datas) == mognetwork::Socket::Ok;
                         // Sera utilisé pour la réception des données
                        std::vector<char>* datas = new std::vector<char>();
                        // On convertit les données en packet
                        mognetwork::Packet packet(datas);
                        // On récupère le pseudo et message
                        char pseudo[40];
                        packet >> pseudo;
                        packet >> message;
                        // On affiche le tout
                        std::cout << pseudo << ": " << message << std::endl;
                    }
                }
        }
        std::cout << "Disconnected by the server." << std::endl;
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return 2;
    }
    return 0;
}

Et voilà, vous avez votre client !

  

C'était une courte introduction à la LibNet ! J'espère que vous en ferez bon usage dans vos projets. :) N'hésitez pas à m'envoyer un MP si vous avez des questions.

À très bientôt !

Exemple de certificat de réussite
Exemple de certificat de réussite