Introduction à la programmation réseau (Sockets) – partie 2

48405289

Intro :

Pour concevoir un jeu vidéo multijoueur il vous faut utiliser dans le programme C++ des objets qu’on appelle sockets.

Ceci constitue la deuxième partie de l’ensemble des articles concernant les sockets.

Dans cette partie nous allons concevoir un petit chat en entrant du texte dans la console côté client et côté serveur.

Prérequis :

– Savoir un peu lire et écrire du C++

– Être sous Windows.

– Avoir suivi la première partie de ce tutoriel

Explications :

1 – Tout d’abord nous allons écrire un petit programme de chat par écrit à travers les entrées clavier de l’utilisateur dans la console.

2 – En deuxième partie, j’énumérerais l’ordre d’appel des fonctions sockets selon le rôle du programme (client ou serveur).

 


 

1 – Petit chat client / serveur

Voici le code du chat côté client (ChatClient.cpp) :

//----------------------------------------------------
// Auteur : Clément Profit
// Nom du fichier : ChatClient.cpp
// Date de création : Avril 2015
// Description : Une implémentation d'un client de chat
//----------------------------------------------------

#include <winsock2.h>

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>

#define PORT 23

// On lie la librairie des sockets Windows
#pragma comment(lib, "ws2_32.lib")

// Structure utilisée pour envoyer ces deux paramètres à la fonction
// thread ; étant donné que l'on ne peut qu'envoyer qu'un seul
// paramètre aux fonctions thread
struct ThreadParam
{
    bool bQuit;
    int server_sock;
};

// Message à envoyer au serveur
struct ChatMessage
{
    // Soit c'est un message texte, soit c'est une requête pour cloturer / quitter
    // la connexion
    enum MessageType {CHAT_MESSAGE, CHAT_QUIT};

    ChatMessage(MessageType message, char* information)
    {
        message_type = message;
        strcpy_s(sInfo, information);
    }

    ChatMessage() {};

    MessageType message_type;

    // On utilise un tableau de caractères à taille fixe
    // car on ne peut envoyer un pointeur char* sur une machine distante
    // (le pointeur étant lié uniquement à la mémoire de la machine émettrice)
    char sInfo[4096];
};

// Fonction pour obtenir une chaîne de caractères tappée
// depuis le clavier de l'utilisateur
char* get_string_input()
{
    char* message = new char[4096];

    // La fonction _getch() permet de tapper un caractère
    // sans l'afficher dans la console
    char ch = _getch();
    
    // Si on tappe la touche entrée on ne fait rien
    if (ch == 13)
    {
        return nullptr;
    }

    unsigned int length = 0;

    // On tappe un caractère par tour de boucle
    while (ch != 13) // Le caractère 13 est la touche entrée
    {
        message[length] = ch;
        length++;

        if (length > 4096)
        {
            break;
        }        
        
        ch = _getch();
    }

    // A la fin du message, on met un indicateur de fin de chaîne de caractère
    // pour finaliser la chaîne
    message[length] = '\0';

    return message;
}

// Thread pour entrer des messages à partir du clavier
DWORD WINAPI get_console_message(LPVOID lpParameter)
{
    // On fait un cast pour obtenir notre paramètre
    ThreadParam* param = (ThreadParam*)lpParameter;

    printf("Saisissez un message.\n");
    char* sMessage = nullptr;

    // A chaque tour de boucle correspond un message tappé
    while(true)
    {
        // On obtient le message tappé
        sMessage = get_string_input();

        if (sMessage == nullptr)
        {
            continue;
        }

        // Si l'on tappe "quit", le programme s'arrête
        if (strcmp(sMessage, "quit") == 0)
        {
            printf("Au revoir !\n");    

            system("pause");

            // On envoie un ChatMessage de sortie
            // pour indiquer d'arrêter le programme de la machine
            // distante
            ChatMessage quitMessage(ChatMessage::CHAT_QUIT, "");
            send(param->server_sock, (char*)&quitMessage, sizeof(quitMessage), 0);

            // Nous forçons aussi l'arrêt de notre programme sur la machine locale
            param->bQuit = true;

            break;
        };

        // On envoie un ChatMessage de texte corresopodant à celui tappé
        ChatMessage chatMessage(ChatMessage::CHAT_MESSAGE, sMessage);
        send(param->server_sock, (char*)&chatMessage, sizeof(ChatMessage), 0);

        printf("[Vous]# %s\n", sMessage);
    }

    delete sMessage;

    return 0;
}

