web-dev-qa-db-fra.com

Envoyer des objets dactylographiés via TCP ou Sockets

Je ne parviens pas à créer une interface réseau pour un jeu très simple que j'ai créé sous Xna. J'aurais simplement besoin d'envoyer des objets via un client/socket TCP. Ex: J'ai une classe nommée «Player». Dans chaque joueur, il y a un nom de champ «Info» de type «PlayerInfo». Dans le client/serveur, je devrais envoyer les informations de chaque joueur à chaque client sauf celui qui l’a envoyé (évidemment). Ceci est un exemple simple, mais j’aurais besoin de le faire avec environ 5 - 10 objets, plus l'envoi des mises à jour du joueur (positions, actions, etc.) Existe-t-il un moyen simple de le faire avec TCP/Sock? Remarque: J'évalue mes connaissances en C #. et la programmation en 6/10, vous n'avez donc pas besoin de tout expliquer si vous avez une solution (Ex: quelle est la différence entre une variable et un champ). Je connais aussi les interfaces, les bibliothèques, etc.… Merci d'avance!

9
Philippe Paré

J'ai une approche que je recommanderais et deux autres qui dépendent de beaucoup de choses.

La première implique que vous savez déjà comment utiliser la classe Socket mais que vous avez besoin de nombreuses classes pour la transmettre.

Du point de vue du transport, vous devez créer/prendre en compte une classe très simple. Appelons cette classe MyMessage:

public class MyMessage {
  public byte[] Data { get; set; }
}

D'accord. D'un point de vue TCP, il vous suffit de vous assurer que vous êtes en mesure de faire passer des instances de cette classe (des clients au serveur et inversement). Je n'entrerai pas dans les détails, mais je ferai remarquer que si vous y parvenez, vous transformez la nature de la connexion TCP/IP de "flux d'octets" à "flux de messages". Cela signifie que normalement, TCP/IP ne garantit pas que les blocs de données que vous envoyez via une connexion arrivent à destination dans les mêmes formations (ils peuvent être joints ou séparés). La seule chose qui soit garantie, c’est que les octets de tous les morceaux finiront par arriver dans le même ordre à l’autre extrémité de la connexion (toujours).

Maintenant que vous avez un flux de messages opérationnel, vous pouvez utiliser la bonne vieille sérialisation .NET pour encapsuler toute instance de classe dans la propriété Data. Qu'est-ce que cela fait est qu'il sérialise les graphes d'objet en octets et vice-versa.

La façon dont vous faites cela (le plus souvent) consiste à utiliser la classe de bibliothèque standard: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter Qui peut être trouvée dans mscorlib.dll comme ceci:

public static class Foo {

  public static Message Serialize(object anySerializableObject) {
    using (var memoryStream = new MemoryStream()) {
      (new BinaryFormatter()).Serialize(memoryStream, anySerializableObject);
      return new Message { Data = memoryStream.ToArray() };
    }
  }

  public static object Deserialize(Message message) {
    using (var memoryStream = new MemoryStream(message.Data))
      return (new BinaryFormatter()).Deserialize(memoryStream);
  }

}

La classe BinaryFormatter est capable de parcourir l’arborescence/le graphe des objets en partant de la racine/sentinelle fournie en tant que deuxième argument de la méthode Serialize (Stream, object) et d’écrire toutes les valeurs primitives plus les informations de type et de position relative. stream. Il est également capable de faire l'inverse exact et de désérialiser un graphe d'objet entier tant que le flux fourni est positionné en conséquence à la place d'une ancienne sérialisation de graphe d'objet.

Cependant, il y a quelques captures: vous devrez annoter toutes vos classes avec [SerializableAttribute]. Si vos classes contiennent des champs d'autres classes écrites par vous et que vous avez répondu oui:

[SerializableAttribute]
public class Player {
  public PlayerInfo Info; 
  //... etc 

alors vous devez aussi annoter ceux avec [SerializableAttribute]:

[SerializableAttribute]
public class PlayerInfo { //... etc

Si vos classes contiennent des champs de types écrits par d'autres (disons Microsoft), il serait préférable que ceux-ci soient déjà annotés avec l'attribut. La plupart de ceux qui pourraient déjà être sérialisés le sont. Les types primitifs sont naturellement sérialisables. Les choses qui ne devraient pas être sérialisées sont: FileStreams, Threads, Sockets, etc.

Après vous être assuré d'avoir des classes sérialisables, vous n'avez plus qu'à sérialiser leurs instances, les envoyer, les recevoir et les désérialiser:

class Client {

  public static void SendMovement(Movement movement) {
    Message message = Foo.Serialize(movement);

    socketHelper.SendMessage(message);
  }
  public static void SendPlayer(Player player) {
    Message message = Foo.Serialize(player);

    socketHelper.SendMessage(message);
  }
  // .. etc

  public static void OnMessageReceivedFromServer(Message message) {
    object obj = Foo.Deserialize(message);
    if (obj is Movement)
      Client.ProcessOtherPlayersMovement(obj as Movement);
    else if (obj is Player)
      Client.ProcessOtherPlayersStatusUpdates(obj as Player);
    // .. etc
  }

  public static void ProcessOtherPlayersMovement(Movement movement) {
    //...
  }
  // .. etc

}

Alors que du côté du serveur:

class Server {

  public static void OnMessageReceived(Message message, SocketHelper from, SocketHelper[] all) {
    object obj = Foo.Deserialize( message );
    if (obj is Movement)
      Server.ProcessMovement( obj as Movement );
    else if (obj is Player)
      Server.ProcessPlayer( obj as Player );
    // .. etc

    foreach (var socketHelper in all)
      if (socketHelper != from)
        socketHelper.SendMessage( message );
  }
}

Vous aurez besoin d'un projet Assembly commun (bibliothèque de classes) à référencer par les deux projets exécutables (client et serveur).

Toutes vos classes qui doivent être échangées devront être écrites dans cette assemblée pour que le serveur et le client sachent se comprendre à ce niveau très détaillé.

Si le serveur n'a pas besoin de comprendre ce qui se dit entre les clients et de ne faire passer que des messages (diffuser un message aux autres clients N-1), oubliez ce que j'ai dit à propos de l'Assemblée commune. Dans ce cas particulier, le serveur ne voit que des octets, tandis que les clients ont une compréhension plus profonde des messages réellement échangés.

J'ai dit que j'avais trois approches.

La seconde concerne .NET Remoting, qui vous enlève beaucoup de travail mais qui est difficile à vivre si vous ne le comprenez pas bien. Vous pouvez en savoir plus sur MSDN à l'adresse suivante: http://msdn.Microsoft.com/en-us/library/kwdt6w2k(v=vs.100).aspx

Le troisième ne serait préférable que si (maintenant ou dans le futur) de XNA vous entendez Windows Phone ou une autre implémentation de XNA qui ne supporte pas la classe BinaryFormatter (ExEn avec MonoTouch, ou autres). Au cas où vous auriez du mal à utiliser votre serveur (une application .NET démodée et complète) pour faire référence à l’assemblage commun dont j’ai parlé et pour avoir également le projet de jeu (qui ne serait pas une bonne application .NET démodée mais avoir une nature plutôt exotique) font référence à la même assemblée.

Dans ce cas, nous aurions besoin d'utiliser et de changer de forme de sérialisation et de désérialisation de vos objets. Vous auriez également besoin d'implémenter de manière identique deux ensembles de classes dans les deux mondes (.NET et WP7 ou WP8). Vous pouvez utiliser une forme de sérialiseur XML dont vous aurez besoin pour mapper explicitement vos classes (pas aussi puissant que la classe BinaryFormatter mais plus polyvalent en ce qui concerne la nature du runtime qui héberge vos classes).

Pour en savoir plus sur la classe XmlSerializer sur MSDN, cliquez ici: http://msdn.Microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx

28
Eduard Dumitru

Ma solution personnelle rapide et propre, utilisant JSON.NET:

class JMessage
{
    public Type Type { get; set; }
    public JToken Value { get; set; }

    public static JMessage FromValue<T>(T value)
    {
        return new JMessage { Type = typeof(T), Value = JToken.FromObject(value) };
    }

    public static string Serialize(JMessage message)
    {
        return JToken.FromObject(message).ToString();
    }

