Kommunikationsprotokoll auf Basis von TCP entwerfen

Hellblazer

Lt. Commander
Registriert
Aug. 2011
Beiträge
1.604
Hallo,

ich bin gerade dabei in C# einen TCP-Server zu programmieren. Der Server basiert auf SocketAsyncEventArgs.
Der Code für den Server ist im Grunde schon vorhanden (senden, empfangen und Unterscheidung der Clients funktioniert). Ich zerbreche mir gerade nur den Kopf über ein Kommunikationsprotokoll.

Das Grundgerüst des Servers soll variabel einsetzbar sein. Ob darüber eine Chat-Anwendung, ein Spiel oder Datenübertragung laufen soll, soll frei wählbar sein.
Deshalb dachte ich mir, dass ich verschiedene Schichten einführe. Die unterste Schicht ist das Servergrundgerüst. Darauf lässt sich eine beliebige weitere Schicht, die eine genauere Kommunikationsprotokolldefinition enthält, aufsetzen.
Das heißt also, dass das Grundgerüst von einer höheren Schicht Daten/Pakete erhält und diese dann entsprechend versendet.
Das Grundgerüst weiß dabei nichts von irgendeinem Kommunikationsprotokoll, sondern sendet/epfängt die Daten einfach nur.
Das einzige das die unterste Schicht mit den Daten macht, ist ein Längenprefix voranzustellen, das die Länge der Daten enthält, damit der Client weiß, wann alle Daten eines Pakets erhalten wurden. Der Prefix ist einfach ein Integer der die Paketgröße enthält in Bytes umgewandelt. Das sieht dann in etwa so aus:

Längenprefix|beliebige weitere Daten der höher liegenden Schicht...

Jetzt bin ich gerade am überlegen, wie ich am geschicktesten ein Kommunikationsprotokoll für die höhere Schicht implementiere. Mein Ziel ist es, verschiedene Kommandos zu haben, die dann auf dem Server/Client die entsprechend Vorgänge einleiten. Mir schwirren da gerade verschiedene Ansätze im Kopf herum:

1. Textbasierte Kommandos mit mehreren Kommandos pro Paket
Ein Paket enthält eine Kombination aus Befehlen, die zusammen eine abgeschlossene Handlung darstellen.
Nachteil: Kommandos als String verbrauchen relativ viel Speicher
Zum Beispiel sendet ein Nutzer in einem Chat eine Nachricht an einen anderen Nutzer
Aufbau: Kommando1:Nutzdaten zum Kommando1; Kommando2:Nutzdaten zu Kommando2...
Beispiel: cmdSAY:Hello;cmdToUser:TestUser

2. Textbasierte Kommandos mit nur einem Kommando pro Paket:
Wie 1., nur dass jedes Paket genau ein Kommando enthält. Führt möglicherweise zu vielen Sende-Aufrufen im Client und die Pakete müssen im Server wieder zusammengesetzt werden, man muss sich aber nicht um die Länge der Nutzdaten eines jeden Kommandos kümmern
Aufbau: Paket1: Kommando1:Nutzdaten zum Kommando1 Paket2:Kommando2:Nutzdaten zu Kommando2...
Beispiel: Paket1: cmdSAY:Hello; Paket2: cmdToUser:TestUser

3. Byte basierte Kommandos

Es wird in der Nachricht ein Byte reserviert, dessen Bits die Kommandos repräsentieren. Vorteil: Es lassen sich mit einem Byte relativ viele Kommandos (256) darstellen.
Aufbau: ByteKommando:Nutzdaten;

Methode 3. könnte dann auch wie Methode 2. ausgeführt werden (pro Paket nur ein Kommando). Da weiß ich nicht was besser ist. Ein Kommando pro Paket endet in vielen Sendeaufrufen. Dafür sind die Pakete klein, müssen aber beim Empfänger wieder zusammengesetzt werden bzw. nacheinander durchlaufen werden zum abarbeiten. Je nachdem wie schnell die Sendevorgänge laufen kommen die einzelnen Pakete sowieso im selben Buffer am Server an und dann müsste extra der Buffer in die einzelnen Pakete gesplittet werden.


Grundsätzlich sollte eine Paket etwa so aussehen:

Länge|Version|Typ|beliebige Kommandos u. Nutzdaten

Die Länge wird von der untersten Schicht hinzugefügt. Alles danach kommt aus einer höheren Schicht.

Meine Ziele sind:
- möglichst schnell beim Parsen der Pakete
- möglichst schnell beim erstellen der Pakete
- Erweiterbarkeit
- Stabilität
- Speicher schonend


