2013-02-21 11 views
6

Mam problem z utworzeniem interfejsu sieciowego dla bardzo prostej gry, którą utworzyłem w Xna. Po prostu musiałbym wysłać obiekty za pośrednictwem klienta/gniazda TCP. Np .: Mam klasę o nazwie "Gracz". W każdym odtwarzaczu jest nazwa pola "Informacje" typu "PlayerInfo". W kliencie/serwerze muszę przesłać informacje o każdym graczu każdemu klientowi oprócz tego, który go wysłał (oczywiście). To jest prosty przykład, ale musiałbym to zrobić z około 5 - 10 obiektami, a także wysyłanie aktualizacji odtwarzacza (pozycje, akcje ...). Czy istnieje prosty sposób, aby to zrobić za pomocą TCP/Sock? Uwaga: Oceniłbym swoją wiedzę w języku C# i programowanie jako 6/10, więc nie musisz wyjaśniać wszystkiego, jeśli masz rozwiązanie (np. Jaka jest różnica pomiędzy zmienną a polem). Wiem również o interfejsach, bibliotekach i tak dalej ... Z góry dziękuję!Wysyłaj wpisane obiekty za pośrednictwem tcp lub gniazd

+0

Powiedziałbym, że trzeba stworzyć własne rozwiązanie, mechanizm serializacji o wysokiej wydajności. Coś na wzór pakowania bitowego byłoby wystarczająco szybkie dla małych informacji. – Machinarius

+0

Czy XNA obsługuje WCF? Jeśli tak, to byłaby to właściwa droga. –

Odpowiedz

24

Mam jedno podejście, które polecam i dwa mniejsze, które są zależne od wielu rzeczy.

Pierwsza z nich oznacza, że ​​już wiesz, jak używać klasy Socket, ale masz wiele klas, które musisz przesłać.

Z punktu widzenia transportu powinieneś utworzyć/wziąć pod uwagę tylko jedną bardzo prostą klasę. Nazwijmy tę klasę MyMessage:

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

Ok. Z punktu widzenia TCP wszystko, co musisz zrobić, to upewnić się, że możesz przekazywać instancje tej klasy (od klientów do serwera iz powrotem). Nie będę zagłębiać się w szczegóły tego działania, ale zwrócę uwagę, że jeśli to zrobisz, przekształcisz naturę połączenia TCP/IP z "bajtu-strumienia" w "strumień wiadomości". Oznacza to, że zwykle protokół TCP/IP nie gwarantuje, że porcje danych wysyłanych przez połączenie docierają do miejsca docelowego w tych samych formacjach (mogą zostać połączone lub rozdzielone).Jedyne, co gwarantuje to, że bajty wszystkich fragmentów ostatecznie dotrą w tej samej kolejności na drugim końcu połączenia (zawsze).

Po uruchomieniu strumienia komunikatów można użyć starej dobrej serializacji .NET do enkapsulacji dowolnego wystąpienia klasy we właściwości Data. Dzieje się tak, ponieważ serializuje wykresy obiektów w bajtach i na odwrót.

Sposób to zrobić (najczęściej) jest użycie standardowej klasy Library: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter który można znaleźć w pliku mscorlib.dll tak:

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

} 

Klasa BinaryFormatter może przechodzić przez drzewa/wykresy obiektów rozpoczynając od głównego katalogu/sentinela podanego jako drugi argument metody Serialize (Stream, object) i zapisywać wszystkie pierwotne wartości oraz informacje o typie i względne informacje o pozycji do podany strumień. Jest również w stanie dokonać dokładnego odwrócenia i deserializacji całego wykresu obiektu, o ile podany strumień jest ustawiony zgodnie z miejscem serializacji byłych obiektów.

Jest tu jednak kilka chwytów: będziesz musiał opisać wszystkie klasy za pomocą [SerializableAttribute]. Jeśli zajęcia zawierają pola, które są z innych klas napisanych przez ciebie, a ty powiedziałeś robią:

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

