Archives pour la catégorie Réseaux

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 :

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

48405289

Intro :

Pour concevoir un jeu vidéo multijoueur il vous faut utiliser dans le programme C++ des objets qu’on appelle sockets. Ceci afin de pouvoir faire communiquer des ordinateurs à distance.

Prérequis :

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

– être sous Windows.

Explications :

Les sockets sont des objets représentants une interface de connexion liée à une machine afin de faire communiquer les ordinateurs à travers un réseau (a fortiori par Internet)

1 – Tout d’abord je vais vous présenter un petit programme qui affiche une page HTML dans la console à partir d’une URL web en utilisant les sockets. Rien ne vaut un bel exemple concret pour comprendre !

2 – Aussi, comme il est important de connaître de quoi l’on parle, je vais énumérer toutes les fonctions et mots-clés utilisés dans la programmation socket en C.

 


 

1 – Voici ce premier programme :

/** Inspirez-vous des commentaires pour comprendre **/

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

// Fonction qui permet d'afficher le contenu d'une page web
void FetchAndPrintPage(SOCKET socket, const char* sHost, const char* sPage)
{
    // Chaîne de caractère représentant la requête HTTP
    const char* sRequestFormat = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: FetchAndPrint.c\r\n\r\n";
    char* sMsg = new char[4096];

    // On compose les paramètres dans une chaîne de caractères finale
    // représentant la requête HTTP à envoyer
    sprintf(sMsg, sRequestFormat, sPage, sHost);

    // La fonction send() permet d'envoyer des informations vers
    // l'ordinateur distant (le serveur) ; ici on envoit la requête HTTP
    if (send(socket, sMsg, strlen(sMsg), 0) != SOCKET_ERROR)
    {
        // Comme la fonction recv(), qui suit, ne réceptionne qu'une partie du message
        // reçu, il faut l'invoquer en boucle jusqu'à ce que le message
        // est entièrement réceptionné.
        while (true)
        {
            // Le nombre d'octet reçu
            int iBytes;
            // Le tampon de caractères stockant le contenu de la page web
            char sBuffer[4096];

            // La fonction recv() ne réceptionne qu'une partie du message
            // reçu ; elle retourne le nombre d'octets reçu et stocke ce contenu
            // dans la variable tampon sBuffer
            iBytes = recv(socket, sBuffer, 4096, 0);

            // On sort de la boucle s'il n'y a plus de message à receptionner ou
            // qu'une erreur a eu lieu !
            if (iBytes == 0 && iBytes == -1)
            {
                break;
            }

            // Pour finaliser la chaîne on doit rajouter le caractère nul à celle-ci
            sBuffer[iBytes-1] = '\0';

            // On affiche le contenu de la page web dans la console
            printf("\n\n%s\n", sBuffer);
            // On met en pause le programme afin de lire la portion du texte reçu
            system("pause");
        }
    }
    else
    {
        printf("Socket Error Code = %i\n", WSAGetLastError());
    }

    delete sMsg;
}

int main()
{
    // Ces deux suivantes instructions initialisent
    // l'utilisation des sockets sous Windows
    WSADATA WSAData;
    WSAStartup(MAKEWORD(2,2), &WSAData);

    // Structure utilisée afin de stocker les informations d'un socket
    in_addr addr;

    const char* sHost = "anthroponaute.fr";

    // Cette fonction créé un socket
    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

    if (s == INVALID_SOCKET)
    {
        // Affiche la signification de l'erreur
        printf("Socket Error Code = %i\n", WSAGetLastError());

        system("pause");

        exit(EXIT_FAILURE);
    }
    
    // Pour obtenir des informations socket sur cette adresse web
    hostent* pHost = gethostbyname(sHost);

    if (pHost != nullptr)
    {
        printf("Adress Name = %s\n", pHost->h_name);

        addr.s_addr = *(u_long*) pHost->h_addr_list[0];

        printf("IP Address = %s\n", inet_ntoa(addr));
    }
    else
    {
        printf("Socket Error Code = %i", WSAGetLastError());

        system("pause");

        exit(EXIT_FAILURE);
    }

    // On remplit cette structure afin de configurer et situer notre socket
    SOCKADDR_IN adress_in;
    adress_in.sin_family = AF_INET;
    adress_in.sin_addr = addr;
    adress_in.sin_port = htons(80);

    /*************/

    // On se connecte sur le serveur web
    if (connect(s, (SOCKADDR*)&adress_in, sizeof(adress_in)) == SOCKET_ERROR)
    {
        printf("Socket Error Code = %i\n", WSAGetLastError());

        system("pause");

        exit(EXIT_FAILURE);
    }
    else
    {
        printf("Connected !\n");
    }
    // On affiche la page du site en spécifiant son répertoire
    FetchAndPrintPage(s, sHost, "/blog-informatique/");

    WSACleanup();

    return 0;
}

 

