Developpez.com - 2D - 3D - Jeux
X

Choisissez d'abord la catégorieensuite la rubrique :


SDLNet

Date de publication : 12/03/2007 , Date de mise à jour : 12/03/2007

Par Jean Christophe Beyler (Autres Articles)
 

L'écriture d'un programme réseau est toujours compliqué. Il y a souvent beaucoup de cas à gérer et si cela est mal fait, cela rend le tout totalement instable. La bibliothèque SDL_Net étend la bibliothèque SDL pour rendre le code gérant le réseau portable et facile. Cet article présente cette bibliothèque.

               Version PDF   Version hors-ligne

1. Introduction
2. Le premier exemple
2.1. Le côté serveur
2.2. Le client
3. Le deuxième exemple
3.1. Le serveur
3.2. Le client
4. Conclusion


1. Introduction

L'écriture d'un programme qui doit envoyer des messages sur le réseau est toujours assez complexe. La gestion des erreurs est, par exemple, un des points les plus difficiles à gérer. Cet article présente l'API de la bibliothèque SDLNet parce qu'elle est portable et vraiment facile d'utilisation.

Le plan de cet article montre deux exemples d'utilisation de la bibliothèque. Le premier exemple crée un serveur qui attend la connexion d'un client. Lorsqu'un client se connecte, le serveur pourra envoyer des messages au client connecté. Le client ne fera rien d'autre que d'afficher les messages du serveur.

Le deuxième exemple, permettra d'avoir plusieurs clients qui se connectent et le serveur envoit le message à chaque client. Les clients pourront aussi se déconnecter et se reconnecter. Pour des raisons de simplicité, le serveur aura un maximum de clients qui pourront être connectés à un instant donné.

Enfin, avant de commencer l'explication du code, il faut rappeler qu'il existe deux grands protocoles de communications. TCP permet d'envoyer des messages sans se soucier de savoir si le message va arriver à destination puisque le protocole TCP l'assure. Il assure aussi le fait que les messages arriveront dans l'ordre des envois. Le deuxième grand protocole est appelé UDP et laisse le soin au programmeur d'assurer le niveau de sécurité voulu. Cela veut dire qu'avec de l'UDP, les messages peuvent se perdre et peuvent arriver dans un ordre différent de l'envoi. Par contre, le protocole UDP est plus léger que le protocole TCP.

Il va donc sans dire que l'utilisation du protocole TCP est nettement plus aisé pour le programmeur que le protocole UDP. Par contre, il est plus lourd pour le réseau, c'est pour cela que les jeux utilisent généralement les deux protocoles. Le protocole TCP est utilisé pour les informations importantes et le protocole UDP pour les informations moins importantes.

Enfin, cet article n'est en rien une présentation des concepts théorique de la programmation réseau.


2. Le premier exemple

Ce premier exemple montre comment un client peut se connecter à un serveur et recevoir les messages qu'envoit le serveur. Ceci est fait en utilisant le protocol de connexion TCP.


2.1. Le côté serveur