to trzeba opisywanie tych z [SerializableAttribute] też:

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

Jeśli zajęcia zawierają pola należące do typów pisanych przez innych (np. Microsoft), to te lepiej już zostały opatrzone adnotacją z atrybutem. Większość z tych, które można już serializować, to. Rodzaje prymitywów są w naturalny sposób szeregowalne. Rzeczy, które nie powinny być szeregowane są: FileStreams, Nici, gniazda, itp

Po upewnieniu masz zajęcia SERIALIZABLE wszystko, co trzeba zrobić, to szeregować swoje wystąpienia, wysłać je odbierać je i deserializowania je:

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 

} 

Choć po stronie serwera:

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

Potrzebny jest projekt wspólnego zespołu (biblioteka klasy), aby odwoływać się zarówno projekty wykonywalnych (klient i serwer).

Wszystkie klasy, które trzeba przekazać, będą musiały zostać zapisane w tym zestawie, tak aby zarówno serwer, jak i klient wiedzieli, jak rozumieć się nawzajem na tym bardzo szczegółowym poziomie.

Jeśli serwer nie musi rozumieć, co jest powiedziane między klientami, a jedynie przekazywać wiadomości (transmitując jedną wiadomość do innych klientów N-1), to zapomnij o tym, co powiedziałem o wspólnym zespole. W tym konkretnym przypadku serwer widzi tylko bajty, podczas gdy klienci mają głębsze zrozumienie faktycznych wiadomości wysyłanych tam iz powrotem.

Powiedziałem, że mam trzy podejścia.

Drugi z nich to .NET Remoting, który może wymagać dużego nakładu pracy, ale który jest trudny do przeżycia, jeśli nie rozumie się go w pełni.Możesz przeczytać o tym na MSDN, tutaj: http://msdn.microsoft.com/en-us/library/kwdt6w2k(v=vs.100).aspx

Trzeci byłby lepszy tylko wtedy, gdy (teraz lub w przyszłości) przez XNA masz na myśli Windows Phone lub inną implementację XNA, która nie obsługuje klasy BinaryFormatter (ExEn z MonoTouch lub innymi). W takim przypadku byłoby ci ciężko, gdybyś potrzebował swojego serwera (pełnej, staroświeckiej aplikacji .NET), aby odnieść się do wspólnego zgromadzenia, o którym mówiłem, a także mieć projekt gry (który nie byłby dobrym staromodnym Aplikacja .NET, ale ma raczej egzotyczną naturę) odwołuje się do tego samego zestawu.

W takim przypadku będziemy musieli użyć i na przemian formę serializacji i deserializacji obiektów. Konieczne jest również identyczne wdrożenie dwóch zestawów klas w dwóch światach (.NET i WP7 lub WP8). Możesz użyć jakiejś postaci serializatorów XML, które musiałbyś jawnie odwzorować na swoje klasy (nie tak potężne, jak klasa BinaryFormatter, ale bardziej uniwersalne w tym, co może być natura środowiska wykonawczego hostującego twoje klasy).

Można przeczytać o klasie XMLSerializer na MSDN, tutaj: http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx

+0

Wszystkie te rozwiązania są bardzo interesujące, ale jedyny problem, jaki mogę wymyślić, to: Jak się dowiedzieć, jaki to jest typ po stronie odbiorcy? Jeśli deserializuję coś, czy będę mógł pobrać typ dla obsady? Dzięki za wszystkie odpowiedzi! Bardzo pomocne. –

+0