/** Client **/
int main(void)
{
    // Il faut appeler ces deux instructions avant toute
    // utilisation de socket
    WSADATA WSAData;
    int error = WSAStartup(MAKEWORD(2,2), &WSAData);
 
    // Voici nos structures de socket
    SOCKET sock;
    SOCKADDR_IN server_in;

    // Si les sockets Windows fonctionnent
    if (!error)
    {
        // Création de la socket
        sock = socket(AF_INET, SOCK_STREAM, 0);
 
        // On passe en mode non bloqué, c'est-à-dire que les fonctions recv() et send()
        // ne bloquent pas quand un message n'a pas été envoyé ou reçu
        unsigned long mode = 1;
        ioctlsocket(sock, FIONBIO, &mode);

        // Configuration de la connexion
        // On peut changer ici l'adresse d'un serveur non local
        server_in.sin_addr.s_addr = inet_addr("127.0.0.1");
        server_in.sin_family = AF_INET;
        server_in.sin_port = htons(PORT);
 
        printf("Tentative de connexion au serveur (tappez exit pour quitter)...\n");

        // On se connecte au serveur
        error = connect(sock, (SOCKADDR*)&server_in, sizeof(server_in));

        if (error != SOCKET_ERROR)
        {
            printf("Connection à %s sur le port %d\n", inet_ntoa(server_in.sin_addr), htons(server_in.sin_port));
        }
    
        // Structure de paramètre pour envoyer
        // plus d'une variable à la fonction thread
        ThreadParam param;
        param.bQuit = false;
        param.server_sock = sock;

        // Création d'un thread pour effectuer la lecture d'un message indépendamment
        // de l'écoute par la fonction recv
        CreateThread(nullptr, 0, &get_console_message, (void*)&param, 0, nullptr);  

        ChatMessage* message = nullptr;

        // Comme les packets réseaux reçus peuvent être de différentes tailles
        // et qu'ils sont reçus par segments,
        // nous avons besoin d'un buffer secondaires pour lui ajouter
        // les segments par segments
        int iTotalBytes = sizeof(ChatMessage);
        int iCurrentBytes = 0;

        char* buffer = new char[iTotalBytes];
        char* pRecvBuffer = new char[iTotalBytes];

        while (!param.bQuit)
        {
            // On recoit des données qui peuvent être un segment
            int iBytes = recv(sock, buffer, iTotalBytes, 0);

            if (iBytes != SOCKET_ERROR)
            {
                // On rajoute le segment reçu dans le buffer à chaque fois
                // que des données ont été reçues plus haut
                memcpy(pRecvBuffer + iCurrentBytes, buffer, iBytes);
                iCurrentBytes += iBytes;

                // Dès qu'on a reçu le message au complet
                // c'est-à-dire dès que la taille du buffer a atteint
                // la taille du message
                if (iCurrentBytes >= iTotalBytes)
                {
                    message = (ChatMessage*) pRecvBuffer;

                    // Si l'on recoit un message qui nous indique de quitter
                    if (message->message_type == ChatMessage::CHAT_QUIT)
                    {
                        wprintf(L"Le serveur s'est deconnecté.\n");

                        system("pause");

                        // On quitte la boucle
                        break;
                    }

                    if (message->sInfo != '\0')
                    {
                        // On affiche le message reçu depuis le serveur
                        printf("[Serveur]# : %s\n", message->sInfo);
                    }    

                    iCurrentBytes = 0;
                }
            }
        }

        // On ferme la socket
        closesocket(sock);
 
        // Termine l'utilisation de Winsock 2
        WSACleanup();
    }

    return EXIT_SUCCESS;
}

 

Voici le code du chat côté serveur (ChatServeur.cpp) :

//----------------------------------------------------
// Auteur : Clément Profit
// Nom du fichier : ChatServeur.cpp
// Date de création : Avril 2015
// Description : Une implémentation d'un serveur de chat
//----------------------------------------------------

#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>

#define PORT 23

// On lie la librairie des sockets Windows
#pragma comment(lib, "ws2_32.lib")

// Structure utilisée pour envoyer ces deux paramètres à la fonction
// thread ; étant donné que l'on ne peut qu'envoyer qu'un seul
// paramètre aux fonctions thread
struct ThreadParam
{
    bool bQuit;
    int client_sock;
};

// Message à envoyer au serveur
struct ChatMessage
{
    // Soit c'est un message texte, soit c'est une requête pour cloturer / quitter
    // la connexion
    enum MessageType {CHAT_MESSAGE, CHAT_QUIT};

    ChatMessage(MessageType message, char* information)
    {
        message_type = message;
        strcpy_s(sInfo, information);
    }

    ChatMessage() {};

    MessageType message_type;

    // On utilise un tableau de caractères à taille fixe
    // car on ne peut envoyer un pointeur char* sur une machine distante
    // (le pointeur étant lié uniquement à la mémoire de la machine émettrice)
    char sInfo[4096];
};

// Si un erreur a lieu, on affiche la raison
// puis on quitte
void exit_on_fail(int sock_err)
{
    if (sock_err == SOCKET_ERROR)
    {
        printf("Socket Error Code = %i", WSAGetLastError());
 
        system("pause");
 
        exit(EXIT_FAILURE);
    }
}

// Fonction pour obtenir une chaîne de caractères tappée
// depuis le clavier de l'utilisateur
char* get_string_input()
{
    char* message = new char[4096];

    // La fonction _getch() permet de tapper un caractère
    // sans l'afficher dans la console
    char ch = _getch();

    // Si on tappe la touche entrée on ne fait rien
    if (ch == 13)
    {
        return nullptr;
    }

    unsigned int length = 0;

    // On tappe un caractère par tour de boucle
    while (ch != 13) // Le caractère 13 est la touche entrée
    {
        message[length] = ch;
        length++;

        if (length > 4096)
        {
            break;
        }        
        
        ch = _getch();
    }

    // A la fin du message, on met un indicateur de fin de chaîne de caractère
    // pour finaliser la chaîne
    message[length] = '\0';

    return message;
}

// Thread pour entrer des messages à partir du clavier
DWORD WINAPI get_console_message(LPVOID lpParameter)
{
    // On fait un cast pour obtenir notre paramètre
    ThreadParam* param = (ThreadParam*)lpParameter;

    printf("Saisissez un message.\n");
    char* sMessage = nullptr;

    // A chaque tour de boucle correspond un message tappé
    while(true)
    {
        // On obtient le message tappé
        sMessage = get_string_input();

        if (sMessage == nullptr)
        {
            continue;
        }

        // Si l'on tappe "exit", le programme s'arrête
        if (strcmp(sMessage, "quit") == 0)
        {
            printf("Au revoir !\n");
            system("pause");

            // On envoie un ChatMessage de sortie
            // pour indiquer d'arrêter le programme de la machine
            ChatMessage quitMessage(ChatMessage::CHAT_QUIT, "");

            send(param->client_sock, (char*)&quitMessage, sizeof(ChatMessage), 0);

            // Nous forçons aussi l'arrêt de notre programme sur la machine locale
            param->bQuit = true;
                
            break;
        };

        // On envoie un ChatMessage de texte correspondant à celui tappé
        ChatMessage chatMessage(ChatMessage::CHAT_MESSAGE, sMessage);
        send(param->client_sock, (char*)&chatMessage, sizeof(ChatMessage), 0);

        printf("[Vous]# %s\n", sMessage);
    }

    delete sMessage;

    return 0;
}