Copiez / collez et compilez !

 


 

2 – Voici maintenant tous les principes, concepts et fonctions de la programmation de sockets :

Voici les fichiers d’en-tête nécessaires :

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

 

Voici les #define préétablis :

SOCKET pour l'utilisation de socket (à la place de int)
SOCKADDR_IN pour struct sockaddr_in
SOCKADDR pour struct sockaddr
IN_ADDR pour struct in_addr
INVALID_SOCKET -1
SOCKET_ERROR -1

 

Voici la structure principale à utiliser :

struct sockaddr_in
{
    short              sin_family;
    unsigned short     sin_port;
    struct   in_addr   sin_addr;
    char               sin_zero[8];
};

Elle est utilisée pour configurer la socket.

 

Voici comment l’utiliser :

SOCKADDR_IN sin;
sin.sin_addr.s_addr = htonl(INADDR_ANY);   
sin.sin_family = AF_INET;
sin.sin_port = htons(23);

sin.sin_addr.s_addr : c’est l’adresse qu’on affecte à cette configuration de socket (c’est peut-être l’adresse représentant un client ou un serveur, cela dépend du rôle de notre socket)

INADDR_ANY : adresse IP automatique

Pour spécifier une adresse manuellement on peut utiliser la fonction inet_addr()

sin.sin_addr.s_addr = inet_addr("127.0.0.1");

sin.sin_family = AF_INET : spécifie que l’on passe la socket en mode Internet

sin.sin_port : spécifie le numéro de port de la socket à utiliser

En occurrence, qu’est-ce qu’un port ?

Un port sert à spécifier à quel service d’application les informations doivent être envoyées.

En effet sur le réseau Internet global, chaque ordinateur est représenté par un numéro d’IP, mais cette adresse IP ne spécifie pas à quelle application de l’ordinateur l’information doit être envoyé.

En conséquence les applications / services sont identifiés par des numéros de port (ex : port 80 pour
le service HTTP)

 

Ces deux fonctions doivent être appelées pour initialiser ou clôturer l’utilisation des sockets :

// Au début du programme :
WSADATA WSAData;
WSAStartup(MAKEWORD(2,2), &WSAData);

// A la fin du programme :
WSACleanup();

 

Pour initialiser une socket :

SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);

Voici les différents paramètres de cette fonction :

AF_INET // Spéficie que l'on va utiliser le mode Internet

Types de socket :

SOCK_STREAM  // Spéficie que l'on va utiliser le protocole TCP/IP. 

SOCK_DGRAM // Spéficie que l'on va utiliser le protocole UDP/IP.

– Le protocole TCP est un protocole dit connecté. Il contrôle si le paquet
est arrivé à destination si ce n’est pas le cas il le renvoie.

– Le procotole UDP est un protocole dit non connecté. A l’inverse du protocole TCP
il ne contrôle pas si le paquet est arrivé à destination.

 

Cette fonction permet de convertir une addresse IP (en char*) afin d’être
affectée à la structure IN_ADDR.

inet_addr("127.0.0.1");
inet_ntoa()
int connect(int socket, struct sockaddr* addr, socklen_t addrlen);
int bind(int socket, const struct sockaddr* addr, socklen_t addrlen);
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr* addr, socklen_t* addrlen);

Résumé :

Nous avons créé un programme qui affiche une page web afin de comprendre la programmation sockets.

Au final nous avons énuméré tous les concepts liés à la programmation sockets.

Références :

– http://pub.phyks.me/sdz/sdz/les-sockets.html