Da die unterste Schicht sowieso nur bytes versendet würde ich zu Methode 3 tendieren. Da müssen dann nur noch die Nutzdaten zu bytes konvertiert werden. Die Kommandos sind dann schon in passender Form. Ein weiteres Problem wäre die Feststellung der Länge der Nutzdaten nach jedem Kommando. Da könnte man dann nach jedem Kommando noch 2 weitere Bytes setzen, die die Länge der folgenden Nutzdaten beinhaltet.

Hat jemand eine Idee, wie ich ein Kommunikationsprotokoll, dass meine oben genannten Ziele erfüllt am geschicktesten umsetzen kann?
 
Zuletzt bearbeitet:
Es gibt für etliche Anwendungen standardisierte Protokolle und im Normalfall ist es überflüssig, das Rad neu zu erfinden.

Außerdem:
"Das einzige das die unterste Schicht mit den Daten macht, ist ein Längenprefix voranzustellen, das die Länge der Daten enthält, damit der Client weiß, wann alle Daten eines Pakets erhalten wurden."

Das wird dir von TCP bereits abgenommen.
 
Für ein Anwendungsframe halt nicht. Ich habs, bei eigenen super simplen Protokollen für einen speziellen Anwendungszweckes, immer so gemacht, dass ich eine Enum genommen habe. Die kannste dann direkt in Byte convertieren bzw. das Byte wieder zur enum casten. Also Methode 3. Wenn du nur ein Command pro Paket machst, brauchst ansonsten keine Länge. Ansonsten, wie du schon geschrieben hast, noch Längenangaben. Du kannst dir aber auch z.B. feste Parameterlängen definieren. Dann kannste dir die Längenangabe sparen.
 
Zuletzt bearbeitet:
asdfman schrieb:
Es gibt für etliche Anwendungen standardisierte Protokolle und im Normalfall ist es überflüssig, das Rad neu zu erfinden.

Mir ist bewusst das es standardisierte Protokolle gibt, aber was kann ich damit anfangen? Irgendwie muss ich die Daten meiner Anwendung ja in einer Form zum Server bringen. Dazu habe ich Kommandos, die auf dem Server entsprechende Prozeduren anstoßen sollen. Was für ein Protokoll sollte ich denn verwenden, wenn ich Beispielsweise einen Chat-Server schreiben will?

Außerdem:
"Das einzige das die unterste Schicht mit den Daten macht, ist ein Längenprefix voranzustellen, das die Länge der Daten enthält, damit der Client weiß, wann alle Daten eines Pakets erhalten wurden."

Das wird dir von TCP bereits abgenommen.

Sende ich beliebige Daten als bytes zu meinem Server/Client, dann kommt das als Stream dort an und landen in einem Buffer. Bei einzelnen Sende-Vorgängen kann das egal sein, dann wird die Empfangs-Methode im Server aufgerufen und im Buffer sind nur die Daten dieser einen Nachricht. Sendet man jedoch schnell hintereinander oder ist die Nachricht größer als der Buffer, dann sind mehrere Nachrichten in einem Buffer, oder einen Nachricht auf mehrere Buffer verteilt. Darum sende ich die Länge mit. Der Empfänger weiß dann wie weit er aus dem Buffer lesen muss oder wie viele Bytes noch fehlen.

BlooDFreeZe schrieb:
Du kannst dir aber auch z.B. feste Parameterlängen definieren. Dann kannste dir die Längenangabe sparen.


Das mit dem Enum klingt gut.
Mit der festen Parameterlänge bin ich aber ziemlich gebunden. Das klappt bei wenigen Commands bestimmt gut und ist auch einfach umzusetzen, nur wenn die Anzahl der Commands zunimmt, müsste ich mich an dem längsten möglichen Parameter orientieren und würde so vermutlicht mehr Bytes verschwenden, als wenn ich einfach noch zusätzlich ein paar Bytes an den Command hänge. ist halt etwas aufwendiger, da das Command geparst werden muss und danach noch die Länge und schließlich die Nutzdaten/Parameter.
Da würde sich dann schon eher "ein Paket, ein Befehl" anbieten.

Wenn ich nun allerdings das Beispiel einer Chatnachricht aufgreife dann wirds wieder kompliziert: Es soll einen Nachricht von einem Nutzer zu einem anderen gesendet werden. Daten die dabei an den Server übertragen werden sollen sidn also folgende: Nachrichtentext, Empfänger.

