Existe-t-il un modèle pour écrire un serveur au tour par tour communiquant avec n clients via des sockets?


14

Je travaille sur un serveur de jeux générique qui gère les jeux pour un nombre arbitraire de clients en réseau TCP jouant sur un jeu. J'ai un «design» piraté avec du ruban adhésif qui fonctionne, mais qui semble à la fois fragile et rigide. Existe-t-il un modèle bien établi pour la façon d'écrire une communication client / serveur qui est robuste et flexible? (Sinon, comment amélioreriez-vous ce que j'ai ci-dessous?)

En gros, j'ai ceci:

  • Lors de la configuration d'un jeu, le serveur a un thread pour chaque socket de joueur, gérant les requêtes synchrones d'un client et les réponses du serveur.
  • Une fois le jeu en cours, cependant, tous les threads à l'exception d'un sommeil, et ce thread passe en revue tous les joueurs un à la fois pour communiquer sur leur mouvement (en inversant la requête-réponse).

Voici un diagramme de ce que j'ai actuellement; cliquez pour une version plus grande / lisible, ou PDF 66kB .

Diagramme de séquence du flux

Problèmes:

  • Cela nécessite que les joueurs répondent exactement tour à tour avec exactement le bon message. (Je suppose que je pourrais laisser chaque joueur répondre avec des conneries aléatoires et ne passer à autre chose qu'une fois qu'il me donne un coup valide.)
  • Il ne permet pas aux joueurs de parler au serveur à moins que ce soit leur tour. (Je pourrais demander au serveur de leur envoyer des mises à jour sur les autres joueurs, mais pas traiter une demande asynchrone.)

Exigences finales:

  • La performance n'est pas primordiale. Cela sera principalement utilisé pour les jeux non en temps réel, et surtout pour opposer les IA les uns aux autres, pas aux humains nerveux.

  • Le jeu se fera toujours au tour par tour (même à très haute résolution). Chaque joueur reçoit toujours un coup traité avant que tous les autres joueurs aient un tour.

L'implémentation du serveur se trouve être en Ruby, si cela fait une différence.


Si vous utilisez un socket TCP par client, cette limite ne s'applique-t-elle pas aux clients (65535-1024)?
o0 '.

1
Pourquoi est-ce un problème, Lohoris? La plupart des gens auront du mal à obtenir 6000 utilisateurs simultanés, sans parler de 60000.
Kylotan

@Kylotan En effet. Si j'ai plus de 10 IA simultanées, je serai surpris. :)
Phrogz

Par curiosité, quel outil avez-vous utilisé pour créer ce diagramme?
matthias

@matthias Malheureusement, c'était trop de travail personnalisé dans Adobe Illustrator.
Phrogz

Réponses:


10

Je ne sais pas exactement ce que vous voulez réaliser. Mais, il y a un modèle qui est utilisé en permanence sur les serveurs de jeux et qui peut vous aider. Utilisez des files d'attente de messages.

Pour être plus précis: lorsque les clients envoient des messages au serveur, ne les traitez pas immédiatement. Au lieu de cela, analysez-les et placez-les dans une file d'attente pour ce client spécifique. Ensuite, dans une boucle principale (peut-être même sur un autre thread), parcourez tour à tour tous les clients, récupérez les messages de la file d'attente et traitez-les. Lorsque le traitement indique que le tour de ce client est terminé, passez au suivant.

De cette façon, les clients ne sont pas tenus de travailler strictement tour à tour; seulement assez rapidement pour que vous ayez quelque chose dans la file d'attente au moment où le client est traité (vous pouvez, bien sûr, attendre le client ou sauter son tour s'il est en retard). Et vous pouvez ajouter la prise en charge des demandes asynchrones en ajoutant une file d'attente «asynchrone»: lorsqu'un client envoie une demande spéciale, elle est ajoutée à cette file d'attente spéciale; cette file d'attente est vérifiée et traitée plus souvent que celle des clients.


1

Les threads matériels ne sont pas suffisamment évolués pour faire de «un par joueur» une idée raisonnable pour un nombre de joueurs à 3 chiffres, et la logique de savoir quand les réveiller est une complexité qui va grandir. Une meilleure idée est de trouver un package d'E / S asynchrone pour Ruby qui vous permettra d'envoyer et de recevoir des données sans avoir à mettre en pause un thread de programme entier pendant l'opération de lecture ou d'écriture. Cela résout également le problème d'attendre que les joueurs répondent, car vous n'aurez aucun fil suspendu à une opération de lecture. Au lieu de cela, votre serveur peut simplement vérifier si un délai a expiré et informer l'autre joueur en conséquence.

Fondamentalement, les «E / S asynchrones» sont le «modèle» que vous recherchez, bien qu'il ne s'agisse pas vraiment d'un modèle, mais plutôt d'une approche. Au lieu d'appeler explicitement `` read '' sur un socket et de suspendre le programme jusqu'à ce que les données arrivent, vous configurez le système pour appeler votre gestionnaire `` onRead '' chaque fois qu'il a des données prêtes, et vous continuez le traitement jusqu'à ce moment.

chaque prise a un tour

Chaque joueur a son tour, et chaque joueur a une prise qui envoie des données, ce qui est un peu différent. Un jour, vous ne voudrez peut-être pas une prise par joueur. Vous pourriez ne pas utiliser du tout de sockets. Séparez les domaines de responsabilité. Désolé si cela semble être un détail sans importance, mais lorsque vous confondez des concepts dans votre conception qui devraient être différents, cela rend plus difficile la recherche et la discussion de meilleures approches.


1

Il y a certainement plus d'une façon de le faire, mais personnellement, je sauterais complètement les threads séparés et utiliserais simplement une boucle d'événement. La façon dont vous procédez dépendra quelque peu de la bibliothèque d'E / S que vous utilisez, mais en gros, votre boucle de serveur principale ressemblera à ceci:

  1. Configurez un pool de connexions et un socket d'écoute pour les nouvelles connexions.
  2. Attendez que quelque chose se passe.
  3. Si le quelque chose est une nouvelle connexion, ajoutez-le au pool.
  4. Si le quelque chose est une demande d'un client, vérifiez si c'est quelque chose que vous pouvez gérer immédiatement. Si oui, faites-le; sinon, mettez-le dans une file d'attente et (facultativement) envoyez un accusé de réception au client.
  5. Vérifiez également s'il y a quelque chose dans la file d'attente que vous pouvez gérer maintenant; si oui, faites-le.
  6. Revenez à l'étape 2.

Par exemple, disons que vous avez n clients impliqués dans un jeu. Lorsque les n-1 premiers envoient leurs coups, vérifiez simplement que le coup semble valide et renvoyez un message disant que le coup a été reçu mais vous attendez toujours que d'autres joueurs bougent. Une fois que tous les n joueurs ont déménagé, vous traitez tous les mouvements que vous avez enregistrés et envoyez les résultats à tous les joueurs.

Vous pouvez également affiner ce schéma pour inclure les délais d'expiration - la plupart des bibliothèques d'E / S doivent disposer d'un mécanisme permettant d'attendre l'arrivée de nouvelles données ou un laps de temps donné.

Bien sûr, vous pouvez également implémenter quelque chose comme ça avec des threads individuels pour chaque connexion, en faisant en sorte que ces threads transmettent toutes les requêtes qu'ils ne peuvent pas gérer directement à un thread central (soit un par jeu, soit un par serveur) qui exécute une boucle comme montré ci-dessus, sauf qu'il parle avec les threads du gestionnaire de connexion plutôt qu'avec directement les clients. Que vous trouviez cela plus simple ou plus compliqué que l'approche à un seul thread, cela dépend vraiment de vous.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.