Socket Server für Mini-RPG

  • C#

Es gibt 17 Antworten in diesem Thema. Der letzte Beitrag () ist von seh.

    Socket Server für Mini-RPG

    Hallo ihr Lieben, :)

    Ich habe vor kurzem angefangen mich mit einem Spiel zu beschäftigen welches über den Browser läuft, ein Entwickler hat bereits einen Emulator in Java veröffentlicht inkl. Source.
    Dadurch konnte ich etwas vom Protokoll und den allgemeinen Aufbau in Erfahrung bringen und würde gerne anfangen meinen eigenen in C# zu schreiben.

    Nun wollte ich einen Socket Server schreiben, ein Freund meinte vor einigen Tagen ich solle am besten die SocketAsyncEventArgs Klasse nutzen für höhere Performance, schließlich möchte ich diesen auch im späteren versuchen auszulasten, mein Wunsch wären 30K Verbindungen problemlos zu tragen. Ich habe mich damit nie beschäftigt und weiß deshalb auch nicht ob das der bestmögliche Weg ist.

    Was haltet ihr davon und könntet ihr mir etwas empfehlen?
    Schau dir github.com/Eastrall/Ether.Network an. Diese Lib nutzt SocketAsyncEventArgs. Ist aber im Moment unstable. Aber dadurch kannst du dir das Prinzip abschauen. Ist wirklich nicht sonderlich schwer. SocketAsyncEventArgs kann ich auch nur empfehlen.

    Der Author der Bibliothek nutzt diese selbst zur Emulation vom Server eines MMORPG namens FlyFF.
    Das Prinzip der Bibliothek ist super und sehr flexibel. Du kannst deinen eigenen PacketProcessor nutzen und eigene Packet Strukturen basteln so wie du das willst.
    Das Standard Packet was mit Ether.Network geliefiert wird (NetPacket) hat einen 4 Byte Header welcher die Größe des Pakets angibt. Der Rest sind dann Daten. Und so auch der DefaultPacketProcessor. Mit GetLength() liest du den Header aus dessen Größe du im Packet Processor überschreibst und somit festlegst, GetLength liefert dann die Größe des Payloads (Gesamtlänge des Pakets). Dann bekommst du nachher ein INetPacketStream wenn du ein Paket empfängst und kannst das Paket auslesen.
    Wirlich simpel zu benutzen!

    Zu den Lasten kann ich nichts sagen, außer das mir 30K ein wenig übertrieben vorkommt. Ich glaube nicht das du so viele Verbindungen händeln kannst, aber ich lasse mich gerne überzeugen. Habe selbst auch noch nie einen solchen Lastentest gemacht.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „seh“ ()

    Vielen Dank für die Antwort.
    Die Paketstruktur ist ungefähr so: 4BYTES [INT, MSG LEN], 2BYTES [SHORT, MSG ID], 2BYTES [SHORT, STRLEN], STR
    Und das oben genannte Repo ist sehr Interessant, werde ich mir anschauen. Also, der Emulator in Java schafft nachweislich auch 50K . (Simuliert - dennoch mit echten TCP Verbindungen!)
    Dann müsste das doch auch in C# machbar sein! :D
    Was genau sagen mir 30.000 Verbindungen aus? oder 50.000? Die Anzahl der Verbindungen ist (zumindest für mich) kein Leistungsmerkmal.
    Speicherverbrauch, Bandbreite, und Bearbeitungszeit währen da schon eher was. Ich würde darauf Wetten, das ich es sogar mit WCF-HTTP es hinbekomme, 30.000 Verbindungen gleichzeitig herzustellen. Jedoch untergräbt mir die SOAP Serialisiserung jegliche Performance.

    Und dann ist noch die Frage der Hardware. Desto größer die Maschine, desto mehr Verbindungen können gleichzeitig, oder die gleiche Anzahl schneller bedient werden.
    Danke für die ausführliche Antwort, ich kann dir nur das wiedergeben was ich von den englischsprachigen Entwicklern aus dem anderen Board erfahre.
    Und wirklich jeder von denen beharrt drauf dass genau der wichtigste Part der Socket Server seie.

    Ich habe dir mal ein Repo via PN geschickt weil ich nicht weiß ob der Inhalt hier nun okay wäre, wäre er aber wahrscheinlich?
    Ich glaube diese Themen sind hier generell unerwünscht weil nicht genau bekannt ist, wie die rechtliche Lage bzgl. Legalität des Ganzen aussieht.
    An sich hast du deine Frage aber von mir beantwortet bekommen, du wolltest Empfehlungen und wissen ob das der bestmögliche Weg ist. Meine Empfehlung hast du und ich sehe jetzt kein Problem was gegen SocketAsyncEventArgs spricht. Auf jeden Fall solltest du, falls vorhanden, aber auch von anderen mehr erfahrenen Benutzern in diesem Board eine Meinung anhören.

    Grüße seh
    der Performancevorteil von SocketEventArgs vs BeginRead/BeginWrite etc. dürfte relativ gering sein, am Ende verwenden sie diesselbe WinAPI mit Overlapped, was zur Verwendung von IOCP in beiden Fällen führen sollte. Jedoch allokiert das Begin zeug ständig etwas, wobei man bei den SocketEventArgs allokationen komplett verhindern kann. Dies ist vorallem wichtig, wenn es eine GameLoop gibt, auch bei Physik Simulationen. Natürlcih beeinlusst der GC auch normale Programme, aber dort ist es oftmals nicht so tragisch. Besser ist es ohne natürlich trotzdem. Jetzt kommt aber der eigentliche clue, das gepostete git Repository allokiert ständig für irgendwelche anderen Dinge, wenn du das so machst, hast du natürlich erst keinen Vorteil und der GC wird dir trotzdem ständig dazwischen funken.
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---
    @jvbsl Da hast du Recht, was mir an der Repository nicht gefällt ist z.B., das man die MaxConnections angibt und im Voraus für die Anzahl an Max Connections bereits SocketAsyncEventArgs zum Senden und Empfangen allokiert. In meinen Augen unnötig und kann anders gelöst werden. Garnichts zu allokieren, wie soll das denn funktionieren, da wäre ich hellhörig, bitte erläutern :)
    Ich meinte damit eher Dinge zu allokieren, aber nie wegzuwerfen. Also wiederzuverwenden aka. MemoryPooling. Die EventArgs pro Client zu erzeugen ist grundsätzlich Sinnvoll, könnte man aber über einen dynamischen Pool machen, sodass man immer welche zur Verfügung hat, aber auch vorallokiert wird um sich die Zeit während der Ausführung zu sparen. Jedoch dürfte allokationen bei .Net ziemlich schnell sein, weshalb ich mir nicht sicher bin, ob das überhaupt einen Performancevorteil bringt(bei nativen allokationen bringt es was). Aber auf jeden Fall bräuchte man da keine Max-Connections.
    Was ich jedoch angeguckt hatte war sein dynamisches Read für arrays z.B., welches einfach hin geht und immer ein neues Array erzeugt, sowas sollte eindeutig gepoolt werden, natürlich ist es dann so, dass man nicht einfach ein Array in korrekter Länge zurückgeben kann, aber das ist nicht so tragisch. Muss man halt zusätzlich die länge mit rausgeben.
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---
    @jvbsl
    Das heißt, man hat für jede Verbindung auch ein allokiertes Array fester Größe z.B. BufferSize welche man im Server einstellen kann und dann wird bei neuen Daten das Array nicht neu angelegt (allokiert) sondern die Daten darin werden überschrieben?
    Ergäbe für mich eigentlich Sinn. Dann müsste man sich darum kümmern, wenn längere Pakete erwartet werden als BufferSize, man das noch irgendwie wieder zusammenbastelt, aber das sollte ja kein Problem sein.
    Naja hab bissl reingeguckt und die scheinen überall Tasks zu verwenden, was zwar das für viele einfachste zu verwenden sein dürfte, aber auch Performancetechnisch am meisten reinhaut.

    Für das Task zeug wird einerseits für die Tasks selbst allokiert, andererseits verwendet es intern die Begin/End Methoden, welches ein IAsyncResult allokiert, welches aber nicht gepoolt wird. Also somit was GC anbelangt schlechter als Begin/End zeugs. Und da es dieses verwendet kann es auch unmöglich performanter sein, da es natürlich noch zusätzlichen overhead hat. Also rein aus dieser Sicht ist Supersocket schon mal nicht die beste Wahl, wenn man wirklich alles rausholen will.
    Aber ansonsten gilt wie bei allem, brauchst du auch wirklich das richtig highperformante Zeug? Wie gesagt hast du eine aufwändige Update/GameLoop, die auf dem Server laufen muss, wo man möglichst konstante Zeit haben möchte, dann nimm Sockets.[...]Async(also ohne Tasks). Jedoch bringt das auch nichts, wenn du den Coder drumherum nicht mit diesem Hintergedanken aufbaust. Ansonsten nimm was auch immer du willst, denn die kleinen Unterschiede dürften kaum relevant sein. Zumindest nicht für die Anzahl der Verbindungen die abgearbeitet werden können o.ä.

    Edit:
    @seh: Zusammenschustern kann man dann beim interpretieren der Daten, hin und her kopieren würde ich also nur im aller letzten Schritt, wenn es darum geht, das eigentliche Datenobjekt zu erzeugen, nicht aber Buffer.
    Für ArrayPools gibt es folgendes:
    nuget.org/packages/System.Buffers/
    D.h. man schnappt sich das zeug aus dem Pool, heißt aber für jeden client zwei buffer, mit einer Buffergröße von 8kb belegt man für 30000 clients 480 MB, das ist gar nichts^^

    Edit2: Natürlich poolt man die Datenobjekte selbst auch, oder guckt jenachdem, dass auch Stackobjekte sinnvoll möglich sind^^
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „jvbsl“ ()

    Ich kann dir aber direkt sagen, dass Performancetechnisch das NuGet package besser ist^^ Das NuGet Package ist von MS selbst, nur so zur Info, wurde halt für .Net Core geschrieben, deshalb als NuGet package. Und Libraries zu verwenden ist nicht wirklich schlimm^^

    Und das Problem mit MaxConnections besteht hier weiterhin, was ich doch etwas unnötig finde^^

    Edit: aber am Ende bezweifle ich, dass das für deinen Anwendungszweck so viel ausmacht...
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---
    Von welchem NuGet ist gerade die Rede? SuperSocket oder System.Buffers?
    Hier ist mal ein Beispiel eines alten Emulators: github.com/Sledmore/PlusEMU, der relevante Teil ist unter Communication/ConnectionManager zu finden.

    Möchte halt nichts c&p'n und selber die bestmögliche Methode anwenden.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „SysOp1“ ()

    jvbsl schrieb:

    D.h. man schnappt sich das zeug aus dem Pool

    Könntest du das genauer erläutern? Ich habe mir das NuGet Package jetzt mal gezogen. Bedienung vom ArrayPool ist ja nicht sonderlich schwer mit Rent und Return.

    Aber wie genau verwende ich das ganze denn jetzt am performantesten. Soll ich für ein SocketAsyncEventArgs für das receiving den Buffer vom ArrayPool setzen mit SetBuffer()?
    Und welche Größe der Buffer hat bleibt mir dann überlassen? Oder gibt es da auch gewissen Vorgaben die man beachten sollte?
    Was wenn ein Paket nicht in den Buffer mit der von mir angegeben Größe reinpasst, dann einen zusätzlich Buffer Renten und am Ende zusammenfrickeln?
    jop einfach SetBuffer verwenden und auf ein gepooltes Array.
    Für das Empfangen würde ich mir etwas Richtung MemoryStream+BinaryReader selbst basteln und dann eben so, dass du zwei Buffer hast, in die alternierend die empfangenen Daten geschrieben werden(SetBuffer+ReceiveAsync), sowie aus welchen ausgelesen werden.

    Nun hat dein "Stream"(ich würde btw. nicht von Stream erben lassen, sondern einfach eigene klasse) die zwei Buffer und die ganzen ReadInt32/ReadString etc.. und liest aus dem aktuell selektierten Buffer, wenn bei diesem zu Ende gelesen wurde, wechselt man zu Buffer Nr 2(vorausgesetzt in diesen wurde bereits Daten empfangen - mit ResetEvents z.B.) und kann dort direkt weiterlesen. Wichtig ist dabei, dass man nie versucht in einen Buffer zu empfangen, an welchem noch gelesen wird. Das ganze kann natürlich dazu führen, dass abhängig von der Datenmenge ständig gewartet wird, was das ganze natürlich langsamer maht, dies kann man mit mehr Buffern lösen aber im Grunde nach dem gleichen Prinzip, dann hat man quasi einen RingBuffer. Auch die Buffergröße selbst zu erhöhen bringt dann etwas, kann aber iimmer wieder zu einem Schluckauf führen, wenn man an die Grenzen kommen sollte. Und die Buffergröße sollte außerdem am besten so groß sein, wie dein größter atomarer Typ, aber im Regelfall sollte das long sein, also kein Stress :D

    Also durch den Ringpuffer erreicht man eben, dass man unnötiges hin und her kopieren Komplett verhindern kann. Das einzige Kopieren ist quasi beim einlesen in den Komplexen Datentyp und dort ist es auch vollkommen in Ordnung(wenn die Daten bereits direkt synchron verarbeitet werden können und danach nicht mehr benötigt werden könnte man sogar das noch verhindern, aber das wird dann doch etwas zu viel :D), aber nicht vergessen die komplexen Datentypen selbst auch zu poolen. Komplexe Datentypen können sich dann selbst aus diesem "Stream" lesen, auch könnte man sich dafür Serializierer schreiben, die kosten aber jenachdem wie gut man die programmiert auch wieder etwas overhead.

    Edit: achja Buffer größe bleibt dir überlassen. Der Pool gibt dir aber immer einen Buffer >= deiner angegebenen größe. D.h. gut möglich dass du einen größeren Buffer bekommst. Sind immer in größen von 2^x.
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „jvbsl“ ()