Versenden von Objekten über TCP

  • C#

Es gibt 5 Antworten in diesem Thema. Der letzte Beitrag () ist von ~blaze~.

    Versenden von Objekten über TCP

    Hallo, ich versuche gerade Objekte über TCP zu senden, dazu hab ich mir eine Klasse BinaryPackage geschrieben, diese enthält einen ByteArray PackageData und einen Identifer vom Typ Int. Ein BinaryPackage ist generisch, das heißt das ganze kann so aussehen:

    C#-Quellcode

    1. var binPackage = new BinaryPackage<Int>();


    BinaryPackage besitzt außerdem die Methoden Serialize und Deserialize, mit Serialize wird ein Objekt vom Typ Int Serialisiert und in die PackageData gepackt, auch der Identifer wird gesetzt. Als nächstes Serialisier ich das BinaryPackage selber, das funktioniert auch alles, auch die Deserialisierung klappt wunderbar:

    C#-Quellcode

    1. var binPackage = new BinaryPackage<Vector2>();
    2. Console.WriteLine("Binary Package created.");
    3. binPackage.Serialize(new Vector2(20, 20));
    4. Console.WriteLine("Serialized Vector into Binary Package.");
    5. Console.WriteLine("Header: " + binPackage.Identifer);
    6. Console.WriteLine("Length: " + binPackage.PackageData.Length);
    7. var stream = PackageSerializer<Vector2>.Serialize(binPackage);
    8. Console.WriteLine("Serialized Package into Stream.");
    9. var package = PackageSerializer<Vector2>.Deserialize(stream);
    10. Console.WriteLine("Deserialized the Package from Stream.");
    11. var value = package.Deserialize();
    12. Console.WriteLine("Deserialized PackageData.");
    13. Console.WriteLine("Result: " + value.GetType().Name);
    14. Console.WriteLine("Vector<" + value.X + ", " + value.Y + ">");


    Allerdings funktioniert das nur, wenn ich den Type von dem Paket habe (Im Beispiel Vector2) wenn ich den nicht weiß, ist es nur ein nutzloser Stream. Nun zu meiner Frage, woher weiß das andere ende, welcher PaketTyp (BinaryPackage<Type>) vorliegt?
    Wieso baust du das ganze nicht so um, dass jedes Paket eine Type-Kennung hat, die genau auf einen Typ zurückführbar ist. Diese steht am Anfang des Pakets, wenn es serialisiert wurde, oder eben deserialisiert werden soll. Dann kannst du einfach die ID auslesen, den Type ermitteln, und den dann an die Methode zur Deserialiserung weiterreichen.
    //EDIT: Ich weiß zwar nicht, was dein "Identifer"-Feld bedeutet, aber soll das nicht schon dafür sein?
    Das ist eine Sache, die mich auch sehr interessieren würde (ich will in GameUtils ja auch NetzwerkKommunikation einbauen).
    Bisher ist mir nur eingefallen, allen Typen eine Zahl zuzuordnen. Der Benutzer des Systems müsste also alle Typen, die er versenden will, einer Liste hinzufügen und das System generiert dann für jeden Typen eine ID.
    @FlashTek:

    Das Problem wäre dann, dass das ganze nicht mehr so dynamisch wär.. Falls ich dich falsch verstanden habe, könntest du mir ja mal ein Beispiel geben?

    Ich habe es jetzt ein bisschen anders gemacht, ich deserialisiere das Paket jetzt als BinaryPackage<Object> nun kann ich mittels Typüberprüfung herrausfinden um welches Paket es sich handelt:

    C#-Quellcode

    1. var binPackage = new BinaryPackage<Vector2>();
    2. Console.WriteLine("Binary Package created.");
    3. binPackage.Serialize(new Vector2(20, 20));
    4. Console.WriteLine("Serialized Vector into Binary Package.");
    5. Console.WriteLine("Header: " + binPackage.Identifer);
    6. Console.WriteLine("Length: " + binPackage.PackageData.Length);
    7. var stream = PackageSerializer<Vector2>.Serialize(binPackage);
    8. Console.WriteLine("Serialized Package into Stream.");
    9. Console.WriteLine("Unknown Package, trying to get Type ...");
    10. var uPackage = PackageSerializer<object>.Deserialize(stream);
    11. if (uPackage.GetType() == typeof (BinaryPackage<Vector2>))
    12. {
    13. var package = (BinaryPackage<Vector2>) uPackage;
    14. Console.WriteLine("Type found: " + package.GetType().Name);
    15. Console.WriteLine("Deserialized the Package from Stream.");
    16. var value = package.Deserialize();
    17. Console.WriteLine("Deserialized PackageData.");
    18. Console.WriteLine("Result: " + value.GetType().Name);
    19. Console.WriteLine("Vector<" + value.X + ", " + value.Y + ">");
    20. Console.WriteLine("Serializeception dude!");
    21. }
    22. else
    23. {
    24. Console.WriteLine("No matching type found.");
    25. }
    26. Console.ReadLine();


    Der Benutzer muss dann also selbst eine determinaton ausführen, was völlig in Ordnung ist.

    Falls jemand eine bessere Lösung findet, kann er ja mal hier Posten :)


    EDIT:
    //EDIT: Ich weiß zwar nicht, was dein "Identifer"-Feld bedeutet, aber soll das nicht schon dafür sein?


    Ja, aber das funktioniert nicht wie gedacht, der User muss ja dann trotzdem eine Typenüberprüfung machen, der kann er auch gleich das Objekt vergleichen ..
    Hi
    wie wär's mit folgender Struktur:
    das Übertragungssystem ist Nachrichten- und Stream-basiert. Das heißt, jeder Client schickt über einen Stream an einen Server Nachrichten und umgekehrt, Empfangen wird durch Stream.Read, gesendet durch Stream.Write. Hierbei werden Nachrichten als Message-Klasse modelliert. Jede Nachricht besitzt einen zugehörigen Typen MessageKindProvider, der Nachrichtendaten aus Nachrichten und Nachrichten aus Nachrichtendaten generieren kann. Nachrichtendaten können halt in ein Byte-Array gepuffert und aus Byte-Array-Daten erzeugt werden (gleich, wie bei System.IO.Stream die Methoden Read und Write) Jeder Nachrichtentyp wird durch eine GUID in deinem System eindeutig identifiziert (kannst du ggf. bei der Initialisierung des Client-Server-Systems zwischen beiden abgleichen, sodass beide die gleichen GUIDs unterstützen. Sonst ggf. einfach nur eine virtuelle Methode OnUnsupportedPackage oder dergleichen aufrufen und hierbei ein Rohdaten-MessageData erzeugen). Die GUID Guid.Empty ist für Systemnachrichten reserviert, die für den Austausch von kernrelevanten Informationen verwendet werden. Folgende Systemnachrichten gibt es:
    - Null - leere Nachricht, nur zur Verbindungsüberprüfung
    - ConnectionRequest - Versuch eines Clients, sich am Server anzumelden
    - ConnectionResponse - Nachricht an einen Client, der sich am Server anzumelden versuchte (Allow/Deny)
    - OpenMessage - eine Nachricht wird geöffnet (einseitige Übertragung Client->Server oder Server->Client, siehe unten)
    - OpenChannel - eine Nachricht mit zugehöriger Response schicken (beidseitige Übertragung, wird als Streaming modelliert, optional mit synchronisiertem Senden und Empfangen, sodass immer [Client->]Server->Client->Server... läuft)
    - ChannelAccept - eine OpenChannel-Nachricht wurde akzeptiert, beidseitiges Streaming wird ermöglicht (stellt die Nachrichten-ID vom Empfänger - siehe unten - bereit, OpenChannel lässt die übertragene ID als Ziel offen)
    - ResponseMessage - eine Nachricht an einen Client oder Server infolge einer Request-Anfrage, wie OpenMessage oder OpenChannel (Allow/Deny/Abort/Retry...)

    Jede zu übertragene Nachricht wird durch eine eindeutige ID identifiziert, die du pro Nachricht inkrementierst. Die ID ist vom Typ Long, sodass du genug Spielraum hast. Jede Nachricht hat, wie gesagt, eine GUID, die die Nachrichtenart eindeutig festlegt (die Typen müssen nicht identisch sein, nur die Nachricht muss später konstruiert werden können). Um eine Nachricht zu rekonstruieren verwendest du den MessageKindProvider, den du über ein MessageKindProviderAttribute-Attribut bereitstellst (einfach Activator.CreateInstance und Cast zu MessageKindProvider, die Attribute ermittelst du per Attribute.GetCustomAttributes oder Type.GetCustomAttributes), hier wird auch zusätzlich die GUID der Nachricht festgelegt. Das Attribut wird dann direkt auf einem von der Message-Klasse erbenden Typ festgelegt.
    Zusätzlich schreibst du eine weitere Klasse MessageProcessing, die für das Weiterverarbeiten von empfangenen Nachrichten zuständig ist. Nachrichten sollen auf Basis ihrer GUID und ihres Typs bestimmten MessageHandler-Delegaten zugeordnet werden, die eben aufgerufen werden, sobald Messages empfangen werden. Hierzu hast du einfach ein Dictionary<Guid, MessageHandler>, wobei MessageHandler ein Multicast-Delegate ist (oder wie du halt die Methoden registrieren willst, die eingehende Nachrichten ähnlich wie Events behandeln. Hier wäre übrigens auch eine Art verkettete Liste interessant, siehe unten), der über die GUID der Message die Verarbeitung aufnimmt. Das Dictionary kapselst du also in der Klasse MessageProcessing und du fügst über void RegisterHandler(Type messageType, MessageHandler handler) Handler an einen Nachrichtentyp dran. Der Typ stellt ja auch die GUID bereit. Zusätzlich hast du ein zweites Dictionary(Of Type, MessageKindProvider), der die GUID dem zugehörigen MessageKindProvider zuordnet. Dort registrierst du den Provider, sofern nicht vorhanden (Try-Catch mit Catchen der ArgumentException, die eintritt, wenn das Element bereits vorhanden war).
    Jetzt schreibst du noch die Kernklasse, die die Nachrichten eben schreibt und liest. Nachrichten sollte man quasi parallel verschicken können, also auch große Nachrichten sollen kleine nicht blockieren. Somit segmentierst du die jeweiligen Nachrichten in Blöcke mit einer maximalen Größe von z.B. 4096 Bytes und schickst die Daten verzahnt raus. Neben der tatsächlichen Größe eines Segments schreibst du die ID der dem Segment zugehörigen Nachricht mit raus (die ID ist für Systemblöcke automatisch < 0 oder = 0, sodass du sie eindeutig unterscheiden kannst) und ein Flag EndSegment, ob es das letzte Segment ist, das für diese Nachricht übertragen wird. Darauf wird später nochmal etwas ergänzt.
    Systemnachrichten werden wie gesagt gesondert behandelt. Sie bestehen immer aus einem einzigen Segment (==> das EndSegment-Flag ist immer 1, die Segmentlänge könnte theoretisch die maximale Segmentlänge überschreiten) und die ID 0. So kannst du OpenMessage dazu benutzen, neue Nachrichten am Ziel zu registrieren, da die ID ja noch nicht geöffnet wurde. Öffnen könntest du theoretisch auch beim ersten Empfangen einer Nachricht mit der spezifizierten ID, aber dann könnten fehlerhafte Übertragungen, wie z.B. durch abgeschlossene Packete passieren. Daher ist es auch wichtig, dass die IDs auf beiden Seiten fortlaufend sind. OpenMessage überträgt die ID der Message und die GUID, die dem Nachrichten-Typen zugeordnet wurde. Nachrichten ohne zugehörige ID werden btw. mit einer ResponseMessage beantwortet, die Deny mit der ID in den Nachrichtendaten zurückgeben. Die Daten für eine Nachricht erhältst du nat. aus der MessageData, die du über den MessageKindProvider abfragen kannst (der wiederum über das Attribut der MessageKind erreichbar ist).
    Eingehende Nachrichten werden eben aus diesen empfangenen Segmenten wieder zusammengesetzt. Über die ID wird die Nachricht ermittelt, der der Datenblock angehört, wenn es ein EndSegment ist, wird eine Nachricht mit bekannter Länge konstruiert. Streaming-Nachrichten, also Nachrichten mit unbekannter Länge (z.B. Videos) besitzen für die OpenMessage-Systemnachricht die Länge -1 und beim Empfangen bereits am Anfang konstruiert.
    Die SegmentHeader besitzen also folgendes Layout (habs mal mit 24-Bit-Länge und 8-Bit-Flags modelliert):

    C#-Quellcode

    1. [StructLayout(LayoutKind.Sequential)]
    2. public struct SegmentHeader
    3. {
    4. public ulong ID { get; private set;} //hier sind Systemnachrichten mit einer ID = 0 versehen
    5. public int SegmentInfo { get; private set;}
    6. public SegmentFlags Flags //EndSegment wird im Vorzeichen-Bit gespeichert
    7. {
    8. get { return (SegmentFlags)(unchecked((uint)SegmentInfo) >> 24);}
    9. }
    10. public int SegmentLength
    11. {
    12. get { return SegmentInfo & 0xffffff;}
    13. }
    14. public SegmentHeader(ulong id, int segmentInfo)
    15. : this()
    16. {
    17. ID = id;
    18. SegmentInfo = segmentInfo;
    19. }
    20. public SegmentHeader(ulong id, int segmentLength, SegmentFlags flags)
    21. {
    22. if (segmentLength < 0)
    23. throw new ArgumentException("Negative segmentLength detected.");
    24. if (segmentLength > 0xffffff)
    25. throw new ArgumentException("Segment length greater than " & 0xffffff & ".");
    26. segmentLenth |= (int)flags << 24;
    27. ID = id;
    28. SegmentInfo = segmentLength;
    29. }
    30. }
    31. [Flags()]
    32. public Enum SegmentFlags : byte
    33. {
    34. None = 0,
    35. EndSegment = 1 //erweiterbar auf 8 Flags
    36. }


    bzgl. dem Segment-Header kann ich dir btw. empfehlen, die maximale Segmentlänge nicht auf zu hoch festzulegen, ein schöner Wert wäre ggf. sogar 2^16-1 und die restlichen 16 Bit für Flags (werden sowieso alligned). Die Daten kannst du elegant über unsafe und einen SegmentHeader-Zeiger ermitteln.

    MessageData und MessageDataProvider könnten dann so aussehen:

    C#-Quellcode

    1. public enum AccessMode
    2. {
    3. Read = 1,
    4. Write = 2
    5. }
    6. public abstract class MessageData
    7. {
    8. public abstract void Write(byte[] buffer, int index, int count);
    9. public abstract int Read(byte[] buffer, int index, int count);
    10. public abstract long MessageLength { get; }
    11. public abstract Message GetMessage();
    12. public abstract AccessMode AccessModes { get; }
    13. public bool IsStreamed
    14. {
    15. get { return MessageLength == -1; }
    16. }
    17. }
    18. publi abstract class MessageDataProvider
    19. {
    20. public abstract MessageData GetData(Message message);// ==> AccessModes == AccessMode.Read
    21. public abstract MessageData Create(long length); // ==> AccessModes == AccessMode.Write
    22. }


    So kannst du dynamisch und kompakt kommunizieren, was insbesondere beim Spiel wichtig sein sollte. Für Nachrichten kann man nat. auch nach MemoryStream binär serialisierte Objekte verwenden, die sind aber halt oftmals auch etwas größer, als nötig. Außerdem kannst du z.B. innerhalb von Messages ein weiteres Messagesystem implementieren, etc.

    btw. lässt sich das System auch gut für Server verwenden, wenn man asynchrone Verarbeitung über eine Clientliste pro Thread implementiert, die die Clients gleichmäßig verteilt (z.B. über eine Art Verkettete Liste, die nach dem letzten Element wieder zum ersten zurückkehrt).

    Schreib' dir außerdem noch Klassen, die Rohdaten und Objekte in serialisierter Form verschicken können, sowie stream kapseln. so kannst du einfach von denen Erben und das Attribut festlegen etc. Die Rohdaten werden über ein Func<byte[]> bereitgestellt, das bei einem konstanten byte-Array einfach () => byteArray zurückgibt, wenn byteArray der Konstruktorparameter vom Typ byte[] ist:

    C#-Quellcode

    1. [MessageKindProvider("Irgendeine Guid (grad zu faul eine zu generieren)", GetType(RawMessageKindProvider))]
    2. public class RawMessage
    3. {
    4. private byte[] _raw;
    5. private Func<byte[]> _rawGenerator;
    6. public RawMessage(byte[] raw)
    7. {
    8. if (rawGenerator == null)
    9. throw New ArgumentNullException("raw");
    10. _raw = raw;
    11. }
    12. public Rawmessage(Func<byte[]> rawGenerator)
    13. {
    14. if (rawGenerator == null)
    15. throw New ArgumentNullException("rawGenerator");
    16. _rawGenerator = rawGenerator;
    17. }
    18. public byte[] Raw
    19. {
    20. get { return _raw ?? (_raw = _rawGenerator()); }
    21. }
    22. }


    Appendix I: :P Liste mit Aufrufer-Element-Zugehörigkeit
    Bei der oben vorgeschlagenen verketteten Liste handelt es sich um eine Alternative zur LinkedList(Of T). Elemente können eingefügt werden, aber nicht direkt auf der Liste entnommen werden, sondern nur derjenige, der sie erzeugt, kennt das "Handle" des zugehörigen Elements. Das Handle ist einfach eine Klasse, die IDisposable implementiert und bei einem IDisposable.Dispose-Aufruf wird das Element aus der verketteten Liste entfernt. Dazu definierst du eine private RemoveItem-Methode, implementierst alle schreibenden Methoden der ICollection(Of T)-Schnittstelle explizit (Count, Add, etc. sind public, Remove, Clear sind private). Anschließend erzeugst du eine Klasse SpecificLinkedListItem(Of T), die jeweils auf das das vorhergehende und nächste Item der Liste verweist und außerdem auf die Liste selbst und einen Wert T enthält (wie LinkedListItem(Of T)). Die SpecificLinkedList(Of T) selbst enthält eben First und Last, der Enumerator geht einfach alle Items durch. RemoveItem ist btw. losgelöst von ICollection(Of T).Remove, setzt aber Previous.Next = item.Next, Next.Previous = item.Previous, item.Previous = item.Next = null, item.List = null, wenn item das zu entfernende Item ist (aufpassen, item.Previous und item.Next können null sein, entsprechend auch list.First und list.Last anpassen). Auf der Liste definierst du AddAfter und AddBefore, die ein Referenz-Item annehmen (kann null sein, AddBefore(null, value, out handle) fügt das Item ganz am Ende der Liste ein, AddAfter(null, value, out handle) ganz am Anfang) und updaten entsprechend auch First, Last, reference.Next.Previous bzw. reference.Previous.Next und reference.Previous bzw. reference.Next, sowie item.Next und item.Previous. AddBefore und AddAfter funktionieren nur mit T-Werten und erzeugen jeweils zugehörige SpecificLinkedListItem(Of T)-Instanzen, d.h. der Konstruktor der SpecificLinkedListItem(Of T)-Klasse ist internal, die Klasse sealed. Außerdem kannst du noch AddLast und AddFirst definieren, die eben AddBefore(null, value, out handle) und AddLast(null, value, out handle) aufrufen. Im letzten Parameter wird das IDisposable-Handle (als IDisposable) zurückgegeben, dessen Typ als private, sealed und in SpecificLinkedList(of T) nested Type zu modellieren ist.
    Auf der Handler-Liste gibst du dann eben das Handle zurück, sodass die Signatur zum Handler registrieren so aussieht:

    C#-Quellcode

    1. public IDisposable RegisterHandler(Type messageType, MessageHandler handler)

    das Dictionary ist folglich auch ein Dictionary(Of Guid, SpecificLinkedList(Of MessageHandler)). So kann man beliebig viele Handler definieren und die Remove-Operationen sind in O(1). Fände die Klasse insg. im FW sehr praktisch.

    Appendix II: Kommunikation über Interface-Delegation
    Ein schöner Weg, über solche Systeme zu kommunizieren, wäre übrigens auch diese Library: InterfaceDelegation
    Pro Aufruf werden dann eben alle Kontextinformationen, wie Generika oder Parameterwerte, in einer Nachricht abgelegt und übertragen. Die Daten der Nachricht werden über einen BinaryFormatter serialisiert und deserialisiert. Am Empfänger werden dann wiederum Services über einen IServiceProvider bereitgestellt, die die Kommunikation übernehmen. So können Methodenaufrufe über Tcp übertragen werden, wie es dort vorgeschlagen wurde.
    Als kleine Anregung (eher weniger zum Projekt beitragend vmtl.): Man könnte vom Server aus auch alle Clients an einem Auftrag rechnen lassen. Außerdem könnte man das System so organisieren, dass über Udp und Holepunching mit Einwilligung der Benutzer ein Kommunikationskanal zwischen den Clients geöffnet wird, wenn Bedarf besteht. Der Server dient hierbei lediglich als Management-Einheit und steuert und überprüft die einzelnen Clients (Pingen, etc.). Somit könnte man's zu einer schön bedienbaren Cloud ausweiten. Gerade schön an dem System ist, dass die Interfaces eine abstrakte Schicht darstellen, bei der es nicht darauf ankommt, wie die Implementierung ausfällt. Gerade das macht sich meine Library zunutze.

    Gruß
    ~blaze~

    Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „~blaze~“ ()