Socket Datentransfer mit Überwachung für InputStream

Hey,

Ich bin dabei einen Datenaustausch zwischen Socket <=> ServerSocket und umgekehrt zu schreiben was auch super klappt.
Nun würde ich das ganze jedoch gerne erweitern indem Client und Server ihren ImputStream auf Befehle überwachen, das habe ich in einem Extra Thread erledigen lassen und an für sich
funktioniert das auch doch ist mir spätestens beim übertragen des eigentlichen Inhalts aufgefallen, das ja der gegenüberliegende InputStream blockiert und somit ist der eigentliche Datenaustausch verhindert bzw verschoben.

Sollte ich vielleicht zum Überwachen der Socket und ServerSocket InputStreams einen anderen Port nutzen, oder gibt es da vielleicht
eine andere Möglichkeit?
Ich hatte schon in Erwägung gezogen mittels inputstream.available() in einer Schleife zu prüfen ob Inhalt vorhanden ist und ggf. den Input zu stoppen, allerdings
gefällt mir dieser Ansatz nicht so. :frowning:

Danke!

Erstmal ein wenig auseinander frimeln …

Du sagst “Socket <-> ServerSocket”, das impliziert das du TCP nutzt, und TCP ist grundsätzlich eine bi-direktionale Verbindung, es können also über eine Verbindung in beide Richtungen Daten ausgetauscht werden. Damit fällt dein Ansatz bezüglich zweier unterschiedlicher Ports schon mal raus.

[off-top]An jeden der den Einwand bringen will : ja, ich weis selbst wie FTP abläuft und das es dort so gemacht wird, aber darauf komme ich später.[/off-top]

Dann hast du noch erwähnt das du von EINEM InputStream mit ZWEI Threads “parallel” liest. Schon mal ne ganz schlechte Idee. Der Grund liegt darin das es normalerweise interne Buffer gibt so das wenn man in einem Thread was liest gewisse Informationen für den anderen Thread verloren und somit die Daten “kaputt gehen”. Davon abgesehen ist es bei Nebenläuffigkeit ohne weitere synchronisation nicht vorhersehbar was wann wie passiert, also ob halt erst Thread1 an die Reihe kommt oder Thread2.
Von daher würde ich empfehlen : nutze nur EINEN Thread der die Daten liest, auswertet/verarbeitet, und wenn nötig an andere Threads weitergibt.

Weiter im Text : deine Aussage bezüglich “der InputStream der anderen Seite blockiert” musst du zumindest mir bitte noch mal deutlicher erklären. Natürlich blockiert InputStream.read() so lange bis Daten vorliegen die dann auch eingelesen werden können, das besagt schon die DOC weshalb man solche Methoden auch “blockierende Methoden” nennt : der Aufruf bleibt an dieser Stelle so lange stehen bis ein Ergebnis geliefert werden kann. Wenn du also einen Thread hast der den InputStream liest, und einen zweiten der gerade was in den OutputStream zu Gegenstelle reinschreibt, dann bleibt read() trotzdem stehen da ja noch keine Daten von der Gegenstelle gekommen sind.

Von deiner Problembeschreibung her lässt sich darauf schließen das du eine Race-Condition bzw. einen Deadlock hast : beide Seiten warten auf die jeweils andere.

Was auch durchschimmert : du hast kein Protokoll, also keine Festlegung in welcher Form die Daten übertragen werden sondern sendest halt munter drauf los und hoffst dass das ganze auf der Gegenseite schon irgendwie zusammenpassend ankommt. Auch das kann natürlich zu weiteren Fehlern, Datenverlust oder ganz anderen Dingen führen.

Vielleicht solltest du dir einfach noch mal einen Zettel und einen Stift zur Hand nehmen und dir Schritt für Schritt überlegen wie das Ganz ablaufen soll. Auch solltest du dir dabei gleich überlegen wie die eine Seite der anderen Commandos übermittelt und wie diese von Payload-Daten getrennt werden.

Ein einfacher Anfang ist es das man die Länge mitsendet : man baut sich auf Seite A die gesamte Nachricht zusammen, ermittelt dann ihre Länge, und schickt diese Information voran. Auf der Gegenseite liest man dann erstmal nur die Länge und weis dann wie viele Daten kommen werden. Das kann man jetzt natürlich noch weiter unterteilen das man erstmal sagt : Länge des nächsten Commandos, Commando, Payload. So weis dann Seite B : aha, jetzt muss ich erstmal 10Byte an Commando lesen, kann dieses dann auswerten, und weis dann durch dieses Commando was an Payload kommt. Parktisch sollte man dann noch in das Commando selbst bzw anschließend die Länge des Payloads packen damit man weis wie viel kommt.

Auch kann man es bei binären Protokollen etwas sparsamer machen in dem man die ersten 1 oder 2 Bytes nicht für die Länge des Commandos nimmt sondern das Commando selbst in diesen festgelegten Header drückt.

Kleines Beispiel : sagen wir mein Protokoll sieht folgendermaßen aus : 2Byte Commando + 4Byte Länge des Payload + Payload

Dann bleibt mir :

2Byte Commando = 16 bit = 65536 Möglichkeiten für ein bestimmtes Commando was ich mit diesen 2 Bytes darstellen kann. Das sollte mehr als ausreichen sein. Normalerweise reicht sogar genau 1Byte = 8Bit = 256Möglichkeiten, weil was bitte ist so umfangreich das man mehr als 256 verschiedene Commando-Sequenzen braucht, aber egal.

4Byte Länge des Payload = 32 Bit = 4294967296 Möglichkeiten. Normalerweise wird ein “signed Int” genutzt, also 31Bit = 2GigaByte. Ich kann also mit den folgenden 4Bytes die Größe der nachfolgenden Nutzdaten bis zu 2GB angeben. Und so große Daten sendet man eigentlich nicht in einem Stück.

Du siehst : mit 6Bytes kann ich bereits angeben WAS gemacht werden soll und WIE VIEL noch an Nutzdaten kommt die dann dem Commando entsprechend zu verarbeiten sind. Diese 6Bytes sind fester Bestandteil des Protokolls, kommen also grundsätzlich immer als erstes. Jetzt kann man seinen Code so aufbauen das der main-Thread mit einem InputStream.read(byte[6]) darauf wartet genau diese 6 Bytes von der Gegenstelle zu lesen und dann zu verarbeiten. Wenn man dann weis was mit den folgenden Daten gemacht werden soll und wie viel nocht kommt, dann liest man diese Daten entsprechend hintereinander ein und übergibt sie dann an z.B. an einen weiteren Thread zur Bearbeitung so das man währenddessen schon die nächsten Daten lesen kann.

Um noch mal auf FTP zu kommen was ich vorhin angerissen hatte : bei FTP läuft das alles ein wenig anders ab. Es gibt nämlich eigentlich 2 Verbindungen : einen Steuer-Kanal (TCP/21) und einen Daten-Kanal (TCP/20). Auf dem Steuer-Kanal werden nur reine Commandos und Status-Rückmeldungen übertragen aber keinerlei Nutzdaten. Auf dem Daten-Kanal entsprechend umgekehrt.
So, was passiert nun : der Client baut eine Verbindung über den Daten-Kanal TCP/21 zum Server auf und meldet sich darüber bei diesem. Es wird dann ein bisschen an Infos ausgetauscht bis es dann zur ersten Eigentlichen Datenübertragung kommt : LIST , dem Auflisten des aktuellen Verzeichnis. Beim “aktiven” FTP baut dann der Server eine Verbindung zum Client auf TCP/20 auf (zu passiv komm ich gleich) und tauscht dann darüber die Daten mit dem Client aus. Wenn also der Client über den Steuer-Kanal ein LIST schickt dann meldet der Server darüber zurück OK , dann die Länge wie viele Daten kommen, und dann die eigentlichen Daten über den Daten-Kanal. Die Information wie viele Daten kommen ist dabei natürlich für den Client sehr wichtig damit er weis wann die Übertragung beendet ist.

Heutzutage findet hingegen fast ausschließlich das “passive” FTP anwendung : dabei wird nicht vom Server eine Verbindung zum Client aufgebaut, sondern der Client sendet dem Server ein PASV, dieser macht darauf hin einen zweiten Socket auf und übermittelt dessen Daten über den Steuer-Kanal zurück an den Client. Dieser baut dann zu diesem Socket auch eine Verbindung auf und nutzt diese für den Daten-Austausch. Wichtig dabei ist das der Server anhand der dem Client übermittelten Daten auch selbst noch wissen muss das dieser zweite Socket zu dieser bestimmten Client-Session gehört. Ist an sich nicht kompliziert und schnell umgesetzt, kann bei Fehlern aber verdammt nach hinten losgehen.

Vielleicht noch als Tipp : dank Base64 kann man auch Binär-Daten über eine Text-Verbindung übertragen, das wäre dann mit den in Java vorhanden Mitteln vermutlich erstmal die einfachste Art da du auf Dinge wie z.B. readLine() eines Reader zurückgreifen kannst und dich erstmal nicht weiter mit der Länge rumschlagen musst. Sollte man aber nur nutzen um erstmal die Kommunikation an sich ans Laufen zu bekommen.

Wenn du noch mehr Ansätze brauchst kann ich dir gerne mal was einfaches zusammenbasteln damit du ein ganz grobe Struktur hast womit man dann erstmal ein paar Daten hin und her senden kann.

@Sen-Mithrarin
Vielen Dank für deine ausführliche Erklärung!

Zu dem was mit „der InputStream der anderen Seite blockiert“ gemeint ist, war sogar das was du im folgendem schon angesprochen hast,
Ich habe mir zuvor schon einige Gedanken zu einem Protokoll gemacht, da ich beidseitig Daten versenden möchte habe ich mir zwei separate Klassen geschrieben,
die erste ist meine DatenTransmitter Klasse, diese verlangt einen Input und Output Stream, sie bietet alles an um bestimmte Datensätze zu senden, der Gegensatz ist meine DatenReceiver Klasse welche die Daten annimmt.
Gesendet wird tatsächlich wie du beschrieben hast, das anfangs die Länge des Inhaltes an den GegenSocket gesandt wird, und in folge der Inhalt.
Die Daten sind (Modelle) welche als XML über die DatenTransmitter Klasse an die ferne DatenReceiver Klasse übergeben werden, und bilden dort wieder das Abbild des (Models) bzw das Objekt.
Damit ich den Spieß einfach umdrehen kann, wird der ServerSocket (Trasmitter Klasse) die Daten an den Client (Receiver Klasse) senden können aber auch genau umgekehrt.

Bis hier hin geht auch alles in Ordnung, doch es ist nun so, das mir noch etwas wichtiges fehlt, und zwar die Unterhaltung bevor zum Beispiel meine Methoden
wie ServerSocket (DatenTransmitter) : sendText(…) => Client (DatenReceiver) : receiveText() überhaupt zusammen Verwendung finden.
Mir fehlen Befehle die genau diese Methoden ansteuern, und genau da fängt das Problem an.

Wenn auf beiden Seiten jeweils die InputStreams blockiert sind, funktionieren meine Transmitter / Receiver Methoden nicht da einer der beiden Sockets wie Server oder Client immer noch einen Befehl erwarten, der ist dann ggf. die Länge des Contents und kein eigentlicher Befehl und so verschiebt sich die Kommunikation der Transmitter und Receiver Methoden immer mindestens im einen Wert.
Sen-Mithrarin, du hast mir das Beispiel vom FTP Protokoll genannt, das ein Port nur für die Kommunikation zuständig ist und der andere für den Datenaustausch, an für sich würde ich jetzt denken das wäre ideal allerdings meinst du das fällt weg, ich verstehe nicht ganz warum dieser Ansatz nicht gut ist.

Nochmals vielen Dank an dieser Stelle! :slight_smile:

[Ot]es gibt jetzt auch den offtopic tag, (ot)(/ot) ;)[/OT]

Hm, mal abgesehen von alledem, wenn man strings hin und her schickt, kann man bei nem bufferedreader auch einfach reader.isReady() abfragen. Das liefert true wenn ein "
" als zeilenumbruch am ende der vorliegenden daten existiert…

Warum musst du irgendwas überwachen und warum nutzt du kein bestehendes Kommunikationsframework wie RMI zum Beispiel?

Ich habe ja nicht gesagt das der Ansatz den FTP nutzt verkehrt wäre. Im Gegenteil : als FTP entwickelt wurde waren die Resourcen sehr knapp und die Kommunikation zwischen zwei Knoten war weitaus komplexer als heute dank TCP und CIDR wo man nur eine Ziel-Adresse angibt und die vorhandene Netz-Infrastruktur kümmert sich um den Rest. Es ist also eigentlich schon recht beachtlich was man schon damals so hinbekommen hat was auch heute noch (wenn auch in der Abwandlung des “passive”) so Verwendung findet.

Mir ist jetzt allerdings schon etwas klarer geworden wo es bei dir hakt : du willst anstatt eine Verbindung bi-direktional zu nutzen zwei Verbindung in nur jeweils eine Richtung nutzen. An sich nicht weiter das Problem und mit heutigen Resourcen auch eher machbar, aber die Gegenfrage wäre natürlich : warum umständlich wenns auch einfach geht ? Außerdem : es kann sein das du zwar von A eine Verbindung zu B herstellen kannst, aber nicht von B zu A weil z.B. eine Firewall diese Verbindung verhindert.

Natürlich kannst du eine Klasse als Sender und die andere als Receiver umsetzen, und auch beide auf beiden Seiten nutzen, das heißt aber nicht zwangsläufig das du auch zwei Verbindungen brauchst.

[QUOTE=NiRu]Bis hier hin geht auch alles in Ordnung, doch es ist nun so, das mir noch etwas wichtiges fehlt, und zwar die Unterhaltung bevor zum Beispiel meine Methoden
wie ServerSocket (DatenTransmitter) : sendText(…) => Client (DatenReceiver) : receiveText() überhaupt zusammen Verwendung finden.
Mir fehlen Befehle die genau diese Methoden ansteuern, und genau da fängt das Problem an.[/QUOTE]

Da kommen wir dem Problem doch schon mal etwas näher. Wenn ich dich richtig verstehe (und bitte korrigiere mich falls ich daneben liege) baust du zwar eine Verbindung auf, weist aber nicht wie du dann diese bestehende Verbindung an deine Klassen zur bi-direktionalen Kommunikation bewegst.

Nun, ich versuche es etwas pseudo-mäßig zu erklären :

Ein normaler Verbindungs-Aufbau über TCP läuft in Java wie folgt ab :

  1. Server öffnet einen ServerSocket auf einem bestimmten Port und hängt in einem loop in einem ServerSocket.accept().
    Da ServerSocket.accept() auch wieder blockierend ist wartet diese Zeile also so lange bis eine eingehende Verbindung ankommt. Schon deshalb ist es wichtig dies in einen Thread auszulagern (zumindest wenn sonst im main-Thread noch was anderes ablaufen soll).
  2. Ein Client baut nun über einen Socket eine Verbindung zu diesem Port auf.
  3. ServerSocket.accept() returnt ein Socket-Objekt was den Gegenpunkt darstellt. Die Verbindung steht jetzt zwischen dem Socket im Client und dem Socket im Server.
  4. Bei einfachen Dingen über blocking-i/o startet man nun einen zweiten sog. Handler-Thread dem dieser Socket übergeben wird. Der Haupt-Thread kehrt dann über den loop wieder zum accept() zurück und wartet auf eine neue Verbindung.
  5. Der neue Handler-Thread hat nun den Socket und kümmert sich um die Streams so wie wenn nötig den initialen Datenaustausch (wie Header bei Object-Streams oder Schlüssel-Austausch bei Verschlüsselung).
  6. Auch im Client wird die nun hergestellte Verbindung einem separaten Handler-Thread übergeben das der Rest des Programmes (also die GUI) normal weiter arbeiten kann.
  7. Aufgabe dieser Handler-Threads ist es nun in einem loop auf ein InputStream.read() zu warten bis Daten vorliegen. Wie bereits erwähnt ist auch InputStream.read() wieder eine blockierende Methode, wartet also bis Daten vorhanden sind.
  8. Beide Seiten verfügen nun über einen separaten Verbindungs-Thread der auf Daten von der Gegenstelle wartet.

So, und das ist nun der Punkt wo, wenn ich dich richtig verstanden habe, dein Problem besteht aus diesem Zustand die Daten über deine Klassen zu übertragen. Eigentlich nicht weiter das Problem und abhängig von der Logik des Protokolls.
Da nun beide Seiten bereit sind und auf ein Commando von der Gegenstelle warten muss nun eine dieser Seiten das gewünschte Commando ausführen und an die Gegenstelle senden. Im genannten Beispiel von FTP wäre dies dann normalerweise das Auflisten des aktuellen Remote-Verzeichnisses. Doch wie wird dieses Commando ausgelöst ? Nun, das ist jetzt wieder Teil der eigentlichen Aufgabe der Programmme (meist erfolgt der erste Schritt duch den Client der ja was vom Server will).

Ein FTP-Client läuft nun so das erstmal die Verbindung hergestellt wird und man dann so lange wartet bis das auch geschehen ist. Wenn dann also das Signal kommt : Verbindung hergestellt läuft der Code weiter und löst dann eben z.B. LIST aus. Dieses LIST wird dann einfach dem Handler-Thread übergeben der es über eine send()-Methode letzten Endes auf OutputStream.write() an die Gegenstelle schickt. Der Server, der ja noch immer auf Daten wartet, liest jetzt den Befehl LIST und beginnt darauf hin diese Abzuarbeiten. Im einfachsten Fall wäre es dann das aktuelle Verzeichnis zu ermitteln (bei FTP meist /ftp-root/@username), dessen Inhalt zu lesen und als Antwort bereit zustellen.

SO, und jetzt der Trick an der Sache :

Anstatt nun wie FTP eine zweite Verbindung zu initialisieren über die dann die Daten geschickt werden geht es einfach über die gleiche Verbindung zurück. Es kommt also z.B. ein OK-LIST-payload. Der Client, der ja seinerseits auf Daten wartet, liest jetzt das Commando OK-LIST, stellt fest das es eine Antwort auf eine vorherige Anfrage ist und ordnet dann den Payload als return der eigentlichen Anfrage zu.
Damit das dann funktioniert muss man im Client eine Art Liste erstellen in der in korrkter Reihenfolge die Anfragen stehen damit diesen dann die Antworten zugeordnet werden können. Das kann man entweder in einer sog. Blocking-Liste machen die sicherstellt das immer erst ein Antwort komplett abgearbeitet sein muss bevor die nächste Anfrage gesendet werden kann, oder man fügt eine Sequenz-Nummer hinzu die dann der eindeutigen Zuordnung dient.

Auf Client-Seite wäre es dann ca. so :

  1. Anfrage an Server : LIST
  2. auf Antwort warten
  3. Server verarbeitet und schickt Antwort
  4. Antwort empfangen und der Anfrage zuordnet
  5. aus dem Warte-Call mit dem Ergebnis zurückkehren

Man kann es natürlich noch etwas einfacher stricken : statt einem Thread der unabhängig von den Anfragen auf Antworten wartet einfach in die Anfrage den Code packen der direkt selbst auf eine Antwort wartet. Ist jetzt persönlich nicht mein Stil, aber das wäre dann das einfachste (vergleichbar mit Telnet oder ähnlichen).

Ich muss mal gucken ob ich mal eben auf die Schnelle ein Beispiel zusammen bekomme.

Das ganze kann man dann noch erweitern mit non-blocking-i/o und Multi-Thread und solche Scherze, aber ich denke das würde den Rahmen dieses Threads hier mehr als sprengen (zu mal ich da auch nciht so bewandert bin).

die Länge des Contents und kein eigentlicher Befehl und so verschiebt sich die Kommunikation der Transmitter und Receiver Methoden immer mindestens im einen Wert.
Sen-Mithrarin, du hast mir das Beispiel vom FTP Protokoll genannt, das ein Port nur für die Kommunikation zuständig ist und der andere für den Datenaustausch, an für sich würde ich jetzt denken das wäre ideal allerdings meinst du das fällt weg, ich verstehe nicht ganz warum dieser Ansatz nicht gut ist.