IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Réseau et multijoueurs Discussion :

Structure des paquets réseau


Sujet :

Réseau et multijoueurs

  1. #1
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut Structure des paquets réseau
    Bonjour,

    tout nouveau dans le réseau j'aimerais créer une application Client/Serveur. La partie sockets, connection etc est gérée mais il me manque l'essentiel : la "norme" de communication entre mes clients et mon serveur, comment se comprenne t-il ?

    Alors j'ai pensé rapidos à une structure de message, dites-moi honnêtement si c'est nul.

    Je construit la trame à partir de suite d'entier signé codé sur 16 bits, suivit d'une suite de paire<entier_signé_16_bits, valeur_string>.

    Ce qui donnerais une trame comme:
    entier_signé_16_bits entier_signé_16_bits... [entier_signé_16_bits string entier_signé_16_bits string]

    Le premier entier serait la cible du message, j'en vois deux pour l'instant :
    - account : tout ce qui concerne les données de l'utilisateur dans l'application en général (mot de passe etc)
    - game : toutes les données qui concerne le jeu (position, score etc)

    Le deuxième entier serait l'objectif du message :
    - info : donne une info, comme ca gratuitement sans rien attendre en retour (exemple position des adversaires)
    - ask : questionne, demande un retour d'information
    - answer : répond à une question

    Le troisième entier serait soit la question posée, soit une action.

    Enfin le reste de la trame renseignerait les potentielles données necessaire à sa compréhension sous la forme <type de l'information (entier signé 16 bits), valeur string>

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     
    // Concern of the message
    const qint8 CONCERN = 0;
     
    const qint8 CONCERN_ACCOUNT = CONCERN + 1;
    const qint8 CONCERN_GAME = CONCERN + 2;
     
    // Purpose of the message
    const qint8 PURPOSE = 10;
     
    const qint8 PURPOSE_INFO = PURPOSE + 1;
    const qint8 PURPOSE_ASK = PURPOSE + 2;
    const qint8 PURPOSE_ANSWER = PURPOSE + 3;
     
    // Content of the message
    const qint8 REQUESTS = 50;
    const qint8 REQUESTS_ASK_ACCOUNT_IDS = REQUESTS + 1;
    const qint8 REQUESTS_INFO_MOVE = REQUESTS + 2;
    const qint8 REQUESTS_INFO_SHOOT = REQUESTS + 3;
     
    // Type of data
    const qint8 TYPE = 150;
     
    const qint8 TYPE_USERNAME = TYPE + 1;
    const qint8 TYPE_PASSWORD = TYPE + 2;
     
    const qint8 TYPE_POS_X = TYPE + 201;
    const qint8 TYPE_POS_Y = TYPE + 202;
    const qint8 TYPE_POS_ANGLE = TYPE + 203;
    const qint8 TYPE_HEALTH = TYPE + 204;
    Exemples de message:

    Demande des identifiants :
    serveur > CONCERN_ACCOUNT PURPOSE_ASK REQUESTS_ASK_ACCOUNT_IDS
    client > CONCERN_ACCOUNT PURPOSE_ANSWER REQUESTS_ASK_ACCOUNT_IDS TYPE_USERNAME user_toto TYPE_PASSWORD password_toto

    Changement de position :
    client > CONCERN_GAME PURPOSE_INFO REQUESTS_INFO_MOVE TYPE_POS_X 50 TYPE_POS_Y 455

    Bien sûr il faut gérer les incohérences, des messages comme ci-dessous serait sans-sens.
    CONCERN_ACCOUNT REQUESTS_INFO_SHOOT TYPE_USERNAME user_toto

    Enfin bref, je sais très bien que ce n'est pas une bonne solution, parce que ca me parait complexe de prévoir tous les cas dès le début, et de plus je trouve qu'il y a pas mal de traitement derriere pour comprendre le message.

    Par contre il est vrai que ce genre de msg ne pèserais que qq octets

    Comment faîtes-vous ? Merci beaucoup

  2. #2
    Modérateur
    Avatar de nouknouk
    Homme Profil pro
    Inscrit en
    Décembre 2006
    Messages
    1 655
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France

    Informations forums :
    Inscription : Décembre 2006
    Messages : 1 655
    Points : 2 161
    Points
    2 161
    Par défaut
    Hello,

    tu trouveras peut-être quelques idées dans ce post (et les suivants).

    Le dernier point (qui commence par "faire un niveau d'abstraction au dessus de ConnexionPool pour pouvoir séparer les communications en 'canaux' logiques") est notoirement intéressant, car il te permettra de séparer tes messages en canaux logiques, et donc de pouvoir 'décentraliser' la gestion de tes messages en les regroupant par 'thème', et donc en les gérant dans des endroits dédiés de ton code, ce que je détaille ci-après:

    concrètement ta structure de message ressemblera à ça:

    [taille] [no_canal] [type_message] [donnees_message]

    - [taille] représente la taille totale du message (en octets)
    - [no_canal] représente le no. de l'entité qui va s'occuper de traiter ledit message
    - [type_message] renseignera sur l'action elle-même a opérer et déterminera le contenu à proprement parler de donnees_message
    - [donnees_message] contiendra les données nécessaires au traitement de l'action (par exemple, pour tel type de message, ce sera 2 entiers, trois string et deux float).

    Et au niveau du code, tu auras différentes couches:

    - la classe TcpframeSocket qui s'occupe de gérer la socket bas niveau, de récupérer les octets reçus et de les découper en 'messages' (grâce à [taille]) pour constituer une instance de TcpFrame. Quand un événément s'est produit (genre 'un nouveau message est disponible') envoie des 'signaux' (pattern observer) à qui a envie de l'écouter.

    - la classe ChannelManager, qui contient une liste de 'Channel' qui s'y seront enregistrés ; c'est elle qui écoute la TcpFrameSocket et quand un message est disponible, elle va décoder le numéro de canal concerné contenu dans la TcpFrame (champ [no_canal]) et lui refiler le message à traiter.

    - les classes dérivant de 'Channel' qui auront donc une méthode qui sera appelée quand un message qui leur est destiné a été reçu et doit être traité. Ce 'channel' (par exemple AuthentificationChannel) va décoder le type de message, en déduire le nombre et le type de données contenues dans 'données-message', les récupérer et faire le traitement approprié. Par exemple côté serveur, le message de type 'LOGIN' envoyé par le client contient deux string: pseudo & motDePasse. Ils serviront à récupérer les infos de l'utilisateur dans une base de données ; une fois la chose faite, ce channel pourra renvoyer au client un message de type LOGIN_ERROR, ou PASS_ERROR, ou LOGIN_OK, chacun avec ses données propres.

    L'avantage c'est que tu peux ainsi bien séparer chaque 'thème' de responsabilités dans des classes différentes, chacune dérivant de la classe 'Channel' de base. Par exemple si tu fais un jeu en ligne, tu auras un channel dédié à la réception/envoi des messages d'authentification, un autre channel dédié à la gestion des messages relatif au 'chat', un channel dédié aux messages relatifs aux actions des joueurs dans le jeu, etc... Il ne te restera alors plus qu'à faire une classe dérivée pour tel Channel côté client, et une classe pour ce même Channel côté serveur.

    En comparaison avec ton idée, on a:

    - "premier entier" est une sirte d'équivalent à ma notion de 'channel'

    - 'troisième entier' est une sorte d'équivalent à ma notion de 'type de message'

    - 'deuxième entier' n'existe plus car ce sont les channels qui connaissent les types de message qui sont amenés à leur être adressé, et donc si tel type est un message 'spontané', une demande ou une réponse à une demande. Poru reprendre l'exemple d'avant, le code côté serveur dans AuthChannel sait que quand il reçoit un message de type 'LOGIN' il devra répondre quelque chose (LOGIN_OK, LOGIN_ERROR, ...). De même le code du ClientAuthChannel sait lui-aussi que lorsqu'il a envoyé 'LOGIN' il doit s'attendre à recevoir quelque chose par le serveur. Donc pas besoin de le coder dans le message, c'est implicite pour chacune des parties: client & serveur.

    - 'le reste' est un équivalent à mes [donnees_message]. Pour la sérialisation à proprement parler du contenu, il n'est pas nécessaire de coder le type de chaque valeur avant la valeur elle-même ; à nouveau le type de message détermine à lui seul ce que contiendra donnees_message, c'est implicite pour chacune des parties: client & serveur.
    Mon projet du moment: BounceBox, un jeu multijoueurs sur Freebox, sur PC et depuis peu sur smartphone/tablette Android.

  3. #3
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Merci beaucoup Nounouk ! (je savais que t'allais passer par là à l'occaz )

    D'ailleurs, j'ai implémenté toute l'architecture que tu proposes dans l'autre lien que j'avais déjà repéré. Faut dire qu'avec Qt c'est rapide

    Tout implémenté excepté les channels justement. C'est donc par ca que je vais commencer. Je reviendrai donner des nouvelles sur ce post (très bientôt je pense).

    Encore merci.

    edit: juste une question qui me vient en tête en relisant, [données_message] serait une succession de "paramètre" disons. Pas besoin de renseigner les types de chaque données, en effet on sait que pour tel type de message on attend 2 int et une string par exemple. Ca peut paraître bête mais comment les séparer ces données dans données_message ? Parce que pour des int on peut se mettre d'accord sur une taille. Mais pour une string ou char*, des informations pourrait prendre 2 caractères et d'autres une centaine. Donc donner une taille fixe pour ttes string parait inadapté...

    Comment qu'on fait ?

  4. #4
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Autre question

    Je t'expose premièrement où en est mon archi...

    TcpServer:
    - nouvelle connexion, informe connexion pool.

    ConnexionPool:
    - stocke la socket, l'associe à un UserDetails.
    - câble le signal disconnected() de la socket (j'utilise Qt pour rappel) pour être au courant de la déconnexion.
    - câble le signal frameReceived(TcpFrameSocket, TcpFrame) de la TcpFrameSocket à ChannelManager.

    ChannelManager:
    - stocke dans une Map<no_channel, Channel*> les channels.
    - la méthode onFrameReceived(TcpFrameSocket, TcpFrame) effectue un frame.getChannel(). Si le numéro existe dans la Map, on fait un map.value(channel_id)->treatFrame(frame).

    Channel:
    - Déserialize la trame, check si elle est correct, puis cohérente.

    Alors là tout le chemin de réception est "géré". Mais une fois la frame traitée, dans certains cas il faudra répondre (au client qui a envoyé la frame, ou à tous les clients dans le cas d'une mise à jour globale).

    Ma question est, quelle classe gère l'envoi ? Comme ca je dirais ConnexionPool car elle est la seule a possèder ttes les sockets clientes.
    J'imagine par contre que c'est Channel qui construit le message de réponse.
    Dans ce cas pour ne pas perdre l'information de "qui à envoyer le message", il faut que je trimballe la socket ou le userDetails associé de ConnexionPool à Channel puis de Channel à ConnexionPool ?...

    Merci beaucoup pour toutes mes questions

  5. #5
    Modérateur
    Avatar de nouknouk
    Homme Profil pro
    Inscrit en
    Décembre 2006
    Messages
    1 655
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France

    Informations forums :
    Inscription : Décembre 2006
    Messages : 1 655
    Points : 2 161
    Points
    2 161
    Par défaut
    Citation Envoyé par keyga Voir le message
    câble le signal frameReceived(TcpFrameSocket, TcpFrame) de la TcpFrameSocket à ChannelManager.
    Tu peux choisir de préfèrer un système de signal / slot pour éviter une dépendance inutile entre ConnexionPool et ChannelManager, au prix d'un (très) léger coût en perf.

    Après, c'est de l'ordre du détail. Perso, pour des raisons historiques, mon ConnexionPool et ChannelManager sont en fait très liées puisque ... c'est une seule et même classe. Si c'était à refaire, j'opterais probablement pour deux entités séparées avec un mécanisme d'observer (= signal/slot en Qt).

    Par contre, ton connexion Pool doit passer, en même temps que ta TcpFrame reçue, l'user qui y est rattaché (qui sera ensuite propagé aux Channel, qui saura ainsi de qui vient le message).

    (signal) ConnectionPool::frameReceived(TcpFrame& frame, User& user)
    (slot) ChannelManager::onFrameReceived(TcpFrame& frame, User& user)

    D'une façon globale, le channel va manipuler des TcpFrame reçues/envoyées et des (collections d') User qui y sont rattachés, tout le reste est abstrait par le plus 'bas niveau' (ConnexionPool, TcpFrameSocket).

    - la méthode onFrameReceived(TcpFrameSocket, TcpFrame) effectue un frame.getChannel().
    Pas besoin d'une méthode spécifique. Ta TcpFrame est une sote d'entité qui se comporte comme une queue (QQueue) dans laquelle on va piocher un par un les 'valeurs' du message. Ton n° de channel est le premier champ que ChannelManager va 'consommer' ; quand il transmettra la TcpFrame au channel, le channel continuera de piocher dans la queue en commençant par le champ après celui du channelId. L'avantage est que ta classe TcpFrame reste ainsi générique et n'inclut pas une notion de channel. Le jour où tu voudras utiliser tes sockets pour faire autre chose que des channels, ta classe n'aura pas besoin de changer.

    Donc ton onFrameReceived ressemblera peu ou prou à ça:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    ChannelManager::onFrameReceived(TcpFrame& frame, User& u) {
        long channelId = frame.getLong();
        Channel* chan = this->channels.get(channelId);
        if (chan != NULL) {
            chan->onIncomingMessage(frame, u);
        }
    }
    Channel: Déserialize la trame, check si elle est correct, puis cohérente.
    onIncomingMessage() sera une fonction intermédiaire qui va commencer par récupérer le champ suivant, le type de message (un string, admettons) avant d'appeler une méthode (virtuelle pure) de traitement à du message ; cette méthode est celle qui sera définie dans les classes dérivées de IChannel:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    IChannel::onIncomingMessage(TcpFrame& frame, User& u) {
        QString msgType = frame.getString();
        this->processMessage(msgType, u, frame);
    }
    Tu as ensuite une classe concrète dérivant de Channel, par exemple pour gérer les authentifications, dont le message 'LOGIN':

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     
    class AuthChannel: public Channel {
        public:
            AuthChannel() : Channel(AUTH_CHANNEL_ID) {}
        protected:
            void processMessage(QString msgType, User& u, TcpFrame& frame) {
                if (msgType == "LOGIN") {
                    QString pseudo = frame.getString();
                    QString pass = frame.getString();
                    // faire le traitement adéquat ici.
                }
            }
    }
    Alors là tout le chemin de réception est "géré". Mais une fois la frame traitée, dans certains cas il faudra répondre (au client qui a envoyé la frame
    Là, on a plusieurs possibilités. Celle que j'ai choisie est grosso modo la même que celle que tu décris, et qu'on va appeler méthode 'en cascade'. Cela consiste à dire que notre classe abstraite Channel a un lien fort avec ChannelManager, qui lui même a un lien (très) fort avec ConnexionPool, qui lui gère les sockets. Techniquement, ça donne:

    Channel expose des fonctions protected à ses classes dérivées pour envoyer des messages à un ou plusieurs User: sendMessageToOne(msgType, frame, user) ; sendMessageToList(msgType, frame, QList<User>).

    Ces fonctions ne font que passer le relais au ChannelManager dans lequel a été enregistré ce Channel. Pour cela, mon Channel a un membre protégé (ChannelManager* ownerChannelManager) auquel on affecte le ChannelManager au moment où on fait un registerChannel()

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    ChannelManager::registerChannel(Channel* chan) {
        this->channels.put(chan->getChannelId(), chan);
        chan->ownerChannelManager = this;
        // ownerChannelManager est protected, mais ChannelManager et Channel sont déclarés friend.
    }
    ChannelManager::unregisterChannel(Channel* chan) {
        this->channels.remove(chan);
        chan->ownerChannelManager = NULL;
    }
    Et donc nos sendMessage ressemblent à:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    Channel::sendMessageToUser(msgType, frame, user) {
        frame.prependString(msgType)
        ownerChannelManager->channelSendMessageToOne(this, frame, user);
    }
    On a donc réussi à relayer la demande d'envoi de message du Channel à son manager, qui lui s'occupera d'ajouter en début de TcpFrame le channelId et refilera le bébé au ConnexionPool (même principe, le ChannelManager peut avoir un pointeur vers son ConnexionPool).
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    ChannelManager::channelSendMessageToOne(Channel* chan, TcpFrame& frame, User& user) {
        frame.prependLong(chan->getChannelId());
        ownerConnexionPool->sendFrameToUser(frame, user);
    }
    Le connexionPool connaissant les socket, il pourra alors effectuer l'envoi concret en récupérant la(les) socket(s) associée(s) au(x) User(s) et en appellant TcpFrameSocket::sendMessage(frame) pour chacun d'eux.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    ConnexionPool::sendFrameToUser(TcpFrame& frame, User& user) {
        TcpFrameSocket* userSocket = this->getsocketByUser(user);
        userSocket->sendFrame(frame);
    }
    Même principe pour envoyer non pas à un seul User, mais à plusieurs: on ajoute une méthode Channel::sendMessage à laquelle on passe une QList et au lieu de faire un appel à TcpFrameSocket::sendMessage, on itère sur chacune des socket de chaque User et on envoie le même message un par un à chaque client.

    Voilà pour la première approche 'en cascade': en résumé, le Channel dérivé utilise un appel à sendMessage de la classe abtraite Channel. sendMessage ajoute le messageType et passe le relais à son ChannelManager. ChannelManager ajoute le channelId et passe le relais à ConnexionPool. ConnexionPool retrouve la socket assocée au User et fais un socket->sendFrame().

    Petite digression: au passage, on est en plein dans le principe d'encapsulation et d'empilement des protocoles du modèle réseau OSI: chaque couche (physique, transport, ... application) encapsule les données de la couche précédente et ajoute ses propres données de contrôle (ici un msgType, un chanId, ...).


    L'autre approche pour envoyer les messages est de dire que la socket est intimement liée aux instance de User. On peut donc ajouter dans la classe User un pointeur (protégé) vers la TcpFrameSocket et rendre User et Channel 'friend' pour que le channel puisse directement appeler le sendMessage de la socket:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
    class User {
        protected: 
            TcpFrameSocket* userSocket;
     
        friend Channel;
    }
     
    Channel::sendMessageToUser(msgType, frame, user) {
        frame.prependLong(this->getId();
        user.getSocket().sendMessage(frame);
    }
    L'avantage de la première méthode (en cascade) est qu'on passe par un point centralisé où tous les messages reçus et émis pourront être interceptés ; cela peut être particulièrement utile (voir vital) ensuite pour des raisons de synchronisation de thread, et pour d'autres plus anecdotiques (par exemple un logger qui enregistre tous les messages émis/reçus pour les séances de debug). Elle a en outre d'autres atouts (séparation intelligible des reponsabilités, ...). C'est pourquoi je privilégie largement cette solution.

    Le désavantage est qu'on lie plus fortement chaque 'strate' (ConnexionPool, ChannelManager, Channel), mais ce n'est pas irrémédiable: les pointeurs 'owner' peuvent parfaitement être remplacés par des sauts via des signaux/slots. C'est juste un poil plus verbeux (et théoriquement un poil moins performant, mais en pratique c'est totalement négligeable).

    C'est ensuite aux instances de channel , ou à tous les clients dans le cas d'une mise à jour globale).
    Dans les deux cas, notre classe dérivée de Channel ne 'verra' qu'un ensemble de méthodes 'Channel::sendMessage' auxquelles elle passera la TcpFrame à envoyer et le ou les User à qui l'envoyer.

    Pour reprendre l'exemple du login, notre classe AuthChannel peut matinenant être complétée pour renvoyer une réponse au message 'LOGIN' reçu:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
            void AuthChannel::processMessage(QString msgType, User& u, TcpFrame& frame) {
                if (msgType == "LOGIN") {
                    QString pseudo = frame.getString();
                    QString pass = frame.getString();
                    // faire le traitement adéquat ici.
                    if (isAuthOk) {
                        TcpFrame response();
                        response.addString("yourUserDetails");
                        QString responseMsgType("LOGIN_OK");
                        sendMessageToUser(responseMsgType, response, u);
                    }   
                    else { 
                        /* ... on envoie une réponse de type 'LOGIN_FAILED', ... */ 
                    }
                }
            }
    Et l'appel à sendMessage va déclencher:

    - l'ajout du msgType par Channel dans la frame qui devient: ["LOGIN_OK] [yourUserDetails]

    - l'ajout du chanId (123) par ChannelManager qui devient: [123] ["LOGIN_OK] [yourUserDetails]

    - la transmission de la frame à la TcpFrameSocket qui va ajouter la taille totale du message(25 octets), qui devint [25] [123] ["LOGIN_OK] [yourUserDetails]

    - ce message est envoyé par notre QTcpSocket sur le réseau.

    - le client va recevoir ça et faire le chemin inverse: [25] va permettre de découper le message ; [123] va permettre au ChannelManager de retrouver la bonne instance de Channel, ["LOGIN_OK"] va permettre à processMessage de trouver le bon traitement à faire, et ledit traitement pourra récupérer son [yourUserDetails] pour faire ce qu'il a a faire.


    Enfin, pour revenir sur les User multiple, c'est aux Channel spécialisés de maintenir la liste des User à qui il peut être amené à envoyer des messages (ou pas s'il n'en a pas besoin).

    Par exemple un channel qui se contente de répondre à une requête PING d'un client (pour que le client puisse estimer la latence entre lui et le serveur) n'a pas besoin de maintenir de liste d'utilisateur puisqu'il ne fera que répondre au User associé à la TcpFrame qu'il reçoit. Même principe pour le channel dédié à l'authentification: il n'a pas besoin de connaître d'autres users hormis celui qui fait la demande.

    A contrario, un Channel d'un jeu qui gère le chat 'global' permettant à tous les joueurs connectés de converser entre eux aura besoin de maintenir une liste des joueurs actuellement connectés ; ainsi quand il recevra un message d'un joueur il pourra le relayer à tous les autres. C'est la responsabilité de la classe dérivée de Channel (GlobalChatChannel) de maintenir cette liste de user à jour, par exemple en écoutant les signaux de ConnexionPool pour être prévenu quand un User se connecte/déconnecte. La liste sera ensuite utilisée quand on voudra envoyer un message à tous les User connectés.


    En espérant que mon pavé indigeste reste à peu près compréhensible ... bon courage

    PS: désolé d'avance pour les erreurs de syntaxe qui ne manquent probablement pas dans mes bouts de code ; dur d'écrire sans faute du C++ à la volée dans un forum quand ça fait presque 4 ans qu'on pratique quasi exclusivement un autre langage (en l'occurrence, Java)
    Mon projet du moment: BounceBox, un jeu multijoueurs sur Freebox, sur PC et depuis peu sur smartphone/tablette Android.

  6. #6
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Que dire... merci énormément je vais avancer vite grâce à toi ! On sent l'XP Bon je vais relire tout ca plusieurs fois pour bien assimiler le principe avant l'implémentation.

    Et je reviendrai surement avec d'autres questions


  7. #7
    Modérateur
    Avatar de nouknouk
    Homme Profil pro
    Inscrit en
    Décembre 2006
    Messages
    1 655
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France

    Informations forums :
    Inscription : Décembre 2006
    Messages : 1 655
    Points : 2 161
    Points
    2 161
    Par défaut
    On sent l'XP
    C'est ce principe sur lequel se base ma signature, et pour le moment, avec 600 000 joueurs, 50 000 parties/jour, 3000 messages/seconde, le tout fait en Java à la truelle (pas de NIO, ...), ça tient avec pas mal de marge

    Bon je vais relire tout ca plusieurs fois pour bien assimiler le principe avant l'implémentation.
    Oui, d'autant que j'ai du éditer 20 fois mon post au cours de la dernière heure pour l'étayer

    Et je reviendrai surement avec d'autres questions
    Pas de problème.

    La petite cerise à propos de la gestion des listes d'utilisateurs dans les Channels, c'est typiquement un concept que tu vas réutiliser dans de nombreuses implémentations concrètes de channels, donc mieux vaut la factoriser ; fais une classe abstraite ChannelWithUsers, dérivée de Channel, qui contient cette liste et des méthodes registerUser(), unregisterUser(), getChannelUserList(), isUserInChannel(), sendMessageToAllUsers(), etc ... Tu pourras ensuite faire dériver de ChannelWithUsers tes channels qui en ont besoin.
    Mon projet du moment: BounceBox, un jeu multijoueurs sur Freebox, sur PC et depuis peu sur smartphone/tablette Android.

  8. #8
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Salut !

    Il a l'air sympa ton jeu BounceBox, jtesterai sur la freebox

    Je reviens avec quelques questions qui sont apparues ce matin dans ma ptite tête pas réveillée .

    La première concerne la TcpFrame. Tu dis quelle peut se comporter comme une QQueue, je pense que tu voulais dire un QStack . Toute la partie construction haut niveau de la frame est clair/simple dans ma tête.

    Dans Channel dérivé on fait genre :
    frame.pushString(QString("user));
    frame.pushString(QString("password");
    frame.pushInt(LOGIN); // type message

    Dans ChannelManager:
    frame.push(channel_id);

    Dans TcpFrameSocket:
    frame.push(taille);

    Pour la réception haut niveau on fait le même principe avec des pop() sur le stack. Tout ca tu me l'a bien expliqué.

    Mais comment on passe de ce stack haut niveau en un bytes array et inversement d'un bytes array à un stack ? Parce qu'à un moment il faut bien que je fasse un socket.write(...), et cette fonction n'accepte pas de stack .

    Aussi comment être sur de l'intégrité de la frame: si lorsque que je lis la frame j'effectue un popString() sur la donnée "123". 123 peut être caster en int comme en string sans erreur... et pourtant une string 123 pourrait ne pas avoir de sens.

    La deuxième question concerne l'archi de l'appli.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void AuthChannel::processMessage(QString msgType, User& u, TcpFrame& frame) {
                if (msgType == "LOGIN") {
                    QString pseudo = frame.getString();
                    QString pass = frame.getString();
                    
                    // Recherche dans la BDD
    
                    if (isAuthOk) {
                        TcpFrame response();
                        response.addString("yourUserDetails");
                        QString responseMsgType("LOGIN_OK");
                        sendMessageToUser(responseMsgType, response, u);
    
                        // Traitement 
    
                    }   
                    else { 
                        /* ... on envoie une réponse de type 'LOGIN_FAILED', ... */ 
                    }
                }
            }
    Jusqu'à présent notre UserDetails représente qqn qui s'est connecté au serveur. Ca ne veut pas dire qu'il est "légitime".
    Si LOGIN_OK, as-tu une classe qui associe un UserDetails à un Player (qui lui reprend les infos existante de la BDD, pseudo etc) ?
    Pour reprendre ton idée de ChannelWithUsers, dans la plupart des cas la liste d'users serait en fait la liste de players (user connecté et authentifié), non ?

    Enfin si LOGIN_FAILED, est-ce au serveur de déconnecter la socket du client, ou au client de déconnecter sa socket à la réception de ce message ?

    Voilà encore pas mal de questions

    A bientôt et merci

  9. #9
    Modérateur
    Avatar de nouknouk
    Homme Profil pro
    Inscrit en
    Décembre 2006
    Messages
    1 655
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France

    Informations forums :
    Inscription : Décembre 2006
    Messages : 1 655
    Points : 2 161
    Points
    2 161
    Par défaut
    Citation Envoyé par keyga Voir le message
    La première concerne la TcpFrame. Tu dis quelle peut se comporter comme une QQueue, je pense que tu voulais dire un QStack .
    C'est un détail, mais non je parle bien d'une QQeue (FIFO) et pas d'une QStack, car on va 'consommer' les données dans leur ordre d'arrivée (ou plutôt dans l'ordre où elles ont été sérialisées par le correspondant.

    Si côté client je fais addInt(A); addInt(B); addInt(C), je veux pouvoir les récupérer dans le même ordre de l'autre côté (A,B,C), pas dans l'ordre inverse (C,B,A). C'est donc bien une FIFO.

    Dans Channel dérivé on fait genre :
    frame.pushString(QString("user));
    frame.pushString(QString("password");
    frame.pushInt(LOGIN); // type message

    Dans ChannelManager:
    frame.push(channel_id);
    Dans TcpFrameSocket:
    frame.push(taille);
    Presque, au détail près que les 'push' ajoutent les champs à la fin de la pile (append), mais dans le cas du Taille, chanId et msgType, on veut les ajouter en début de pile car ce seront eux qui seront lu en premier. Or ces champs sont ajoutés après que la TcpFrame ait déjà été peuplée par le code applicatif (plus précisément on les ajoute au moment du sendMessage, juste avant de les refiler à la socket). Donc pour ces deux valeurs, on fait un 'prepend' (et pas un 'append').

    Cf. mon post précédent, paragraphe "Et l'appel à sendMessage va déclencher [...]": la TcpFrame est d'abord peuplée par le code applicatif (yourUserDetails) puis on ajoute en début de frame [msgType], pûis [chanId], puis [taille].

    En Qt, la QQueue est très proche d'une QList (dont elle dérive d'ailleurs). Et dans QList, tu as justement les fonctions append() et prepend() qui sont déja dispo et dont tu peux t'inspirer.

    Mais comment on passe de ce stack haut niveau en un bytes array et inversement d'un bytes array à un stack ? Parce qu'à un moment il faut bien que je fasse un socket.write(...), et cette fonction n'accepte pas de stack .
    Si tu te bases sur Qt, QVariant::toByteArray le fera pour toi pour l'ensemble des types de base dont tu auras besoin (int, long, float, double, string, ...).

    A noter que Qt propose aussi QDataStream pour lire/écrire directement les types courants supportés par QVariant depuis/vers un QByteArray, ce qui devrait te simplifier encore plus l'implémentation de Tcpframe.

    Aussi comment être sur de l'intégrité de la frame: si lorsque que je lis la frame j'effectue un popString() sur la donnée "123". 123 peut être caster en int comme en string sans erreur... et pourtant une string 123 pourrait ne pas avoir de sens.
    C'est à ton code applicatif de ne pas se planter. Si d'un côté tu envoies un 'int', tu dois obligatoirement le récupérer en tant qu'int de l'autre côté (quitte ensuite à le convertir en String si tu veux t'en servir dans ton code comme un string).

    Au pire, si tu veux un mode 'debug' pour t'assurer pendant le développement et les tests que tu ne te plantes pas en essayant de récupérer un string au lieu d'un int, tu peux faire une version spéciale de TcpFrame, TcpFrameDebug qui, pour chaque valeur ajoutée dedans, ajoute en fait deux champs au lieu d'un: le premier renseignant le type et le second la valeur elle-même. Et ta TcpFrameDebug sera capable de lever une exception si tu fais un getMonTypeA() alors qu'elle constate que la prochaine valeur à lire est de type B. Mais je déconseille d'utiliser ça en 'release', mieux vaut limiter son utilisation en debug, car ça introduit une belle quantité d'overhead totalement inutile une fois que le code applicatif est testé.

    Jusqu'à présent notre UserDetails représente qqn qui s'est connecté au serveur. Ca ne veut pas dire qu'il est "légitime".
    C'est là aussi de la responsabilité de ton code applicatif de faire le distinguo entre les deux.
    Pour reprendre ton exemple, si l'utilisateur n'est pas encore authentifié, tu peux parfaitement le stocker dans un booléen de ton UerDetails ; quand un message est reçu par un channel, il peut décider de vérifier si l'utilisateur est authentifié avant de prendre en comtpe le message.

    A noter que le cas de l'authentification est un peu spécial ; puisque logiquement tu ne veux pas que ton client ait la moindre chance d'accéder à tes channels tant qu'il n'est pas dûment authentifié, tu peux faire a en deux étapes: au lieu que toute socket qui vient de se connecter soit automatiquement et immédiatement enregistrée dans le ConnexionPool, tu vas d'abord la refiler à une classe AuthManager séparée (et donc en dehors de toute notion de ConnexionPool, de Channel, etc...). Elle se chargera de l'échange des messages entre le serveur et le client pour l'authentification. Une fois l'authentification dûment faite, c'est seulement là que tu peux enregistrer ta socket et ton UserDetails (peuplé avec les infos de ta BdD par exemple) dans le ConnexionPool.

    Si LOGIN_OK, as-tu une classe qui associe un UserDetails à un Player (qui lui reprend les infos existante de la BDD, pseudo etc) ?
    Pour reprendre ton idée de ChannelWithUsers, dans la plupart des cas la liste d'users serait en fait la liste de players (user connecté et authentifié), non ?
    Ca dépend des besoins de ton code. Le channel qui va gérer le chat global aura sûrement besoin de tous les joueurs connectés ; mais le channel qui permet à deux joueurs de jouer une partie ensemble ne contiendra que ces deux joueurs.
    A noter que ta classe ChannelWithUsers peut parfaitement décider de systématiquement éliminer tout message entrant qui ne viendrait pas d'un user enregistré dans sa liste de user déclarés dans le channel.

    Enfin si LOGIN_FAILED, est-ce au serveur de déconnecter la socket du client, ou au client de déconnecter sa socket à la réception de ce message ?
    Pour des raisons de hack, c'est toujours au serveur de prévoir de déconnecter le client, à minima s'il ne le fait pas lui-même (par exemple au bout d'un certain temps). Ca n'empêche pas que le client prévoie lui-aussi de se déconnecter quand il reçoit un tel message (ou pas, tu peux aussi vouloir laisser une deuxième chance de rentrer son login/pass sans avoir à déconnecter/reconnecter à chaque fois).

    A nouveau, tout cela dépend de la façon dont tu veux que ton code applicatif se comporte. Ce n'est pas de la reponsabilité du framework réseau de prendre ce genre de décision ; lui n'a pour but que de proposer une API simple et générique pour faciliter l'écriture du code applicatif au dessus.
    Mon projet du moment: BounceBox, un jeu multijoueurs sur Freebox, sur PC et depuis peu sur smartphone/tablette Android.

  10. #10
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Encore une fois réponse très détaillée et rapide !!!

    Merci

  11. #11
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Je vais en effet reprendre cette notion d'AuthManager qui se place entre TcpServer et ConnexionPool, je trouve ca propre

    Ca sera mon AuthManager qui en cas de LOGIN_OK instanciera un Player avec les infos de la BDD, et passera à ConnexionPool le couple <Player, Socket>. En prenant garde de se désabonner de tout évènement de la socket, pour vraiment passer la main à 100%. Ah oui j'ai pensé renseigner la version du protocol utilisée à la place du channel_id de la trame pour la phase authentication, si le client n'a pas la bonne version, AuthManager renverra un joli BAD_PROTOCOL

    Donc pour l'instant, j'ai de quoi coder sur le serveur

    Juste un petit doute sur qui possède qui dans l'histoire mais c'est du détail puisqu'il n'existe pas de solution idéale. Je dirais que TcpServer possède Un AuthManager et Un ConnexionPool. ConnexionPool possède Un ChannelManager qui lui possède Plusieurs Channels.

    Je réfléchis maintenant au client... alors tu me diras ce que tu penses de ce qui suit.
    TcpClient possède Un TcpFrameSocket, Un AuthManager, Un ChannelManager et Un Player (vide avant l'authentication).
    TcpClient connecte les évènements de la socket à AuthManager au début, avant de reprendre le contrôle une fois authentifié (et l'objet Player construit) pour prendre le rôle du ConnexionPool dans le serveur (redirection des frames entrantes au ChannelManager, envoi des frames provenant de ChannelManager) puisque c'est le seul à avoir accès à la socket.

    Voili voilou

  12. #12
    Modérateur
    Avatar de nouknouk
    Homme Profil pro
    Inscrit en
    Décembre 2006
    Messages
    1 655
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France

    Informations forums :
    Inscription : Décembre 2006
    Messages : 1 655
    Points : 2 161
    Points
    2 161
    Par défaut
    Citation Envoyé par keyga Voir le message
    Je vais en effet reprendre cette notion d'AuthManager qui se place entre TcpServer et ConnexionPool, je trouve ca propre

    Ca sera mon AuthManager qui en cas de LOGIN_OK instanciera un Player avec les infos de la BDD, et passera à ConnexionPool le couple <Player, Socket>. En prenant garde de se désabonner de tout évènement de la socket, pour vraiment passer la main à 100%. Ah oui j'ai pensé renseigner la version du protocol utilisée à la place du channel_id de la trame pour la phase authentication, si le client n'a pas la bonne version, AuthManager renverra un joli BAD_PROTOCOL
    Ne pas oublier que la socket qui vient d'ête acceptée (côté serveur) ou qui vient de se connecter (côté client) doit être un minimum checkée pour s'assurer que l'autre bout est bien une TcpFrameSocket aussi. Par exemple ta TcpSocket, au moment où la connexion est établie/acceptée, envoie un premier bytearray constant pour dire à celle en face 'salut je suis une TcpSocket, pas un telnet, un navigateur web ou un scanport agressif d'un hackeur'

    Attention également à bien blinder certains points pour éviter les hacks. Genre si ta taille de message est codée sur un int, ça peut paraître malin au départ. Mais si je veux jouer à l'emmerdeur, je peux me connecter sur ton serveur et j'envoie une fausse trame avec comme taille 2^32 octets (4Go) et du 'garbage' à n'en plus finir ensuite. J'aurai vite fait de faire tomber ton serveur pour cause de memory full: il n'en finira plus de bufferiser le message à concurrence de 4Go par connexion et fake message envoyé puisque tant qu'il n'a pas atteint la taille du message indiquée dans le premier champ, il bufferise avant de construire une TcpFrame.
    Dans ce cas là, une taille genre sur 2 octets sera plus prudente: au pire, on limite les message à 2^16, soit 65ko maximum.

    Juste un petit doute sur qui possède qui dans l'histoire mais c'est du détail puisqu'il n'existe pas de solution idéale.
    Effectivement, c'est une solution parmi d'autre qui fonctionnera.


    A noter au passage qu'on n'a pas parler d'un point important: les déconnexions:

    - parfois, quand une connexion se perd, la socket de l'ordi en face met beaucoup, beaucoup de temps à le détecter. Pour éviter d'avoir des connexions fantômes, il faut mettre en place un mécanisme de 'keepalive': toutes les n secondes, chaque socket envoie un message (de taille 0 par exemple). Si l'entité en face n'a rien reçu au bout de n*2 secondes, elle considère que la connexion a merdé et déclare la socket comme déconnectée.

    - la déconnexion d'un client est un événement qui intéresse les couches 'hautes' (ie. code applicatif) de ton programme. Par exemple ton channel de Chat voudra pouvoir envoyer un message aux autres joueurs pour dire 'machin a quitté' ; ont Channel d'une partie en cours devra pouvoir saborder la partie, etc...
    C'est donc un événement qui doit être remonté par ton ConnexionPool (ou TcpClient) jusqu'aux couches en relation avec ton code applicatif (tes Channel) ; tu peux parfaitement suivre le même principe que les sauts de 'couche en couche' qu'on fait pour remonter un message reçu.
    Mon projet du moment: BounceBox, un jeu multijoueurs sur Freebox, sur PC et depuis peu sur smartphone/tablette Android.

  13. #13
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Merci pour tte ces précisions

  14. #14
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Salut salut,

    je reviens ici pour un avis. Je pensais naïvement pouvoir réutiliser les classes de mon serveur pour le client notamment la chaîne ChannelManager, Channel, ChannelWithUsers etc.

    Mais je constate que coté client, toute cette chaîne est un peu diffèrente, me trompe-je ? Avoir des ChannelManagerServer/Client, ChannelServer/Client... parrait-il abérant ?

    Ou alors "bidouiller" les classes pour qu'elles implémentent des méthodes Server et Client. Je ne sais pas vraiment.

    Merci d'avance

  15. #15
    Modérateur
    Avatar de nouknouk
    Homme Profil pro
    Inscrit en
    Décembre 2006
    Messages
    1 655
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France

    Informations forums :
    Inscription : Décembre 2006
    Messages : 1 655
    Points : 2 161
    Points
    2 161
    Par défaut
    Citation Envoyé par keyga Voir le message
    Mais je constate que coté client, toute cette chaîne est un peu diffèrente, me trompe-je ? Avoir des ChannelManagerServer/Client, ChannelServer/Client... parrait-il abérant ?
    Non, tu as parfaitement raison, en premier lieu car un serveur c'est fait pour gérer une multitude de clients (1 serveur, n connectés) et que le code client ne gère qu'un seul serveur (1 client - 1 serveur).

    Par contre, les classes de plus bas niveau (TcpFrameSocket, TcpFrame) ont vocation à être réutilisées à l'identique des deux côtés.
    Mon projet du moment: BounceBox, un jeu multijoueurs sur Freebox, sur PC et depuis peu sur smartphone/tablette Android.

  16. #16
    Membre du Club
    Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Avril 2009
    Messages
    65
    Détails du profil
    Informations personnelles :
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Avril 2009
    Messages : 65
    Points : 48
    Points
    48
    Par défaut
    Merci de me conforter dans mon idée
    Tu arrives à avoir un niveau d'abstraction entre la version Serveur et Client ? Je pensais au départ, mais vu le peu de chose en commun entre les 2 versions, channelID pour un Channel par exemple, je me pose des questions...

Discussions similaires

  1. Le réseau dans les jeux vidéo : Envoyer et recevoir des paquets
    Par LittleWhite dans le forum Développement 2D, 3D et Jeux
    Réponses: 0
    Dernier message: 03/05/2015, 19h10
  2. Réponses: 2
    Dernier message: 02/06/2012, 19h33
  3. structure des bases de données Palm
    Par nomdutilisateur dans le forum Bases de données
    Réponses: 2
    Dernier message: 17/01/2004, 17h47
  4. Structure des données en retour d'un DBExtract ?
    Par mikouts dans le forum XMLRAD
    Réponses: 4
    Dernier message: 24/01/2003, 15h15
  5. Redimensionnement des Paquets IP sur un Réseau Local
    Par Bonoboo dans le forum Développement
    Réponses: 2
    Dernier message: 12/07/2002, 15h40

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo