2013-02-21 26 views
6

Ho problemi a creare un'interfaccia di rete per un gioco molto semplice che ho realizzato in Xna. Avrei semplicemente bisogno di inviare oggetti tramite un client/socket TCP. Es: Ho una classe chiamata "Player". In ogni giocatore, c'è un nome di campo "Info", di tipo "PlayerInfo". Nel client/server, avrei bisogno di inviare le informazioni di ogni giocatore ad ogni cliente tranne quello che lo ha inviato (ovviamente). Questo è un semplice esempio, ma avrei bisogno di farlo con circa 5-10 oggetti, oltre a inviare gli aggiornamenti del giocatore (posizioni, azioni ...) C'è un modo semplice per farlo con TCP/Sock? Nota: valuterei le mie conoscenze in C# e programmare come 6/10, quindi non devi spiegare tutto se hai una soluzione (Es: qual è la differenza tra una variabile e un campo). So anche di interfacce, librerie e così via ... Grazie in anticipo!Invia oggetti digitati tramite TCP o socket

+0

Direi che avresti bisogno di creare la tua soluzione, un meccanismo di serializzazione ad alte prestazioni. Qualcosa sulla falsariga di un po 'di imballaggio sarebbe abbastanza veloce per piccole informazioni. – Machinarius

+0

XNA supporta WCF? Se è così, allora quella sarebbe la strada da percorrere. –

risposta

24

Ho un approccio che consiglierei e due minori che dipendono da molte cose.

Il primo implica che si sa già come utilizzare la classe Socket ma che ci sono molte classi che è necessario inviare attraverso di essa.

Dal punto di vista del trasporto è necessario creare/prendere in considerazione solo una classe molto semplice. Chiamiamo questa classe MyMessage:

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

Ok. Da un punto di vista TCP tutto ciò che devi fare è assicurarti di essere in grado di passare istanze di questa classe (dai client al server e viceversa). Non mi dilungherò nei dettagli per farlo, ma sottolineerò che se si riesce a fare ciò si trasforma la natura della connessione TCP/IP da "byte-stream" a "message-stream". Ciò significa che, in genere, TCP/IP non garantisce che i blocchi di dati inviati tramite una connessione arrivino a destinazione nelle stesse formazioni (potrebbero essere uniti o divisi).L'unica cosa che garantisce è che i byte di tutti i blocchi alla fine arrivino nello stesso ordine all'altro capo della connessione (sempre).

Ora che si dispone di un flusso di messaggi attivo e in esecuzione, è possibile utilizzare .NET vecchia serializzazione buona per incapsulare qualsiasi istanza di classe all'interno della proprietà Data. Quello che fa è serializzare i grafici degli oggetti in byte e viceversa.

Il modo in cui lo fate (più comunemente) è quello di utilizzare la classe di libreria standard: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter che può essere trovato in mscorlib.dll in questo modo:

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 è in grado di attraversare l'albero/grafico degli oggetti a partire dalla radice/sentinella fornita come secondo argomento del metodo Serialize (Stream, oggetto) e scrivere tutti i valori primitivi più le informazioni sul tipo e le relative informazioni sulla posizione il flusso fornito. È anche in grado di eseguire l'inverso e deserializzare un intero oggetto grafico purché il flusso fornito sia posizionato di conseguenza rispetto alla posizione di una serializzazione di un oggetto grafico precedente.

Tuttavia, ci sono alcune catture: è necessario annotare tutte le classi con [SerializableAttribute]. Se le classi contengono campi che sono di altre classi scritte da te, e tu hai detto che fanno:

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

allora avete bisogno di annotare quelli con [SerializableAttribute] troppo:

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

Se le classi contengono campi che sono di tipi scritti da altri (ad esempio Microsoft), allora quelli sarebbero meglio già annotati con l'attributo. La maggior parte di quelli che potrebbero essere serializzati lo sono già. I tipi primitivi sono naturalmente serializzabili. Le cose che non dovrebbero essere serializzati sono: filestreams, Fili, prese, ecc

Dopo essersi assicurati di avere classi serializzabili tutto quello che dovete fare è serializzare i loro casi, inviarli, li riceve e li deserializzare:

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 

} 

Mentre sul lato server:

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); 
    } 
} 

Avrete bisogno di un progetto di montaggio comune (libreria di classi) a cui fa riferimento entrambi i progetti eseguibili (client e server).

Tutte le classi che devono essere passate dovranno essere scritte in quell'assieme in modo che sia il server che il client sappiano come capirsi a questo livello molto dettagliato.

Se il server non ha bisogno di capire cosa viene detto tra i client e di passare solo i messaggi (trasmettendo un messaggio agli altri client N-1), dimentica ciò che ho detto sull'assemblaggio comune. In quel caso particolare, il server vede solo i byte, mentre i client hanno una comprensione più profonda dei messaggi effettivi inviati avanti e indietro.

Ho detto che avevo tre approcci.

Il secondo include .NET Remoting che può richiedere molto lavoro alle spalle ma che è difficile da vivere se non lo capisci completamente.Puoi leggere su MSDN, qui: http://msdn.microsoft.com/en-us/library/kwdt6w2k(v=vs.100).aspx

Il terzo sarebbe migliore solo se (ora o in futuro) di XNA intendi Windows Phone o un'altra implementazione di XNA che non supporta la classe BinaryFormatter (Esci con MonoTouch o altri). In questo caso avresti delle difficoltà se avessi bisogno del tuo server (una vera e propria applicazione .NET vecchio stile) per fare riferimento all'assembly comune di cui ho parlato e avere anche il progetto di gioco (che non sarebbe un buon vecchio stile App .NET ma hanno una natura piuttosto esotica) fanno riferimento allo stesso identico assemblaggio.

In tal caso, è necessario utilizzare e alternare la forma di serializzazione e deserializzazione dei propri oggetti. Dovresti anche implementare identicamente due insiemi di classi nei due mondi (.NET e WP7 o WP8). È possibile utilizzare una qualche forma di serializzatori XML che è necessario associare alle classi in modo esplicito (non potente come la classe BinaryFormatter ma più versatile in quale potrebbe essere la natura del runtime che ospita le classi).

Si può leggere sulla classe XmlSerializer su MSDN, qui: http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx

+0

Tutte queste soluzioni sono molto interessanti, ma l'unico problema che posso pensare è come scoprire di che tipo si trova sul lato ricevitore? Se ho deserializzato qualcosa, sarò in grado di recuperare il tipo per un cast? Grazie per tutte le risposte! Molto utile. –

+0

Se si utilizza l'approccio BinaryFormatter, in qualche modo sarà come se le due controparti fossero nello stesso processo. In breve: hai un problema classico di lanciare un sacco di classi per obiettare e poi cercare di capire quali siano realmente gli oggetti (Giocatore, Movimento, Interazione?). Quindi se usi qualcosa come se (obj is Player) {Player asPlayer = obj come Player;/* esegui l'elaborazione specifica del giocatore */risolvi la tua domanda. Il metodo deserialize sa già (in fase di esecuzione) cosa è necessario solo per la spedizione (è un giocatore o no, è una mossa, ecc.). –

+0

E 'solo che logicamente parlando, Microsoft non avrebbe mai immaginato che tu, nel febbraio 2013, avresti scritto una classe di giocatori e ne avresti deserializzato. Quindi a livello di tempo sintattico, scritto, desing, il metodo restituisce l'oggetto. –

3

È possibile creare la propria soluzione utilizzando le varie classi fornite nel framework .net. Si consiglia di eseguire il checkout del WCF o del namepsace Sockets, in particolare le classi TcpClient e TcpListener, vedere MSDN. Ci sono un sacco di fantastici tutorial se fai una ricerca relativa all'uso di questi. Dovresti anche considerare come trasformare gli oggetti digitati in array di byte, in modo simile a questo question.

Un approccio alternativo sarebbe utilizzare una libreria di rete. Ci sono librerie di basso livello e librerie di alto livello. Dato il tuo livello di esperienza di programmazione e l'obiettivo finale specifico suggerirei una libreria di alto livello. Un esempio di tale libreria di rete sarebbe lidgren. Sono lo sviluppatore di un altro libreria di rete networkComms.net e un rapido esempio di come è possibile inviare oggetti digitato con questa libreria segue:

Shared Base (definisce oggetto 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 (invia un singolo oggetto 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(); 
     } 
    } 
} 

Server:

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(); 
     } 
    } 
} 

Quanto sopra è una versione modificata di questo tutorial. Ovviamente è necessario scaricare la DLL NetworkCommsDotNet dal sito Web in modo che sia possibile aggiungerla nel riferimento "using NetworkCommsDotNet". Vedi anche l'indirizzo IP del server nell'esempio del client è attualmente "127.0.0.1", questo dovrebbe funzionare se si esegue sia il server che il client sulla stessa macchina.

+0

Hmm. Interessante. –

8

La mia soluzione rapida e pulita-personale, utilizzando 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>(); 
    } 
} 

Ora è possibile serializzare gli oggetti in questo modo :

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

inviare tali dati attraverso il filo e poi l'altra estremità si può fare qualcosa di simile:

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... 
+0

Tutte queste soluzioni sono molto interessanti, ma l'unico problema che posso pensare è come scoprire di che tipo si trova sul lato ricevitore? Se ho deserializzato qualcosa, sarò in grado di recuperare il tipo per un cast? Grazie per tutte le risposte! Molto utile. –

+2

Il metodo che ho delineato risolve esattamente il tuo problema, ovvero come determinare il tipo. Guarda come dico 'message.Type == typeof (Player)' o 'message.Type == typeof (Enemy)' nell'esempio? Ecco come "recuperi il tipo per un cast". –

1

Dopo oltre 2 anni, ho trovato nuovi modi per risolvere questo problema e ho pensato che condividerlo potrebbe essere utile a qualcuno. Si prega di notare che la risposta accettata è ancora valida.

Un modo più semplice per serializzare gli oggetti digitati che ho trovato è utilizzando il convertitore json in Json.NET. C'è un oggetto impostazioni che ti permette di memorizzare il tipo in JSON come un valore chiamato $type. Ecco come farlo e il JSON risultante:

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

JsonConvert.SerializeObject(myObject, settings); 

risultato JSON:

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

Quando deserializzazione, se si utilizza la stessa impostazione, un oggetto di tipo corretto sarà deserializzato. Esattamente quello di cui avevo bisogno! Spero che questo ti aiuti.

Problemi correlati