Nous allons garder le code très simple pour ce premier exemple. Premièrement, nous avons les inclusions suivantes :
Les inclusions
#include <stdlib.h>
#include <SDL.h>
#include <SDL_net.h>
Le code du côté serveur est inclus entièrement dans le main. Nous avons besoin de quelques variables locales avant d'initialiser la SDL et la SDL_Net.
Les inclusions
int main(void)
{
    IPaddress ip, *ipadd;
    TCPsocket tcpserveur, tcpsocket;
    char buf[1024];
    unsigned int result;

    if(SDL_Init(SDL_INIT_TIMER) != 0) {
        fprintf(stderr, "Erreur d'initialisation\n");
        return EXIT_FAILURE;
    }

    if(SDLNet_Init() != 0) {
        fprintf(stderr, "Erreur d'initialisation de SDL_Net\n");
        return EXIT_FAILURE;
    }
Vous remarquerez que nous n'initialisons que le module Timer parce que nous allons utiliser la fonction SDL_Delay plus tard. Par contre, nous ne ferons pas de graphisme tout de suite donc ce n'est pas utile de tout initialiser.

Ensuite, nous voulons créer un serveur. Il va falloir passer par un socket de type serveur. SDL_Net permet d'en créer en utilisant la fonction SDLNet_TCP_Open. La technique la plus facile pour le mettre en place est d'utiliser SDLNet_ResolveHost pour initialiser correctement la structure que nous passerons à SDLNet_TCP_Open.

Voici la signature de la fonction SDLNet_ResolveHost :
Le prototype de la fonction SDLNet_ResolveHost
int SDLNet_ResolveHost(IPaddress *address, const char *host, Uint16 port);
Cette fonction remplit donc la structure de type IPaddress avec les informations nécessaires. Il faut savoir que nous allons utiliser les mêmes fonctions que ce soit le code du serveur ou le code du client. Dans le cas d'un serveur comme ici, le deuxième argument sera NULL, sinon, dans le cas d'un client, nous pouvons passer le nom de la machine ou l'IP sous la forme d'une chaîne de caractères. Enfin, le dernier argument est le port pour la connexion, nous demanderons le port 12345 pour ce programme.
La préparation de la connexion
    /* Creation du serveur */
    if(SDLNet_ResolveHost(&ip,NULL,12345) != 0) {
        printf("SDLNet_ResolveHost: %s\n", SDLNet_GetError());
        return EXIT_FAILURE;
    }

    tcpserveur = SDLNet_TCP_Open(&ip);

    if(tcpserveur == NULL) {
        fprintf(stderr, "Erreur d'initialisation de la socket\n");
        return EXIT_FAILURE;
    }
Comment toujours, il faut toujours vérifier le retour des fonctions. Ceci est particulièrement vrai dans un programme réseau.

Une fois le serveur mis en place, il faut accepter une connexion d'un client. Ceci se fait en utilisant la fonction suivante :
Le prototype de la fonction SDLNet_TCP_Accept
TCPsocket SDLNet_TCP_Accept(TCPsocket server);
Un grand avantage de cette fonction est qu'elle est non bloquante. Cela veut dire que la fonction retourne une socket d'un client ou NULL. Si jamais la fonction retourne NULL, cela veut dire qu'il y a eu une erreur ou qu'aucun client veut se connecter.

Nous tentons donc une première connexion, s'il n'a pas de client alors nous rentrons dans une boucle qui attendra un client en faisant une pause d'une seconde entre chaque essai.
La connexion d'un client
    /* On accepte un client */
    printf("En attente\n");
    tcpsocket = SDLNet_TCP_Accept(tcpserveur);
    while(tcpsocket == NULL) {
        SDL_Delay(1000);
        tcpsocket = SDLNet_TCP_Accept(tcpserveur);
    }
Une fois que nous sommes à ce point du code, nous avons un client en attente. Nous allons affiché son adresse IP ou le nom de l'ordinateur distant. Ceci se fait avec la fonction SDLNet_TCP_GetPeerAddress. Comme toujours, cette fonction peut retourner NULL si jamais il y a une erreur.
Récupération des informations du client
    ipadd = SDLNet_TCP_GetPeerAddress(tcpsocket);

    if(ipadd != NULL) {
        printf("Client connecte : %s\n", SDLNet_ResolveIP(ipadd));
    }
Une fois que la connexion est établie, on met en place une boucle récupérant l'entrée utilisateur. Ensuite, si la ligne n'est pas vide, on envoit le message au client. Pour simplifier le code, nous envoyons tout le tampon, même si une infime partie est utilisée.

Voici le prototype de la fonction d'envoi est nommée SDLNet_TCP_Send :
Le prototype de la fonction SDLNet_TCP_Accept
int SDLNet_TCP_Send(TCPsocket sock, void *data, int len);
Cette fonction prend en argument un pointeur vers la socket TCP, le pointeur des données à envoyer et la longueur en octet des données. Enfin, cette fonction retourne le nombre d'octets qui sont envoyés.
Récupération des informations du client
    while(fgets(buf, sizeof(buf), stdin) != NULL) {
        /* Si c'est une ligne vide */
        if(strlen(buf) <= 1) {
            break;
        }

        /* Envoi de ce message */
        result = SDLNet_TCP_Send(tcpsocket, buf, sizeof(buf));

        if(result < sizeof(buf)) {
            break;
        }

        /* On recoit l'accuse de reception */
        result = SDLNet_TCP_Recv(tcpsocket, buf, sizeof(buf));

        if(result < sizeof(buf)) {
            break;
        }
    }
Enfin, si jamais il y a un problème lors de l'envoi et la réception de l'accusé de réception, le nombre d'octets envoyé sera plus petit que la taille du tampon. Dans ce cas, nous sortirons de la boucle et fermerons tout :
Fermeture du programme
    SDLNet_TCP_Close(tcpsocket);
    SDLNet_TCP_Close(tcpserveur);

    SDLNet_Quit();
    SDL_Quit();
    return EXIT_SUCCESS;
}
Lors de la mise en place de ce tutoriel, j'ai remarqué que la SDL_net avait un petit problème pour détecter si le programme de l'autre côté de la socket se ferme. En effet, si on envoit un message à une socket qui est fermée, la fonction ne le détecte pas tout de suite. Par contre, si on tente de renvoyer un message vers cette socket, le programme se ferme.

La solution qu'il faut adopter est de demander que les clients envoyent un accusé de réception. En appelant la fonction de réception, nous verrons tout de suite si la socket est toujours ouverte.


2.2. Le client

