Wie versprochen im letzten Eintrag, gibt es gleich den Code zum TippDesTagesServer und Client, aber da ich bereits die Lösungen eingetragen habe, zeige ich den Code aus den Folien:


Der Server enthält ein Array namens tippListe, welches die Tipps beinhaltet, die der Server dann an den Client sendet. Der ServerSocket sorgt dafür, dass auf Port 4242 auf Client-Anfragen gewartet wird. Die accept Methode blockiert so lange, bis eine Anfrage hinein kommt. Dann gibt die Methode einen Socket für die Kommunikation zurück. Jedoch handelt es sich dann nicht um den Port 4242, sondern um einen anonymen. Mit dem sock.getOutputStream erzeugt man die Socket-Verbindung zum Client. Mit dieser Verbindung einen PrintWriter und senden an diesen, mit der Methode println, einen String mit dem Tipp. Mit writer.close wird der Socket geschlossen.
Der Tipp des Tages Client ist ein wenig kürzer:


Der Code erzeugt einen Socket und einen BufferedReader und liest die Sachen, die auf dem Port 4242 laufen. Zunächst wird eine Socket-Verbindung zu dem hergestellt, zu dem, was auf dem Port 4242 läuft auf dem localhost (also in dem Falle unseren Laptop). Der BufferedReader wird dann mit dem InputStreamReader verkettet, welcher dann wieder mit dem Eingabestrom vom Socket verkettet ist. In der Variable tipp wird dann die gelesene Zeile gespeichert (reader.readline()). Und diese Zeile wird dann zusammen mit „Tipp des Tages:“ ausgegeben.
und hier die Aufgabe die wir hierzu machen sollten:

So das mal so als Einstieg, nun kommen wir zu dem Anfang der Vorlesung und zur Lösung der Aufgabe.
Am Anfang der Vorlesung wurden wir gebeten unsere Resultate, falls wir schon welche hatten, da wir die Aufgabe ja schon machen konnten, zu zeigen und wer nicht, sollte die Aufgabe in dieser Zeit machen. Also habe ich mich erst einmal an die Aufgabe gesetzt. Nachdem ich eine Lösung hatte, wurde mir, während die anderen ihre vorzeigten klar, dass ich mir den Weg ein wenig zu einfach gemacht hatte, denn ich hatte nur beim Client, aber nicht beim Server eine Schleife gebaut, die zwar immer wieder einen anderen Tipp ausgelesen, aber dafür auch immer wieder einen neuen Port geöffnet hatte. Deshalb hatte ich dann meinen Bildschirm geteilt, was auch wieder problematisch war, aber auch dieses Problem konnte ich heute lösen. Her Noll gab mir den Tipp, dass es einfacher sei drei mal writer.println(tipp); ausgeben zu lassen, als eine Schleife zu bauen nur, kam dann leider bei der Ausgabe als Resultat drei mal Tipp des Tages mit einem Tipp und zwei Mal Tipp des Tages mit dem Wert null. Danach hatte ich auch einfach eine Schleife eingebaut, aber es änderte sich nichts am Resultat. Nachdem ich es nach einiger Zeit aufgegeben hatte, da ich ja auch von der Vorlesung nichts verpassen wollte, verschob ich die Aufgabe erst einmal nach hinten. Herr Noll gab mir auch die Möglichkeit ihm meinen Code zu senden, aber irgendwie wollte ich das alleine schaffen.
Der Übersicht zu liebe werde ich die Lösung, auch wenn ich sie erst in der Mitte bzw. Ende der Vorlesung gefunden hatte, werde ich hier gleich erklären und zeigen:
der Server-Code:

der Client-Code:

und hier die Ausgabe:

Was hat sich geändert?
Server und Client beinhalten jetzt eine Schleife. Beim Client liest diese Schleife 3 Mal den Tipp vom Server und gibt diesen dann anschließend aus. Beim Server wird drei Mal über prinln() eine Ratgeberbotschaft an einen PrintWriter. Damit man sich sicher sein kann, dass der writer „geleert“ wird, schreibt man noch writer.flush(). Warum die Schleife auf einmal ging, weiß ich immer noch nicht. Nach dem x-ten umbauen des Codes war ich dann einfach nur froh, dass es lief. Komisch war nur, dass ich den Code bereits vorher getestet hatte und es da nicht lief.
Zurück zur Vorlesung. Nachdem wir (ich ja noch nicht zeitlich gesehen) die Aufgabe machen und vorzeigen sollten war schon 14 Uhr, also ging es dann mit dem Stoff weiter. Die Methode accept(), die ich vorhin schon im Code gezeigt und genannt hatte, wartet auf eine Anfrage des Clients und erzeugt dann ein neues Socket-Objekt und liefert es als Ergebnis zurück. Aber hier eine kleine Übersicht über 3 Methoden, die wir besprochen hatten:
| public Socket accept() | wartet auf eine Anfrage des Clients und erzeugt dann ein neues Socket-Objekt und liefert es als Ergebnis zurück |
| public Socket (InetAddress address, int port) | erzeugt einen Socket und verbindet ihn mit der Anwendung, die auf dem Rechner mit der durch address festgelegten Adresse am Port läuft (diese Methode hatten wir oben auch benutzt, als wir den localhost 127.0.0.1 als IP nannten) |
| public Socket (String host, int port) | erzeugt auch einen Socket und verbindet ohn mit der Anwendung, die auf dem Rechner mit dem Host-Namen bzw. der IP-Adresse host am Port port läuft. Der Unterschied hier zum Oberen ist, dass, da es sich um einen String handelt, zuvor eine DNS (Domain Name System) Anfrage zur Bestimmung des Inet-Adress-Objektts durchgeführt wird |
Hier kurz ein Ausflug zum Domain Name System. Dieses hatten wir schon in der Vorlesung Kommunikationssysteme, aber eigentlich mag ich das Thema, also folgt jetzt ein kleiner Einblick in das DNS. Wir hatten in der Vorlesung immer gesagt, dass das DNS das Telefonbuch des Internets sei. Zu jedem namen gibt es eine Telefonnummer und umgekehrt. Außerdem ist es unbedingt notwendig, um Internet Dienstangebote erreichen zu können. Denn nur mit dem DNS kann man eine Webseite durch die Eingabe einer URL ansteuern. Damit Teilnehmer im Internet miteinander kommunizieren können, hat jeder Computer eine eindeutige IP-Adresse. Auf diese Zahlenkombination bildet das DNS dann die eingegebene URL ab. Also kurz gefasst: es verknüpft Rechnernamen, die es auch vergibt, mit der IP-Adresse. Zusätzlich kann über eine DNS-Abfrage auch ermittelt werden, welcher Rechner als E-Mail-Server für die Domain dient. Warum es das DNS gibt? IP-Adressen sind nicht gerade benutzerfreundlich, vor allem weil bald IPv6 mehr verbreitet sein wird und Menschen sich dann 8 Blöcke aus Hexadezimalzahlen merken müssten. Um also uns das Surfen zu erleichtern wurde 1983 das DNS entwickelt. Nicht zu vergessen ist, dass das natürlich auch anders herum geht, also man kann auch die DNS-Adressen wieder in IP-Adressen aufschlüsseln.
Danach sind wir noch kurz auf dieses Bildchen eingegangen:

Was man hier gut sehen kann, sind die bildlich dargestellten Ein- und Ausgabeströme. Was beim Client der Eingabestrom ist, ist bei dem verbundenen Server der Ausgabestrom.
Anschließend haben wir noch über den folgenden Code gesprochen:


Innerhalb der los()-Methode wird die GUI der Seite aufgebaut. Wenn der senden Button gedrückt wurde, startet der Listener. Die importierten Klassen java.swing und java.awt sind ebenfalls für das Uswr Interface zuständig. JFrame ist das Fenster und JPanel das innere Fenster. Der Button, wie auch das Textfeld werden zum JPanel hinzugefügt und dieses dann schlussendlich zum Frame.
Dann ging es weiter mit der Frage, wann man eine Nachricht vom Server erhält. An sich gibt es drei Möglichkeiten:
- Den Server alle 20 Sekunden abfragen
- Jedes Mal, wenn der Benutzer eine Nachricht verschickt, vom Server lesen
- Nachrichten lesen, sobald sie vom Server gesendet werden
Möglichkeit 1 würde viel Bandbreite kosten und es wäre nervig. Der Server müsste die Nachricht speichern, anstatt sie einfach zu verteilen und es würde zu Verzögerungen kommen. Möglichkeit zwei ist, wie bei einem Postfach und wenn der Nutzer nur liest und nichts sendet, dann hat er ein Problem. Möglichkeit drei ist die richtige Option, denn es ist benutzerfreundlich und effizient. Nur stellt sich hier die Frage, wie man zwei Dinge gleichzeitig macht. Die Antwort ist Multithreading. Also hat man so etwas wie eine Nebenhandlung und diese ist ein neuer, weiterer Thread. Diese Handlungsstränge laufen dann bei Java parallel. Das ganze geht relativ einfach:
Thread t = new Thread();
t.start();
Nun leider war’s das noch nicht, da der Thread hier ja rein gar nichts macht. Der Thread braucht noch eine Aufgabe. Ein Thread bedeutet auch immer ein separater Aufruf-Stack. Jede Java Anwendung startet einen main-Thread, welcher main() als unterste Methode auf den Stack setzt. Die JVM startet dann den main-Thread.

Hier zum Beispiel: Wenn buh() fertig ist, wird es vom Stack genommen und bah() wird ausgeführt. Wenn dies auch wieder fertig ausgeführt ist, kommt esvom Stack und pfui() ist dran. Das geht dann immer so weiter, bis main() dran kommt. Ist die main-Methode zu ende, endet auch das Java-Programm.
Die Klasse Thread hat Instanzvariablen, die Ausführungsstränge repräsentieren. Sie enthält Methoden, um einen Thread zu starten, um einen Thread mit einem anderen zu verbinden und um einen Thread schlafen zu legen.

Zu beachten ist, dass man nur den Eindruck hat, dass mehrere Dinge gleichzeitig geschehen, in Wirklichkeit aber, kann nur ein echtes Multiprozessorsystem mehr als eine Sache gleichzeitig tun. Die Ausführung wechselt also so schnell zwischen den Stacks hin und her, dass man davon gar nichts mitbekommt und das Gefühl hat, dass das gleichzeitig geschieht. Die JVM führt immer das aus, was als oberstes auf dem aktuellen Stack liegt (deshalb auch der Name Stack, der auf deutsch Stapel heißt, denn auch wie bei einem Stapel nimmt man immer das Oberste zuerst runter). 100 Millisekunden später könnte dann der ausgeführte Code zu einer anderen Methode auf einem anderen Stack wechseln. Also gehört es auch zu den Aufgaben des Threads zu verfolgen, welche Anweisung gegenwärtig auf seinem Stack ausgeführt wird.
Nun kommen wir dazu wie man einen neuen Thread zur Ausführung bringt:
- Ein Runnable-Objekt machen: Runnable threadJob = new MeinRunnable();
Runnable ist ein Interface, das die Aufgabe für den Thread ist. Also ein Job, um es einfach zu sagen. Man schreibt eine Klasse, die das Interface Runnable implementiert und in dieser Klasse legt man dann fest, welche Arbeit von dem Thread erledigt werden soll bzw. welche Methode von dem neuen Aufruf-Stack des Threads aus ausgeführt wird.
2. Ein Thread-Objekt machen und ihm ein Runnable geben: Thread meinThread = new Thread (threadJob);
Man übergibt dem Thread-Konstruktor das neue Runnable-Objekt. So weiß das Thread-Objekt welche Methode als unterstes auf den neuen Stack kommt und zwar die Methode run().
3. Den Thread starten: meinThread.start();
Es passiert erst etwas, wenn man die Methode start() des Threads aufruft. Hier ist der Punkt, an dem man von einer Thread-Instanz zu einem neuen Ausführungs-Thread übergeht. Wenn der neue Thread startet, nimmt er die run()-Methode des Runnable-Objekts und setzt sie zuunterst auf seinen Stack.
Um das bildlich darzustellen:

Das Interface Runnable definiert nur eine einzige Methode und zwar: public void run(). Die erste Methode, die auf dem Stack des neuen Threads landet, ist immer die Methode run(). Diese wird immer ausgeführt, wenn er gestartet wird. Das muss immer so aussehen:
public void run() {
//Code, der vom neuen Thread ausgeführt wird
}
Der Job eines Threads kann in jeder Klasse definiert werden, die auch Runnable implementiert.
Danach hatten wir einen Code angeschaut, der in den Folien gegeben war, jedoch werde ich erst später darauf eingehen, da wir dazu noch eine Aufgabe machen sollten, die vom Kontext her erst ein wenig später hinein passt.
Anschließend ging es weiter mit den Zuständen eines Threads. Verwirrender weise, steht auf der Folie des Skripts, dass es drei Zustände bei einem Thread gibt, jedoch gibt es mehrere und hier sind nur drei genannt.

Wenn ein Thread läuft, dann auf der CPU, die einen Befehl nach dem anderen ausführt. Das Problem hier: eine CPU und mehrere Threads, die quasi gleichzeitig laufen, was ja aber nicht geht, da eine CPU nur einen Thread bzw. einen Prozess abarbeiten kann. Deshalb erhalten die Prozesse sogenannte Zeitscheiben, damit der Prozessor schnell zwischen ihnen wechseln kann. Haben manche Prozesse eine höhere Priorisierung, dann kommen sie öfter an der Reihe, als andere (das nennt man Priority-Scheduling). Hat man aber mehrere Kerne, können auch mehrer Threads, oder Prozesse, gleichzeitig laufen.
Exkurs Prozessor bzw. CPU:
- Der Prozessor, auch CPU (Central Processing Unit) genannt, ist das Herzstück jeder Hardware, ohne ihn funktioniert nichts.
- Die CPU verarbeitet Prozesse in der binären Maschinensprache
- Neben den Arbeitsbefehlen reagiert der Prozessor auch auf unvorhergesehene Ergebnisse. Diese werden mittels Interrupts (Unterbrechungen) an die CPU gesandt. Der Prozessor unterbricht dann seine momentane Arbeit, speichert sich die Werte und bearbeitet zuerst das unvorhergesehene Ereignis. Danach widmet er sich wieder seinem ursprünglichen Befehl
so funktioniert ein Prozessor:
- ein Prozessor arbeitet in 4 Zügen: Fetch, Decode, Fetch Operands und Execute
- Fetch: Zuerst wird aus dem Befehlszeilenregister im Arbeitsspeicher die Adresse des nächsten Befehls gelesen und in den Befehlsspeicher geladen.
- Decode: Der Befehlsdecoder entschlüsselt den Befehl und aktiviert alle Schaltungen, welche für die Ausführung des Befehls notwendig sind.
- Fetch Operands: Nun werden alle Werte und Parameter des Befehls in die Register geladen, die verändert werden sollen. Der Prozessor findet diese Werte auf der Festplatte, im Cache oder im Arbeitsspeicher.
- Execute: Der Prozess wird ausgeführt. Das kann beispielsweise die Ansteuerung eines Peripheriegerätes wie eines Druckers oder eine Operation im Rechenwerk sein.
Danach kann dann der Zyklus wieder von vorne beginnen.
Zurück zu den Threads. Ein Thread pendelt meist zwischen den Zuständen laufend und lauffähig. Es ist jedoch auch möglich einen Thread zu blockieren. Das könnte man wollen, wenn der Thread z.B. einen Code ausführt, der aus einem Socket-Eingabestrom liest, aber es sind gerade keine Daten zum Lesen da. Entweder kann der Scheduler den Thread aus dem Zustand laufend herausnehmen, bis Lesestoff da ist, oder man benutzt sleep(), oder der Thread wartet, weil er versucht eine Methode auf einem Objekt aufzurufen, das gesperrt ist. Dann wird ein Thread in den Zustand „vorübergehend nicht lauffähig“ gesetzt. Wenn er „schlafen gelegt“ wird, dann wartet er z.B., dass ein anderer Thread fertig wird, so dass Daten aus einem Strom verfügbar werden.
Hier ein Bild vom Thread Lebenszyklus (beschriftet):

Zu den einzelnen Schritten hatte ich noch die Abfolge hinzugefügt. Wie man hier sehen kann, gibt es also mehr als nur 3 Zustände eines Threads.
Anschließend wurde noch eine Reihe von Methoden vorgestellt, über die wir aber relativ schnell drüber gegangen sind.
Die mir am wichtigsten erscheinenden und die, die ich wohl noch brauchen könnte sind die folgenden:




Die oben genannte Priority-Methode hängt, meines Wissens nach, mit der Einteilung der Zeitscheiben zusammen. Also kommen die Threads häufiger an die Reihe bei der CPU, die eine höhere Priorität haben.
Ein Dämon Thread ist ein Thread mit geringer Priorität, zum Beispiel, garbage collection.
Zurück zum Programm 🙂 Eines der letzten Themen heute war der Thread- Scheduler. Dieser gibt an welcher Thread wann und wie lange läuft und wann er nicht mehr laufend ist. Man kann den Scheduler nicht steuern und ganz wichtig: es gibt keine Planung und keine Garantien. Ich muss schon sagen, dass mich das wirklich gewundert hatte und das war etwas, was ich wirklich nicht wusste und auch noch in keiner anderen Vorlesung gehört hatte. Vor allem, ist nicht einmal der Algorithmus hierzu veröffentlicht. Man kann jedoch, und das hatten wir heute auch praktisch angewendet und gleich komme ich auch auf die Aufgabe zurück, die wir heute machen sollten, den Scheduler beeinflussen durch die sleep()-Methode. Meist lässt der Scheduler jedoch die Threads in der Reihe ablaufen, aber wie gesagt gibt es keine Garantie hierfür.
und eeeeeendlich zur Aufgabe:
Diese war den Code, der auf Folie 80 zu finden ist, abzuschreiben und dann zu schauen, welche Methode zuerst abläuft. Das konnte man daran erkennen, welche Ausgabe zuerst kam, also „oben auf dem Stack“, oder „zurück in main“. Dann sollten wir, um den Thread-Scheduler zu täuschen, die Methode, die erster aufgerufen wurde, schlafen legen. Hier ist meine Lösung:

Ausgabe:

Also lässt mein Thread Scheduler erst den unteren und dann den oberen Thread laufen. Deshalb hatte ich den unteren sleep eingebaut und dann passiert folgendes:

wie schon zu erwarten kommt folgende Ausgabe:

Ein Thema, das noch zwischen die Aufgabe geschoben wurde, war die Deadlock Situation. Hier hatten wir das Beispiel von 5 Philosophen, die zusammen am tisch sitzen und essen möchten. Jeder braucht zwei Gabeln und muss erst die Rechte und dann die Linke zur Hand nehmen. Nun ist das Problem, dass wenn sie die linke Gabel nehmen wollen, diese bereits weg ist. Nun sind sie blockiert. Das sollte eine Assoziation zu den Threads sein. Wenn Threads sich gegenseitig blockieren, dann kommt der Task manager ins Spiel.

Das wars dann auch schon mit dieser Vorlesung! Bis zum nächsten Mal 🙂