Jeśli użyjesz podejścia BinaryFormatter, będzie to wyglądało tak, jakby dwa odpowiedniki były w tym samym procesie. W skrócie: masz podstawowy problem z rzuceniem grup przedmiotów na obiekt, a następnie próbujesz dowiedzieć się, czym naprawdę są przedmioty (gracz, ruch, interakcja?). Więc jeśli użyjesz czegoś takiego jak (obj to Player) {Player asPlayer = obj jako Player;/* wykonaj konkretne przetwarzanie odtwarzacza */rozwiążesz swoje pytanie. Metoda deserializacji już wie (w czasie wykonywania) to, czego potrzebujesz tylko do wysłania (to gracz, czy nie, it'a ruch, itp.). –

+0

Po prostu logicznie rzecz biorąc, Microsoft nie mógł odgadnąć, że Ty, w lutym 2013 r., Napiszesz klasę gracza i będziesz potrzebował jej deserializacji. Tak więc na poziomie syntaktycznym, pisemnym, desingowym metoda ta zwraca obiekt. –

3

Można utworzyć własne rozwiązanie, korzystając z różnych klas dostępnych w ramach .net. Użytkownik chciałby sprawdzić nazwę usługi WCF lub nazwy gniazd, w szczególności klasy TcpClient i TcpListener, patrz: MSDN. Jest mnóstwo świetnych samouczków, jeśli wykonasz wyszukiwanie związane z ich użyciem. Trzeba również rozważyć, jak przekształcić wpisane obiekty w tablice bajtów, podobne do tego question.

Alternatywnym podejściem byłoby korzystanie z biblioteki sieciowej. Istnieją biblioteki niskiego poziomu i biblioteki wysokiego poziomu. Biorąc pod uwagę twój poziom doświadczenia w programowaniu i konkretny cel końcowy sugerowałbym bibliotekę wysokiego poziomu. Przykładem takiej biblioteki sieciowej może być lidgren. Jestem twórcą innej biblioteki sieciowej networkComms.net i szybki przykład jak można wysłać wpisane obiektów za pomocą tej biblioteki następująco:

Shared podstawa (definiuje przedmiot 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 (wysyła pojedynczy celem gracza):

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

Serwer:

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

Powyższe jest zmodyfikowaną wersją tego tutorial. Oczywiście będziesz musiał pobrać bibliotekę DLL NetworkCommsDotNet ze strony internetowej, aby można było dodać ją do referencji "using NetworkCommsDotNet". Zobacz również adres IP serwera w przykładzie klienta jest obecnie "127.0.0.1", powinno to działać, jeśli uruchomisz zarówno serwer jak i klienta na tym samym komputerze.

+0

Hmm. Ciekawy. –

8

moja osobista szybkiego i oczyszczone rozwiązania, korzystając 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>(); 
    } 
} 

Teraz można szeregować swoje obiekty tak jak :

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

Wyślij tych danych w całej drutu, a następnie na drugim końcu można zrobić coś takiego:

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

Wszystkie te rozwiązania są bardzo interesujące, ale jedyny problem, jaki mogę wymyślić, to: Jak się dowiedzieć, jaki jest typ po stronie odbiorcy? Jeśli deserializuję coś, czy będę mógł pobrać typ dla obsady? Dzięki za wszystkie odpowiedzi! Bardzo pomocne. –

+2

Opisana przeze mnie metoda rozwiązuje dokładnie twój problem, a mianowicie, jak określić typ. Zobacz jak mówię 'message.Type == typeof (Player)' lub 'message.Type == typeof (Enemy)' w przykładzie? W ten sposób "pobierasz typ do obsady". –

1

Po ponad 2 latach znalazłem nowe sposoby rozwiązania tego problemu i pomyślałem, że udostępnienie go może być przydatne dla kogoś. Pamiętaj, że zaakceptowana odpowiedź jest nadal ważna.

Najprostszym sposobem serializacji wpisanych obiektów, które mogłem znaleźć, jest użycie konwertera json w Json.NET. Istnieje obiekt ustawień, który umożliwia zapisanie typu w jsonie jako wartości o nazwie $type. Oto jak to zrobić i otrzymaną json:

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

JsonConvert.SerializeObject(myObject, settings); 

Json wynik:

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

Podczas deserializacji, jeśli stosuje to samo ustawienie, celem prawidłowego typu będą rozszeregować. Dokładnie to, czego potrzebowałem! Mam nadzieję że to pomoże.

Powiązane problemy