Du côté client, il n'y pas grand chose de différent. Il n'y pas d'appel à SDLNet_TCP_Accept puisque ce n'est pas un serveur. Son code commence aussi par l'inclusion et la déclaration du main :
Inclusion et déclaration du main du programme client
#include <stdlib.h>
#include <SDL.h>
#include <SDL_net.h>

int main(void)
{
    IPaddress ip;
    TCPsocket tcpserveur;
    char buf[1024];
    unsigned int result;
Puisque le client n'utilisera pas la fonction SDL_Delay, le client n'a pas besoin du module Timer, nous passerons 0 à la fonction SDL_Init. Juste parce que nous passons 0, cela ne veut pas dire que cet appel ne fait rien derrière. Il initialise tout de même la SDL.
Initialisation de la SDL et de la SDL_Net
    if(SDL_Init(0) != 0) {
        fprintf(stderr, "Erreur d'initialisation\n");
        return EXIT_FAILURE;
    }

    if(SDLNet_Init() != 0) {
        fprintf(stderr, "Erreur d'initialisation de SDL_Net\n");
        return EXIT_FAILURE;
    }
Ensuite, nous initialisons une structure de type IPaddress pour que cela représente le serveur. Cet exemple fait tourner le serveur et le client sur la même machine, du coup, nous utilisons localhost comme nom de machine. Ensuite, il ne reste plus qu'à ouvrir la connexion.
Résolution de l'adresse IP et l'ouverture de la socket
    /* Creation de l'adresse IP */
    if(SDLNet_ResolveHost(&ip, "localhost", 12345) != 0) {
        fprintf(stderr, "Erreur de resolution d'IP\n");
        return EXIT_FAILURE;
    }
    
    /* connexion au serveur */
    tcpserveur = SDLNet_TCP_Open(&ip);

    if(tcpserveur == NULL) {
        fprintf(stderr, "Erreur de connexion au serveur : %s\n", SDLNet_GetError());
        return EXIT_FAILURE;
    }
Une fois que la connexion au serveur est réussie, nous recevons les messages du serveur grâce à cette fonction :
Prototype de la fonction SDLNet_TCP_Recv
int SDLNet_TCP_Recv(TCPsocket sock, void *data, int maxlen);
Nous l'avons déjà vu dans la partie serveur mais expliquons un peu plus son fonctionnement. Cette fonction prend en paramètre la socket qui lie le client au serveur. Le deuxième paramètre est un pointeur vers une zone mémoire qui sera remplie par la réception réseau. Le dernier paramètre est la taille maximale de la zone mémoire mais il faut remarquer une chose : cette fonction est bloquante jusqu'à ce que la socket se ferme ou que la zone mémoire soit remplie.

Ceci veut donc dire que si nous demandons 100 octets, il faut recevoir au moins 100 octets avant que la fonction ne retourne (ou que la socket se ferme). C'est pour cette raison que le serveur envoit le même nombre d'octets que le client l'attend.
Boucle de réception
    while(1) {
        result = SDLNet_TCP_Recv(tcpserveur, buf, sizeof(buf));

        if(result < sizeof(buf)) {
            fprintf(stderr, "Erreur de connexion avec le serveur : %s\n", SDLNet_GetError());
            break;
        }
        printf("%s", buf);

        /* On envoit un accuse de reception, a la limite, juste le message */
        result = SDLNet_TCP_Send(tcpserveur, buf, sizeof(buf));
        if(result <sizeof(buf)) {
            break;
        }
    }
Enfin, on termine ce code avec la fermeture de la socket et la fermeture des API.
Fermeture du programme
    /* On accepte un client */
    SDLNet_TCP_Close(tcpserveur);

    SDLNet_Quit();
    SDL_Quit();
    return EXIT_SUCCESS;
}

3. Le deuxième exemple

Dans ce deuxième exemple, le serveur va accepter plusieurs clients et va envoyer les messages à chaque client qui est connecté. Nous allons présenter le code du serveur pour voir présenter une solution pour gérer un serveur centralisé.


3.1. Le serveur

La première chose qui change dans le code du serveur est la déclarations des sockets clients.
La déclaration des sockets clients
    TCPsocket tcpsocket[MAX_CONNEXIONS];
Comme vous le voyez, nous avons défini une constante qui sera la nombre maximal de clients que nous acceptons en même temps. En principe, pour être le plus général, il faudrait soit utiliser une liste chaînée, soit utiliser une allocation dynamique pour étendre les possibilités du serveur.