/** Serveur **/
int main(void)
{
    // Il faut appeler ces deux instructions avant toute
    // utilisation de socket
    WSADATA WSAData;
    int error = WSAStartup(MAKEWORD(2,2), &WSAData);

    SOCKET sock;
    SOCKET client_sock;

    // Voici nos structures de socket
    SOCKADDR_IN server_in;
    SOCKADDR_IN client_in;

    int recsize = sizeof(client_in);

    int sock_err;
 
    // Si les sockets Windows fonctionnent
    if (!error)
    {
        // Création de la socket
        sock = socket(AF_INET, SOCK_STREAM, 0);

        // On passe en mode non bloqué, c'est-à-dire que les fonctions recv() et send()
        // ne bloquent pas quand un message n'a pas été envoyé ou reçu
        unsigned long mode = 1;
        ioctlsocket(sock, FIONBIO, &mode);

        // Si la socket est valide
        if (sock != INVALID_SOCKET)
        {
            printf("Bienvenu sur le chat (tappez exit pour quitter) !\n\nLe serveur est maintenant ouvert en mode TCP/IP sur le port %d\n\n", PORT);
 
            // Configuration de la socket
            server_in.sin_addr.s_addr = htonl(INADDR_ANY);
            server_in.sin_family = AF_INET;
            server_in.sin_port = htons(PORT); 

            // On configure le serveur
            sock_err = bind(sock, (SOCKADDR*)&server_in, sizeof(server_in));

            exit_on_fail(sock_err);

            sock_err = listen(sock, 5);

            exit_on_fail(sock_err);

            printf("Patientez pendant que le client se connecte...\n");        

            // On boucle pour attendre la connexion d'un client
            while (true)
            {
                client_sock = accept(sock, (SOCKADDR*)&client_in, &recsize);

                // Si un client se connecte
                if (client_sock != SOCKET_ERROR)
                {
                    printf("\nUn client se connecte avec la socket %d de %s:%d\n", client_sock, inet_ntoa(client_in.sin_addr), htons(client_in.sin_port));
                    break;
                }
            }

            // Structure de paramètre pour envoyer
            // plus d'une variable à la fonction thread
            ThreadParam param;
            param.bQuit = false;
            param.client_sock = client_sock;

            // Création d'un thread pour effectuer la lecture d'un message indépendamment
            // de l'écoute de la socket
            CreateThread(nullptr, 0, &get_console_message, (void*)&param, 0, nullptr);

            ChatMessage* message = nullptr;

            // Comme les packets réseaux reçus peuvent être de différentes tailles
            // et qu'ils sont reçus par segments,
            // nous avons besoin d'un buffer secondaires pour lui ajouter
            // les segments par segments
            int iTotalBytes = sizeof(ChatMessage);
            int iCurrentBytes = 0;

            char* buffer = new char[iTotalBytes];
            char* pRecvBuffer = new char[iTotalBytes];

            while (!param.bQuit)
            {
                // On recoit des données qui peuvent être un segment
                int iBytes = recv(client_sock, buffer, iTotalBytes, 0);

                if (iBytes != SOCKET_ERROR)
                {    
                    // On rajoute le segment reçu dans le buffer à chaque fois
                    // que des données ont été reçues plus haut
                    memcpy(pRecvBuffer + iCurrentBytes, buffer, iBytes);
                    iCurrentBytes += iBytes;

                    // Dès qu'on a reçu le message au complet
                    // c'est-à-dire dès que la taille du buffer a atteint
                    // la taille du message
                    if (iCurrentBytes >= iTotalBytes)
                    {
                        message = (ChatMessage*) pRecvBuffer;

                        // Si l'on recoit un message qui nous indique de quitter
                        if (message->message_type == ChatMessage::CHAT_QUIT)
                        {
                            wprintf(L"Le client s'est deconnecté.\n");

                            system("pause");

                            // On quitte la boucle
                            break;
                        }

                        if (message->sInfo != '\0')
                        {
                            // On affiche le message reçu depuis le client
                            printf("[Client]# : %s\n", message->sInfo);
                        }

                        iCurrentBytes = 0;
                    }

                }
            }

            // On ferme la socket
            closesocket(sock);
        }
 
        WSACleanup();
    }

 
    return EXIT_SUCCESS;
}

 


 

2 – Ordre d’appel des fonctions socket selon le rôle de l’application (client ou serveur) :

OrdreTCP

 

Résumé :

Nous avons construit une application client / serveur pour communiquer en messagerie par le biais de la fenêtre console.

Références :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *