# Client/Server-Protokoll implementieren - aber wie?



## clupus (23. Oktober 2010)

Hallo an alle,

ich will ein Java-Programm schreiben, das als Client/Server-Architektur aufgebaut ist. Jetzt müssen die beiden Teile miteinander reden und hier habe ich eine Frage:

Wie geht das am einfachsten/effektivsten? (es sollen hier ca. 15 Befehle realiesiert weden, die teilweise umfangreiche Rückgabewerte liefern (Verzeichnis auflisten,...))

Ich hatte mit folgende Ideen zurecht gelegt:

Serialisierte Objekte
Plain String mit entsprechend definierten Komandos und manuelles Auslesen
Plain String und automatisches Auslesen über z.B. jflex/byaccj (Textmustererkennung)
via XML

Alle 3 Möglichkeiten haben Vor- und Nachteile:
Objekte sind einfach zu realisieren,  Änderngen an den Objekt-Klassendefinitionen erfordern aber gleichzeitiges Update beider Software-Teile.
Strings sind einfach auszugeben aber das Einlesen macht Probleme. Entweder man muss mit einem StringTokenizer den String zerstückeln und alles per Hand auswerten (Auswand!) oder man setzt Software-Werkzeuge und Grammatiken ein. Zweiteres scheint mir etwas zu sehr mit Kanonen auf Spatzen geschossen zu sein.
XML is auch recht schnell erstellt, aber die Analyse einer DOM-Struktur etwas aufwändiger.

Ich habe auch schon an RMI gedacht aber da tun sich mehrere Fragen/Probeleme auf:
Geht das auch über eine nicht-sichere Verbindung?
RMI scheint mir recht komplex zu sein bzw man muss eben immer ein regestry laufen haben. Das kommt mir sogar noch mehr wie "Kanonen" vor.

Vieleicht kann ich von euch ein paar gute Tipps/Ideen bekommen und vielleicht auch eine Aussage wie "So macht man das im Normalfall am einfachsten..."

Danke
Christian


----------



## SPiKEe (23. Oktober 2010)

zwei prozesse über eine client/client (P2P) bzw server/client achitektur mit ein ander kommunizieren zu lassen ist sehr einfach und mit nicht mal 100 zeilen code realisierbar *zumindest in JAVA*
ein umfangreiches , standardisiertes protokoll was fest vorgibt welche informationen ausgetauscht werden können / müssen und wie diese darzustellen ... das ist schon etwas ganz anderes ...

zunächst mal solltest du dir klar machen WELCHE daten du übertragen willst und WIE diese ggf encoded werden müssen um über den entsprechenden stream geschickt werden zu können
wenn du also nur "text" senden willst *eine nicht-graphische darstellung eines verzeichnisses ist auch nur text* kannst du auf die zeichen-orientierte streams zurück greifen ...
willst du allerdings RAW-daten austauschen brauchst du schon die tieferliegenden byte-orientierten streams
*wobei die zeichen-orientierten streams auch nur auf den darunterliegenden byte-orientierten streams aufbauen und damit der unterschied eigentlich nicht so groß ist sollte man darüber keine RAW-daten senden ... oder zumindest nicht ohne diese vorher entsprechend zu codieren *z.B. Base64**
dann brauchst du auch etwas logik hinter deinem protokoll ...
kugg dir z.B. mal HTTP und FTP im vergleich an ...
HTTP geht nur über eine verbindung die auch *in den meisten fällen* nur für eine einmalige übertragung genutz wird *ausnahme ist hier HTTP/1.1 welche mit dem Keep-Alive - Header dafür sorgen kann das die verbindung offenbleibt um mehrere daten von einem server anzufordern ... sparrt deutlich overhead*
FTP dagegen besitz schon 2 verbindungen ... die controll-connection *TCP/21* und die data-connection *active : TCP/20 ; passive TCP/1025-65000* ...
die controll-verbindung bleibt die ganze FTP-session über geöffnet und dient dem meist a-synchronen übertragen von steuer-daten ...
diese daten bestehen nur aus für menschen lesbaren zeichen ... hier kann man also bei einer beispiel-implementierung auf zeichen-orientierte streams zurückgreifen ohne das fehlerquellen entstehen ...
die zweite verbindung ... die sog. data-verbindung wird nur zum übertragen von binär-daten verwendet ...
auch für z.B. TXT oder HTML ... also was man zwar als mensch auch lesen kann was aber trotzdem binär übertragen wird
diese verbindung wird meistens auch so lange offen gehalten bis die zugehörige datei vollständig übertragen wurde ... danach wird sie wieder abgebaut ...
was den unterschied ACTIVE-FTP und PASSIVE-FTP angeht kannst du unter http://de.wikipedia.org/wiki/FTP nachlesen
gut ... ziemlich viel theorie erstmal ... aber um sowas musst du dir echt gedanken machen bei der entwicklung eines protokolls

wenn du dann weist welche und vorallem wie daten übertragen werden sollen kannst du dich daran machen für beide seiten entsprechenden writer und reader zu basteln ...
wenn du z.b. nur lesbare informationen übertragen willst würde ein stringtokenizer schon viel arbeit abnehmen ...
dann noch die aufbereitung und auswertung der daten in jeweils eigene klassen stecken und schon bist du fast fertig ...
wenn du aber noch zusätzlich *oder ganz und gar NUR* binäre-daten übertragen willst / musst dann wird das ganze schon etwas haariger ...
weil dann musst du dir wirklich überlegen wie du dann die einzelnen binären daten sicher von einander wieder trennst ... ein einfach 1-byte-zeichen wird hier nicht genügen ... da müssen dann schon terminierungs-bytes ... offsets ... längen ... und so weiter angegeben werden damit da halbwegs verwertbare informationen gewonnen werden können ...

wie gesagt : die einfach kommunikation zweier prozesse übers netz an sich stellt kein problem dar ...
erst das verwendete protokoll und der damit verbunden overhead macht das ganze immer so kompliziert

hoffe ich konnte dir damit erstmal ein kleines stück weiterhelfen
ich selbst habe nicht wirklich ahnung von der entwicklung eines netzwerk-protokolls ...
klar habe ich auch schon server/client - apps geschrieben die ihr eigenes mini-protokoll auf basis von einfachen commando-strings hatten .. aber das kann man noch nicht als protokoll bezeichnen da es lediglich der austausch von nötigen informationen war die zusammengepackt einfach auf die reise geschickt wurden ... die encodierung hat dann die meiste zeit in anspruch genommen

aber ich bin sicher das dir hier einige andere durchaus weiterhelfen können und dir tipps und denkanstöße zur entwicklung eines netzwerk-protokolls geben können


----------



## clupus (23. Oktober 2010)

Hallo SPiKEe,

nein, binäre Daten oder so was brauche ich nicht zu übertragen, da kann ich notfalls auf einen HTTP-Server ausweichen ;-)

Ein einfaches "Protokoll"  sollte da schon genügen


> apps geschrieben die ihr eigenes mini-protokoll auf basis von einfachen commando-strings hatten



Mir gehts auch nicht wirklichum das Protokoll an sich, sonern, was ich in Java einfach umsetzen kann: Eine Implementierung eines Datenaustauschs auf Basis von lesbaren Strings wäre schon ausreichend.

Nur als Bsp (nich ganz logisch, weil Teile des Programms fehlen):
Ich möche das aktuelle Verzeichnis wecheln können und im aktuellen Verzeichnis Dateien und Verzeichnise auflisten (und zurückgeben) lassen können. Es sollen Dateien gelöscht werden und andere Aktionen mit ausgeführt werden können.
Soweit klingt das fast wie der Log einer Konsole. Die anderen Befehle werden ungefähr den selben Komplexitätsgrad haben.

Verstehe ich das richtig, dass du der Meinung bist, wenn ich über einen StringTokenizer gehe, dass dann eine manuelle Auswertung der übertargenen Daten von beiden Seiten aus am effektivsten/einfachsten ist?

Danke schon mal für die langen Ausführungen
Christian


----------



## zeja (23. Oktober 2010)

Klingt eigentlich wie FTP was du vorhast. Sollst du das nachbauen?

Ich würde keinen StringTokenizer verwenden, sondern mir geeignete Objekte für den Austausch deiner Informationen überlegen und dies dann Serialisieren und auf der anderen Seite wieder auslesen. Strings parsen ist immer problematisch.