    public static JMessage Deserialize(string data)
    {
        return JToken.Parse(data).ToObject<JMessage>();
    }
}

Vous pouvez maintenant sérialiser vos objets de la manière suivante:

Player player = ...;
Enemy enemy = ...;
string data1 = JMessage.Serialize(JMessage.FromValue(player));
string data2 = JMessage.Serialize(JMessage.FromValue(enemy));

Envoyez ces données sur le réseau puis, à l’autre bout, vous pouvez faire quelque chose comme:

string data = ...;
JMessage message = JMessage.Deserialize(data);
if (message.Type == typeof(Player))
{
    Player player = message.Value.ToObject<Player>();
}
else if (message.Type == typeof(Enemy))
{
    Enemy enemy = message.Value.ToObject<Enemy>();
}
//etc...
8
Timothy Shields

Vous pouvez créer votre propre solution à l'aide des différentes classes fournies dans le framework .net. Vous voudriez extraire WCF ou Sockets namepsace, en particulier les classes TcpClient et TcpListener, voir MSDN . Il existe de très nombreux tutoriels si vous effectuez une recherche liée à leur utilisation. Vous devrez également examiner comment transformer vos objets typés en tableaux d'octets, semblables à ceci question .

Une approche alternative consisterait à utiliser une bibliothèque réseau. Il existe des bibliothèques de bas niveau et des bibliothèques de haut niveau. Compte tenu de votre niveau d'expérience en programmation et de votre objectif final, je suggérerais une bibliothèque de haut niveau. Un exemple d'une telle bibliothèque de réseau serait lidgren . Je suis le développeur d'une autre bibliothèque réseau networkComms.net et voici un exemple rapide illustrant comment envoyer des objets typés à l'aide de cette bibliothèque:

Base partagée (définit l'objet Player):

[ProtoContract]
class Player
{
    [ProtoMember(1)]
    public string Name { get; private set; }
    [ProtoMember(2)]
    public int Ammo { get; private set; }
    [ProtoMember(3)]
    public string Position { get; private set; }

    private Player() { }

    public Player(string name, int ammo, string position)
    {
        this.Name = name;
        this.Ammo = ammo;
        this.Position = position;
    }
}

Client (envoie un seul objet Player):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using NetworkCommsDotNet;
using ProtoBuf;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            Player player = new Player("MarcF", 100, "09.09N,21.12W");

            //Could also use UDPConnection.GetConnection...
            TCPConnection.GetConnection(new ConnectionInfo("127.0.0.1", 10000)).SendObject("PlayerData", player);

            Console.WriteLine("Send completed. Press any key to exit client.");
            Console.ReadKey(true);
            NetworkComms.Shutdown();
        }
    }
}

Serveur:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using NetworkCommsDotNet;
using ProtoBuf;

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            // Convert incoming data to a <Player> object and run this method when an incoming packet is received.
            NetworkComms.AppendGlobalIncomingPacketHandler<Player>("PlayerData", (packetHeader, connection, incomingPlayer) =>
            {
                Console.WriteLine("Received player data. Player name was " + incomingPlayer.Name);
                //Do anything else with the player object here
                //e.g. UpdatePlayerPosition(incomingPlayer);
            });

            //Listen for incoming connections
            TCPConnection.StartListening(true);

            Console.WriteLine("Server ready. Press any key to shutdown server.");
            Console.ReadKey(true);
            NetworkComms.Shutdown();
        }
    }
}

Ce qui précède est une version modifiée de ce tutorial . Vous devrez évidemment télécharger NetworkCommsDotNet DLL depuis le site Web afin de pouvoir l'ajouter à la référence 'à l'aide de NetworkCommsDotNet'. Voir également que l'adresse IP du serveur dans l'exemple de client est actuellement "127.0.0.1", cela devrait fonctionner si vous exécutez le serveur et le client sur le même ordinateur.

3
MarcF

Après plus de 2 ans, j'ai trouvé de nouvelles façons de résoudre ce problème et je pensais que le partager pourrait être utile à quelqu'un. Veuillez noter que la réponse acceptée est toujours valide.

Le moyen le plus simple de sérialiser les objets typés que j'ai pu trouver consiste à utiliser le convertisseur JSON dans Json.NET. Il existe un objet de paramètres qui vous permet de stocker le type dans le JSON sous la forme d'une valeur nommée $type. Voici comment le faire et le JSON résultant:

JsonSerializerSettings settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
};

JsonConvert.SerializeObject(myObject, settings);

Résultat Json:

{
    "$type" : "Testing.MyType, Testing",
    "ExampleProperty" : "Hello world!"
}

Lors de la désérialisation, si le même paramètre est utilisé, un objet du type correct sera désérialisé. Exactement ce dont j'avais besoin! J'espère que cela t'aides.

1
Philippe Paré