Es gibt dann ein Command cmdSAY mit den Parametern Text und Empfänger.
ALso in etwa so:
cmdSay|Text|Empfänger
Jetzt ist die Frage: Wie löse ich das mit den Parametern am geschicktesten?
Der Befehl cmdSay ist sind die bytes eines Enums. Die bytes werden dann wieder umgewandelt und ich weiß, welchen Befehl ich habe. Dazu weiß ich dann noch, dass der Befehl eben die Parameter Text und Empfänger hat (das ist eben irgendwo irgendwie vermerkt). Nur wie bekomme ich die Parameter heraus?

Entweder gebe ich die Länge eines jeden Paramters mit. Nur wo schreib ich die hin und wie erkenne ich diese? Was ist wenn ein Befehl keine oder 3 oder mehr Parameter hat? Dann steigt die Anzahl Längenangaben mit. Beispiel:

cmdSAY|LängeParameter1|LäneParameter2|Parameter1|Parameter2

Ich könnte ein Trennzeichen zwischen den Parametern einführen, nur muss ich dann schauen, dass dieses Trennzeichen nicht in den Parametern oder sonstwo vorkommt.

Ich könnte das textbasiert lösen:
cmdSAY|text="blablub"|user="testuser"
und dann eben nach 'text="' und 'user="' suchen. Das ist aber wieder blöd zum parsen (von bytes zu String, dann im String suchen und den gefunden Wert dann extrahieren)

Oder noch folgendes:
Ein Paket ein Command/Parameter. Das heißt, es kommt ein Paket mit einem Command an und danach folgen in einzelnen Paketen die Parameter zum Command.
 
Zuletzt bearbeitet:
Hellblazer schrieb:
Mir ist bewusst das es standardisierte Protokolle gibt, aber was kann ich damit anfangen? Irgendwie muss ich die Daten meiner Anwendung ja in einer Form zum Server bringen. Dazu habe ich Kommandos, die auf dem Server entsprechende Prozeduren anstoßen sollen. Was für ein Protokoll sollte ich denn verwenden, wenn ich Beispielsweise einen Chat-Server schreiben will?
RFC 1459



Sende ich beliebige Daten als bytes zu meinem Server/Client, dann kommt das als Stream dort an und landen in einem Buffer. Bei einzelnen Sende-Vorgängen kann das egal sein, dann wird die Empfangs-Methode im Server aufgerufen und im Buffer sind nur die Daten dieser einen Nachricht. Sendet man jedoch schnell hintereinander oder ist die Nachricht größer als der Buffer, dann sind mehrere Nachrichten in einem Buffer, oder einen Nachricht auf mehrere Buffer verteilt. Darum sende ich die Länge mit. Der Empfänger weiß dann wie weit er aus dem Buffer lesen muss oder wie viele Bytes noch fehlen.
Fragmentierte Pakete werden von TCP wieder zusammengesetzt. Wenn ein Paket größer ist als dein Puffer, solltest du ihn bei Bedarf vergrößern. Deine Superidee hatten schon andere und sie führte zu Heartbleed.

Der Rest deines Posts ist mit RFC 1459 erschöpfend beantwortet.
 
Hellblazer schrieb:
Was für ein Protokoll sollte ich denn verwenden, wenn ich Beispielsweise einen Chat-Server schreiben will?
Im einfachsten Fall? Bonjour/ZeroConf, in Kombination mit Jabber. Anschalten und läuft. Keine Client-Server - Architektur, kein IP-Gemurkse,...
 
Hellblazer schrieb:
Der Server basiert auf SocketAsyncEventArgs.
Dein Server basiert auf Sockets und nicht auf irgendwelchen EventArgs.

Hellblazer schrieb:
Das Grundgerüst des Servers soll variabel einsetzbar sein.
Wie meine Vorposter schon erwähnt haben brauchst du das Rad nicht neu erfinden.
Da du auf .NET setzt, solltest du dir die Windows Communication Foundation ansehen.

Es existiert auch ein umfangreicher CodeProject Artikel der die Erstellung einer WCF p2p Chat Application beschreibt: http://www.codeproject.com/Articles/19752/WCF-WPF-Chat-Application
 
asdfman schrieb:

Daaron schrieb:
Im einfachsten Fall? Bonjour/ZeroConf, in Kombination mit Jabber. Anschalten und läuft. Keine Client-Server - Architektur, kein IP-Gemurkse,...

Okay, ich hab nach einem Chat-Protokoll gefragt und mir wurden passende Antworten geliefert ;). Möglicherweise will ich den Server aber für etwas anderes als einen Chat nutzen. Klar, ich könnte in meiner höheren Schicht diverse standardisierte Protokolle für jeden möglichen Einsatzzweck (Chat, Datentransfer...) umsetzen. Aber dann müsste ich mich ja mit Standards rumschlagen :D


asdfman schrieb:
Fragmentierte Pakete werden von TCP wieder zusammengesetzt. Wenn ein Paket größer ist als dein Puffer, solltest du ihn bei Bedarf vergrößern. Deine Superidee hatten schon andere und sie führte zu Heartbleed.


Bei Verwendung der SocketAsyncEventArgs wird ein Buffer-Block verwendet, dessen Größe am Anfang festgelegt wird, und aus dem jedes SocketAsyncEventArgs einen Bufferbereich zugewiesen bekommt. Vergrößern oder verkleinern geht dann nicht mehr. Wo liegt konkret das Problem dabei? Am Anfang eines jeden Frames wird die Länge angegeben. Ist die größer als eine angegebene maximal Länge, wird die Verbindung getrennt. Passt die Größe werden eben die nötigen Bytes aus dem Buffer gelesen. Kommen manipulierte Daten an, deren Länge kürzer als die angegebene ist, fällt das beim Parsen der Nachricht auf, oder möglicherweise auch beim nächsten Längenprefix. Dabei werden dann nämlich Bytes an der falschen Stelle gelesen und das könnte dann zu einem zu großen Wert führen. Manipuliert jemand die Daten so geschickt, dass der nächste Längenprefix noch eine erlaubte Länge ergibt, beim Parsen alles klappt (valides Kommando) und die AUsführung des Kommandos auch wie erwartet ist, hat man eigentlich nichts gewonnen, da ja alles reibungslos lief.
Aber eigentlich ist das egal, da mit den Daten idR etwas geschieht und kein einfaches Echo ausgeführt wird. Selbst wenn nur ein Echo ausgeführt werden sollte, dann bekommt man nur Daten aus dem eigenen Buffer, also etwas was sowieso schonmal gesendet wurde.
Und das ist nicht meine "Superidee"

Grantig schrieb:
Dein Server basiert auf Sockets und nicht auf irgendwelchen EventArgs.

Dass der Server im Grunde auf Sockets basiert, wenn ich ein Link zu den SocketAsyncEventArgs poste, ist doch klar. Nur kann man Sockets eben verschieden "ansprechen". Ich hätte einen primitiven Socket-Server schreiben können, einen Threaded-Socket-Server mit einfachen Threads, einen asynchronen Socket-Server mit den BeginXXX-Methoden oder eben einen asynchronen Socket-Server mit den SocketAsyncEventArgs. Ja, es ist ein Server basierend auf Sockets, aber er basiert ja auch auf den SocketAsyncEventArgs, oder baut darauf nichts auf? Vielleicht hätte ich schreiben sollen, dass der Server auf dem Muster der SocketAsyncEventArgs basiert...
Aber danke, dass du mich darauf hinweißt ;)

Wie meine Vorposter schon erwähnt haben brauchst du das Rad nicht neu erfinden.
Da du auf .NET setzt, solltest du dir die Windows Communication Foundation ansehen.

Es existiert auch ein umfangreicher CodeProject Artikel der die Erstellung einer WCF p2p Chat Application beschreibt: http://www.codeproject.com/Articles/19752/WCF-WPF-Chat-Application


Ich habe die SocketAsyncEventArgs schon bewusst gewählt. Ich will kein XML, sonst hätte ich mir einen SOAP-Service schreiben können, ich will kein RPC, sonst hätt ich das genutzt.

Warum hab ich gerade das Prinzip mit den SocketAsyncEventArgs genutzt? Ich wollte einfach mal einen Socket-Server schreiben. Ein einfacher Server mit Threads ist zu langweilig und skaliert nicht gut. Also kamen entweder die asynchronen BeginXXX-Methoden in Frage oder eben die SocketAsyncEventArgs. In Verbindung mit der Wiederverwendung diverser Objekte sind die ziemlich performant und skalieren gut. Auf meinem Core I5 4200u laufen im Echo-Modus (mangels Kommunikations-Protokoll geht nicht viel anderes als Echo) 10000-15000 Verbindungen ohne sonderlich hohe CPU-Last. Nur der Speicher wird, wenn gesendet wird was geht (Endloschleife), durch diverse Warteschlangen und lange Test-Strings (500-1000 Zeichen) etwas belastet. Bei mehr als 15000 Verbindungen steigt dann entweder das Client Programm oder der Server aus (woran das liegt, habe ich noch nicht weiterverfolgt, möglicherweise daran, dass Client-Programm und Server auf dem gleichen PC laufen).

So, nun habe ich mir eben diesen Socket-Server mehr aus Spaß geschrieben und will nun ein Kommunikationsprotokoll dazu entwerfen (auch nur mehr zum Spaß und der Erfahrung halber).
 
Zuletzt bearbeitet:
Zurück
Oben