----------



## SPiKEe (23. Oktober 2010)

clupus hat gesagt.:


> Mir gehts auch nicht wirklichum das Protokoll an sich, sonern, was ich in Java einfach umsetzen kann: Eine Implementierung eines Datenaustauschs auf Basis von lesbaren Strings wäre schon ausreichend.



öhm ... das is ganz einfach ...
einfach ne verbindung aufbauen und dann einfach mit den streams des sockets arbeiten
n einfaches beispiel wäre z.B. das der server über Runtime.getRumtime().exec() ne CMD oder n TERMINAL startet und dann an den socket hängt ...
mit dem client machst du das genau umgekehrt ... da hängst du den clienten an die console und leitest die eingaben durch den socket zum server weiter ... der führt die dann auf seiner konsole aus und schickt das ergebnis via socket zu dir zurück ... das kannst du dann einfach ausgeben lassen ...

ich weis ... klingt wahrscheinlich ziemlich dumm dieses beispiel ... aber abgesehen von verschlüsselung und authetifizierung ist es im grunde das was zum beispiel bei SSH abläuft ..

wie gesagt ... wäre jetzt so das einfachste beispiel ...



clupus hat gesagt.:


> Nur als Bsp (nich ganz logisch, weil Teile des Programms fehlen):
> Ich möche das aktuelle Verzeichnis wecheln können und im aktuellen Verzeichnis Dateien und Verzeichnise auflisten (und zurückgeben) lassen können. Es sollen Dateien gelöscht werden und andere Aktionen mit ausgeführt werden können.
> Soweit klingt das fast wie der Log einer Konsole. Die anderen Befehle werden ungefähr den selben Komplexitätsgrad haben.



ähm ... dieses beispiel würde ich unter dem sammelbegriff DATENVERWALTUNG abstempeln
abgesehen davon das es sowas mitlerweile eine million mal implementiert gibt *auch als netzwerk-variante *sie PowerShell *win* oder SSH *unix** würde ich bei sowas immer abstrakt beliben und versuchen so viel wie möglich an das host-system zu delegieren ... spart dir ne menge code



clupus hat gesagt.:


> Verstehe ich das richtig, dass du der Meinung bist, wenn ich über einen StringTokenizer gehe, dass dann eine manuelle Auswertung der übertargenen Daten von beiden Seiten aus am effektivsten/einfachsten ist?



solltest du dich wirklich entschließen ein eigenes protokoll zu erarbeiten würde ich sagen das StringTonkenizer auf jeden fall die einfachste variante ist ... effektiv ... darüber lässt sich streiten
gucken wir uns doch mal den HTTP-Client von Apache etwas näher an ...
wenn du das ding im source mal völlig auseinander nimmst wirst du feststellen das du an einem punkt ankommen wirst wo genau solche dinge getan werden z.B. die zerlegung in HTTP-STATUS *erste zeile* HTTP-Header *standardisiert* sog. X-Header *nicht genormte header welche durch module *z.B. PHP - X-Powered-by: PHP* gesetz werden um clienten die möglichkeit zu geben erweiterte informationen zum server zu erhalten* und letzendlich dem content
grade die header werden mit StringTokenizer und der gleichen auseinander genommen *ggf auch mit regular expressions geparsed* und mit konstanten verglichen um zu überprüfen was denn genau drin steht ...
das ganze wird dann schön aufbereitet und dem user mit einer vielzahl von methoden zugänglich gemacht ...
genau das ist es aber was eine gute protokoll-implementation ausmacht ...
na klar ist jede implementation nur so gut wie das protokoll selbst auf der diese läuft

um jetzt aber nich wieder so n kilometer an text hier hin zu klatschen

ein einfaches protokoll zur datenverwaltung wäre zum bleistift folgendes

erstmal die definierten befehle , wie
DELETE
MOVE
COPY
NEW
etc ...

dann die syntax ... also wie diese befehle zusammengesetz werden müssen und wie die parameter zu übergeben sind ... z.b.
<COMMAND> <OPTIONS> <FILE>
oder auch sowas *was sich im parsen nachher einfacher macht*
"<COMMAND>"|"-o<OPTION1>=<VALUE1>[&<OPTION2>=<VALUE2>]"|"<FILE>"
hier ist schon die trennung mit vorgegeben ...
wenn du in einer normalen konsole bist ist der trenner meist <LEERZEICHEN> *oder hex 0x20*
wenn du das ganze aber nun über streams oder pipes schickst dann interpretieren diese manchmal das leerzeichen als terminator oder fehler ... daher ist es besser wenn man wirklich alles zusammen in einem zusammhängenden string verschickt
die anführungszeichen sowie die eindeutigen trenner zwischen einzelnen segmenten *z.B. zwischen den optionen* ermöglichem dem parser nachher gezielt das kommando in seine bestandteile zu zerlegen
dessweiteren wird von den meisten modernen interpretern etwas zwischen zwei anführungszeichen als zusammenhängende einheit betrachtet womit diese nicht z.b. an leerzeichen oder anderen verbindungsspezifischen sondernzeichen *in unserem fall das | zur trennung zwischen COMMAND , OPTION und FILE* ignoriert werden

das wäre dann schon erstmal die eine hälfte des protokolls ... die sog. COMMANDOS ...
weiter gehts mit den folgen ... die sog. RESPONSES ...

jedes commando was du ausführst hat auch rückgaben ... *zumindest sollte dies so sein*
diese müssen natürlich auch wieder verarbeitet und verschickt werden ...
und jetzt wird es in meinem kleinen beispiel schon haarig ...
mein protokoll sieht so aus
"<COMMAND>"|"<OPTIONS>"|"<FILE>"
um jetzt bei einer antwort meinem protokoll treu zu bleiben brauchen wir als COMMAND ein festes keyword das signalisiert : achtung ! das hier ist ne antwort auf ein command ... *auf welches ist hier noch egal*
nehmen wir dafür als COMMAND ganz eifnach mal "RESPONSE"
allerdings haben wir jetzt keine datei mehr auf die wir uns beziehen können ...
also setzen wir diesen wert einfach auf "NULL"
also sieht eine antwort IMMER ... und wirklich IMMER in jedem fall .. so aus
"RESPONSE"|"<OPTIONS>"|"NULL"
hier ist jetzt der unterschied zwischen NULL und "NULL" zu beachten ...
während "NULL" ein string ist der dann auch genau so auftaucht ist NULL einfach das fehlen von informationen / pointern / daten / etc
also muss als weitere protokoll-spezifikation festgelegt werden : ALLES IST EIN STRING ...
der parser muss nachher natürlich entsprechend gebastelt werden das er entweder dieses "NULL" ignoriert oder als sicherheitsmerkmal nimmt um zu prüfen ob es ein gültiger RESPONSE ist ... dann laut spezifikation *siehe oben* muss FILE in einem RESPONSE den string "NULL" enthalten

die eigentlich antwort müssen wird jetzt verpacken und in OPTIONS reinquetschen ...
dafür haben ich oben schon extra dieses kontrukt hier angeführt

"-o<OPTION1>=<VALUE1>[&<OPTION2>=<VALUE2>]"

erklärung

die anführungszeichen sind wieder vom protokoll vorgeschrieben ... diese dienen zusammen mit dem | zeichen als trenner
das -o leitet den OPTION-tag ein ...
da es auch COMMANDOS gibt die als optionen halt auch mal NICHTS haben können können wir hier bandbreite sparen in dem bei solchen fällen nach dem COMMAND gleich das FILE kommt ...
um dies festzustellen kann man einmal die möglichkeit wählen das man prüft ob es nur 2 elemente *COMMAND|FILE* oder doch alle 3 elemente *COMMAND|OPTION|FILE* gibt
zusätzlich kann man zur sicherheit noch auf das vorhandensein von -o prüfen ...
so kann man z.B. client-seitig schon fest stellen ob das kommando syntaktisch korrekt ist bevor man sinnlosen overhead erzeugt in dem man dieses falsche kommando erst zum server schickt der dann erst feststellen muss ob das kommando valid ist ...
das mag zwar bei 3 oder 4 kommandos noch keinen unterschied machen ... bei ein paar 10'000 wird man sehen wie anfällig es ist wenn man die gesamten zeiten zusammen rechnet
zurück zum thema
also aufbau für OPTION wählen wir folgendes model
"-oCMD=<Kommando, welches diesen RESPONSE ausgelöst hat>&STAT=<Status ; kann mit numerischen werten oder weiteren kommandos belegt werden>[&<DATA>]"
der letzte in eckigen klammernstehende teil ist optional und kann z.B. für Base64-kodierte binär-daten *ja ... man kann binär-daten in strings packen wenn diese durch Base64 in darstellbare zeichen umgewandelt wurden* oder ausführlichen fehlermeldungen falls der fehler beim server liegt und der client über den mitgeteilten status nicht genug informationen hätte *müsste man server-seitig lösen das immer genug informationen vorhanden sind*