Pour cet exemple, nous allons simplement faire une allocation statique. Si jamais le tableau est rempli, nous arrêterons de vérifier pour une nouvelle connexion. Le code commence comme dans l'exemple 1 mais, à la place d'attendre une connexion avant d'envoyer des messages, nous allons intégrer la vérification de demande de connexions des clients dans la boucle générale. Nous allons à présent montrer le code du serveur.
La boucle générale
while(fgets(buf, sizeof(buf), stdin) != NULL) {
    /* Si on a de la place pour ce client */
    while(nbr_connectes < MAX_CONNEXIONS) {

        /* On accepte un client */
        tmpsocket = SDLNet_TCP_Accept(tcpserveur);

        /* Si un client est connecte, on l'ajoute au tableau */
        if(tmpsocket != NULL) {
            printf("On accepte un client\n");
            ipadd = SDLNet_TCP_GetPeerAddress(tmpsocket);
            if(ipadd != NULL) {
                printf("Client connecte : %s\n", SDLNet_ResolveIP(ipadd));
            }
            tcpsocket[nbr_connectes] = tmpsocket;
            nbr_connectes++;
        }
        else {
            /* Plus de client, on sort */
            break;
        }
    }
Comme vous le voyez, cette boucle générale commence par vérifier si on a une place pour un nouveau client. Si c'est le cas, nous regardons si un client veut se connecter. Ensuite, nous l'ajoutons dans le tableau des clients et incrémentons la variable nbr_connectes. Enfin, nous affichons les informations du client.

Avant de passer à la suite, je ferais remarquer l'utilisation d'une boucle à la place d'une simple condition. Ceci nous permet de vérifier s'il y a plusieurs connexions en attente.

La suite du code vérifie si la ligne entrée par l'utilisateur du serveur est vide (donc ne contient qu'un '\n'). Si c'est le cas, nous fermons le serveur. sinon nous envoyons le message à tous les clients. Une fois fait, nous attendons la réception des accusés de réceptions de la part des clients.

L'envoi des messages
        /* Si c'est une ligne vide */
        if(strlen(buf) <= 1) {
            break;
        }

        /* Envoi de ce message a tous les clients */
        i = 0;
        printf("Envoi d'un message\n");
        while(i<nbr_connectes) {
            result = SDLNet_TCP_Send(tcpsocket[i], buf, sizeof(buf));

            if(result < sizeof(buf)) {
                SDLNet_TCP_Close(tcpsocket[i]);

                if(nbr_connectes > 0) {
                    tcpsocket[i] = tcpsocket[nbr_connectes-1];
                    nbr_connectes--;
                }
            }
            else {
                /* On passe au prochain */
                i++;
            }
        }
Enfin, nous devons récupérer la réception des accusés de réception des clients.
Réception des accusés de réception
        /* Reception des accuses de reception */
        i = 0;
        printf("Reception d'un message\n");
        while(i<nbr_connectes) {
            result = SDLNet_TCP_Recv(tcpsocket[i], buf, sizeof(buf));

            if(result < sizeof(buf)) {
                SDLNet_TCP_Close(tcpsocket[i]);

                if(nbr_connectes > 0) {
                    tcpsocket[i] = tcpsocket[nbr_connectes-1];
                    nbr_connectes--;
                }
            }
            else {
                /* On passe au prochain */
                i++;
            }
        }
Entre cette deuxième version et la première version, il n'y a pas grand chose qui change. Mais les différences sont importantes. Périodiquement le serveur vérifie si un nouveau client attend une connexion et ceci peut se passer n'importe quand au cours de la vie du serveur. L'accusé de réception permet de savoir si les clients sont toujours présents.

Bien sûr, si un client n'est plus là, on ferme la socket et on bouge la dernière socket à la case qui est vide et on décrémente le compteur du nombre de connectés.

La prochaine version du programme permettra aux clients de s'écrire directement des messages mais aussi de demander au serveur des messages. Nous mettrons directement un protocole de communication entre les clients et le serveur. Ceci permettra de demander au serveur le nombre de clients et leur nom d'utilisateur, d'envoyer un message à tous les clients mais aussi à un seul en particulier.


3.2. Le client

En fait, il n'y a rien qui change du côté client. Le travail du client est de juste recevoir un message du serveur et envoyé un accusé de réception au serveur.

Du coup, rien ne change entre les deux versions.


4. Conclusion

Nous avons présenté une utilisation basique de cette bibliothèque pour gérer les connexions réseau. Cette bibliothèque facilite la programmation réseau et est portable.

Les deux exemples montrent le code nécessaire pour connecter un client à un serveur et comment envoyer des messages aux clients.

On termine donc ce premier article sur la SDLNet avec une meilleure compréhension de la programmation réseau. Certes, tout le code est contenu dans le main, du coup, ce n'est pas très modulable. Dans la prochaine partie, nous allons voir comment généraliser le serveur et permettre aux clients de demander des informations aux serveurs mais aussi d'envoyer des messages.



               Version PDF   Version hors-ligne

Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2007 fearyourself. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique 2D - 3D - Jeux : LittleWhite -