damit wären wir erstmal mit dem protokoll an sich selbst soweit durch ...
alle spezifikationen die benöigt werden sind gegeben ...
wenden wir uns der aufbereitung der daten zu

wenn nun einer der beiden ein solches kommando bekommt .. sei es angefordert oder nicht *z.B. durch erreichen von TimeOut's sendet der server dem client ein PING welches er bestätigt haben möchte ...
der client muss darauf hin mit einem spezifierten PONG *ping-response* antworten ... andernfalls wird die verbindung getrennt* ... muss dieses ersteinmal wieder in seine bestandteile zerlegt werden
dazu bediesen wir uns entweder java.lang.String.split(String regex) oder z.B. dem StringTokenizer
hierbei muss auf korrekte implementation geachtet werden ...
in der ersten phase darf nur an

"|"

getrennt werden ... *einmal bei COMMAND|FILE ; zweimal bei COMMAND|OPTION|FILE*
daher ist es beim zusammenbau des strings auf der anderen seite unbedingt zu vermeiden das genau dieses sonderzeichen *ja es sind zwar 3 zeichen .. aber es wird als ein trenner gehandhabt* nur an den dafür vorgesehen stellen auftaucht ...
sollte es nicht möglich sein dies einzuhalten muss es entsprechend maskiert werden damit genau diese regel erfüllt wird

wenn wir also an unserem trenner getrennt haben haben wir folgende drei teil-strings

"COMMAND
OPTION
FILE"

die beiden anführungszeichen vorne und hinten können jetzt weg *waren halt nur drum zur vermeidung von fehlerhaften trennungen des strings*
dann muss man sich überlegen was sinnvoller ist ...
erst versucht die OPTION zu parsen *VORHER natürlich chekcen ob überhaupt das OPTION-tag vorhanden ist*
oder aber erst zu kuggn welches COMMAND vorliegt um sich darauf vorzubereiten das es eventuell garkein OPTION-tag gibt welches noch zusätzlich geparst werden muss ...

ich würde die letztere variante bevorzugen ... wobei es sicher auch dinge gibt wo die erste durchaus sinn machen wird

für den fall das OPTION noch geparst werden muss

einfach wieder am trenner *diesmal &* trennen ...
dann haben wir erstmal die OPTION=VALUE - paare ...
da kann man entweder drüber-iterieren oder in einer Map *geht hier auch property ? .. mal bitte was sagen wenn ja* die entsprechent gestaltet ist speichern

das wäre eigentlich alles zum punkt OPTION

nach dem wir also unseren fest-spezifizierten string wieder zerlegt haben beginnt das interpretieren der informationen
dazu geb ich jetzt hier kein beispiel ... ich denke dass solltest du auch alleine hinbekommen


analog geht natürlich das senden von informationen
einfach von hier unten anfangen und dann wieder nach oben alles verschachteln ... zum schluss kommt dann oben an der stelle wo ich geschrieben hab "wenn nun einer der beiden ein solches kommando bekommt" wieder das fertige kommando zustande


die ganze kommunikation *netzwerk , internet* und die umsetzung der befehle ist jetzt dein restlicher part der implementation ...

ich habe dir jetzt hier lediglich ein beispiel-protokoll geliefert wie ich das angehen würde ...
wie du es jetzt umsetz und ob es dir reicht .... nun ... das hängt von dir und davon ab was du damit machen willst


hoffe ich konnte dir helfen ... auch wenn es mal wieder extrem lang geworden ist ...


----------

