www.r-krell.de
Webangebot für Schule und Unterricht, Software, Fotovoltaik und mehr

Willkommen/Übersicht  >  Informatik  >   Informatik-Seite i)

Informatik mit Java

Teil i): Netzwerke, Server und Client, Threads

Die Überschriften a) bis h) verweisen auf die vorhergehenden Seiten:
a)  Grundlegendes zu Java, benötigte Software (inkl. Downloadadressen) und Installation
b)  Erste Java-Programme (u.a. Autorennen u. Aufzug) sowie Verweise (Links) auf fremde Java-Seiten
c)  Sortieren und Suchen in Java sowie grafische Oberflächen mit der Java-AWT
d)  Adressbuch- und Fuhrpark-Verwaltung sowie Datei-Operationen mit Java
e)  Lineare abstrakte Datentypen Keller, Schlange, Liste und Sortierte Liste (sowie Tiefen- u. Breitensuche)
f) Abstrakter Datentyp Baum (binäre Sortierbäume, Rechenbaum und Spielbaum)
g) Abstrakter Datentyp Graph: Typische Graphenprobleme, Wegsuche, Breiten- und Tiefensuche.
h) Bau eines Compilers Java -> 1_AMOR (Syntaxdiagramm, Scanner, Parser, Variablentabelle, Codeerzeuger)

Und auf dieser Seite erwartet Sie:

i) Informatik mit Java, Teil i): Netzwerke, Server und Client, Threads

Eine vollständige Übersicht aller Java- (und weiterer) Seiten gibt's auf der Informatik-Hauptseite!


zum Seitenanfang / zum Seitenende

Netzwerke im Informatikunterricht



Im Folgenden sollen einige Grundbegriffe von Computernetzen kurz angerissen werden. Für ausführliche(re) Erklärungen wird auf fremde Quellen verwiesen; die Darstellung im Unterricht muss natürlich deutlich über das hier nur grob skizzierte Gerippe hinausgehen.



Kategorisierungen

Zum Einstieg in die Netzwerktechnik können Computernetze nach verschiedenen Kriterien unterschieden und benannt werden. Ähnlich wie man beispielsweise Autos nach Größe etwa in Kleinwagen, Mittelklassewagen, Vans, Lastwagen usw. oder nach Farbe in schwarze, rote, silberfarbene,.. Autos oder nach Antrieb nach Elektro-, Hybrid-, Benzin- oder Dieselautos oder nach Anzahl der Sitzplätze, nach Anzahl der Türen, nach Anzahl der Achsen usw. einteilen kann, werden Rechnernetze ebenfalls gerne nach verschiedenen Eigenschaften eingeteilt. Dabei sind heute keineswegs mehr alle Sorten von Netzen gebräuchlich, die man noch in Büchern findet. Bevor das Internet in den 1990er Jahren weltumspannend eingeführt wurde, gab es schon kleinere Vorläufernetze in einigen Städten, die man als MAN (metropolitan area networks) bezeichnet hat. Auch bei der Verbindungstechnik wurde u.a. mit Koax-Kabeln und anderen experimentiert, bevor sich die heute üblichen Ethernet-/Patch-Kabel-Verbindungen allgemein durchgesetzt haben. Deswegen hier nur einige mögliche Unterteilungen ohne Anspruch auf Vollständigkeit:

Natürlich können noch beliebige weitere Merkmale (etwas die Geschwindigkeit der Datenübertragung usw.) zur Netzeinteilung herangezogen werden. So wie ein bestimmtes Auto gleichzeitig ein Kleinwagen, ein schwarzes Auto, ein Auto mit Benzinmotor bzw. ein Viersitzer sein kann, gehören auch Rechnernetze gleichzeitig mehreren Kategorien an. Es ist sinnvoll herauszufinden, in welche Kategorien etwa das Internet, ein Firmen- bzw. das Schulnetzwerk oder das typische LAN-Party- oder Heimnetzwerk fallen! Mitunter ändern sich auch die Eigenschaften der Netze mehrfach auf dem Weg z.B. von einem Rechner im Heimnetzwerk, der per Funk mit dem Access Point bzw. WLAN-Router kommuniziert, während der WLAN-Router per Kupferleitung mit dem Kabelmodem verbunden ist, das dann etwa über die Antennenleitung im Haus, den Hausanschluss im Keller, den Straßenverteiler und weitere Zwischenpunkte des Fernsehkabels zum Provider und so letztlich ins bzw. von dort ins Internet geht. Netzwerkeigenschaften ändern sich auch, wenn man etwa nach einem Online-Einkauf auf http-Seiten beim Gang zur Kasse auf https-Seiten des gleichen Online-Shops weiter geleitet wird...




zum Seitenanfang / zum Seitenende

Schichtenmodelle

Informations.- bzw. Datentransport wurde nicht erst mit dem Internet eingeführt. Seit über hundert Jahren werden z.B. Briefe per Post befördert. Während der Briefträger eigentlich nur mit bestimmten Mustern gefärbtes Papier in Umschlägen transportiert (und nur in manchen Fällen, etwa bei Einschreiben mit Rückschein, dem Absender eine Empfangsquittung zukommen lässt), erkennen des Lesens kundige Empfänger aus der Farbverteilung auf dem Papier Buchstaben und -- bei Beherrschung der Sprache, in der die Nachricht verfasst wurde -- Wörter und Sätze und daraus Bedeutungen, d.h. erschließen den Sinn bzw. den Inhalt der Nachricht, der möglicherweise z.T. sogar "zwischen den Zeilen" gelesen werden muss.

Auch beim Datenaustausch zwischen Computern gibt es die unterschiedlichen Ebenen bzw. Schichten: Auf der untersten Schicht werden (so wie der Briefträger Papier- und Farbmoleküle transportiert) letztlich nur Bits in Form von Strom- oder Lichtimpulsen transportiert. Die Zusammenfassung z.B. von acht Bits zu einem Byte, die Unterscheidung der Informationsbytes von Steuerzeichen, das Kombinieren von Bytes zu Informationspaketen mit einem Header und der Nutzlast (IP-Pakete [packets] und darin TCP-Pakete [frames]) und schließlich das Interpretieren der Nutzlast-Bytes als Bilddaten oder als Zeichen etwa eines HTML-Quelltextes bis hin zur Anzeige des korrekt formatierten Textes und der eingebetteten Bilder im Browserfenster stellen verschiedene Schichten dar. In der obersten Schicht wird der Inhalt der übertragenen Nachricht von der richtigen Anwendung angezeigt. Je nachdem, wie fein man die Unterteilung (insbesondere in den Anwendungsprogrammen) treiben will, wird das ISO/OSI-7-Schichten-Modell oder das einfachere Vier-Schichten-Internetmodell bevorzugt.

Bilder der Schichtenmodelle und weitere Erklärungen findet man u.a.

bzw. dort angegebenen weiterführenden Links. (ISO = International Standardization Organisation, OSI = Open System Interconnection, IP = Internet Protocol, TCP = Transmission Control Protocol)

Das Schichtenmodell soll deutlich machen, dass Datentransport eine komplexe (eben vielschichtige) Aufgabe darstellt, die auf den beteiligten Ebenen verschiedenes Vorgehen erfordert.


zum Seitenanfang / zum Seitenende

Typische Geräte in Rechnernetzen, Routing

In LANs und im Internet werden die Daten paketweise transportiert. Jedes Datenpaket muss im so genannten Header u.a. die Adressen des Absenders und des Empfängers enthalten. Auf dem Weg vom Absender zum Empfänger werden viele Geräte passiert, die die Pakete geeignet weiter leiten müssen. Eine Nachricht besteht normalerweise aus mehreren Paketen, wobei die einzelnen Pakete -- also die verschiedenen Teillieferungen -- durchaus unterschiedliche Wege gehen können und erst beim Empfänger wieder richtig zusammengesetzt werden. Neben den Computern (ob als Client, als Server oder als weiterleitender Netzknoten) sind typische Geräte im Netz:

Im Alltag bzw. in Verkaufsprospekten werden die Bezeichnungen nicht immer korrekt angewendet. Andererseits wurden hier spezielle Eigenschaften wie Broadcasten, Puffern usw., die oft noch zur Unterscheidung heran gezogen werden, nicht berücksichtigt. Mehr Details und ausführlichere, nicht immer ganz übereinstimmende Beschreibungen lassen sich u.a. über folgende Leitseiten zu den (aktiven) Netzwerkkomponenten finden:

Auf jeden Fall lustig anzusehen und lohnenswert, wenn auch nicht immer ganz stimmig, ist das ältere, auch mit deutschem Ton erhältliche rund 13-minütige Video "(The Good) Warriors of the Net" (das es inzwischen auch bei YouTube gibt): Hier kann man den Weg der Datenpakete durchs LAN und im Internet und vorbei an vielen Netzwerkkomponenten verfolgen.


zum Seitenanfang / zum Seitenende

IP-Adressen

Jeder Rechner, jeder Router, jedes netzwerk- oder gar interentfähige Gerät muss innerhalb des Netzes, in dem er/es sichtbar ist, eine eindeutige Adresse haben. In LANs und bisher im Internet (und z.Z. weiterhin im amerkanischen und europäischen Teil des Internets) werden dazu 32-stellige Dualzahlen verwendet. Da es fast 4,3 Milliarden verschiedene 32-Bit-Dualzahlen gibt, könnten im größten Netz -- dem Internet -- damit 4,3 Milliarden verschiedene Geräte eindeutig gekennzeichnet werden. Weil allerdings viele Adress-Bereiche für Sonderaufgaben gesperrt sind, bleiben weniger Adressen verfügbar. Und weil diese zunächst großzügig in USA und Europa verteilt wurden, reichen die für Asien reservierten Adressen in diesem schnell wachsenden Raum nicht mehr aus. Deshalb werden dort und demnächst wohl überall 128-Bit-Adressen verwendet (IP v6). Das sind fast 1029 mal mehr Adressen als bisher: Pro Erdenbewohner können damit dann rund 4,2 x 1028 Geräte adressiert werden, was nicht nur für die nächste Zeit genug sein sollte. Obwohl seit Juni 2012 angeblich nur noch IPv6-Adressen neu vergeben werden [IPv6-LaunchDay 6.6.2012], soll im Folgenden noch die 32-Bit-Adressierung skizziert werden (IP v4). Da vielstellige Dualzahlen schwer zu merken sind, werden die Dualzahlen byteweise in vier Dezimalzahlen (jeweils zwischen 0 und 255) übersetzt und mit Trennpunkten angegeben, oder -- noch besser -- im Domainssytem mit URLs aus Textteilen angegeben (URL = uniform resource locater). So ist z.B. die

Webadresse 11010001010101011000011101101010 = 11010001.01010101.10000111.01101010 = 209.85.135.106 = www.google.de

und es gelingt hier, auch mit den vier durch Punkte getrennten Dezimalzahlen die Webseite des Suchmaschinenbetreibers aufzurufen (wie man durch Anklicken des vorstehenden Links feststellt, wobei aber offenbar eine etwas andere Seite als bei der Text-URL angezeigt wird!). Die Vier-Zahlen-Version einer URL kann man übrigens selbst leicht mit dem Befehl ping in der Dos-Eingabeaufforderung ermitteln, hier also

Bild: Eingabeaufforderung mit ping für Google

Zunächst überraschend gelingt zwar bei vielen, aber nicht bei allen Webseiten der Aufruf über die Vier-Zahlen-Adresse; bei meiner Webseite etwa wird die Zahladresse 81.169.145.75 von keinem Browser gefunden (dass die Webseite trotzdem erreichbar ist, merken Sie ja gerade):

Bildschirmansicht: ping und 3 Browserfenster für 81.169.145.75 (www.r-krell.de)
Der Grund für die Nicht-Auffindbarkeit per IP-Adresse liegt offenbar darin, dass ich mein Webangebot nicht auf einem eigenen Server im Internet zur Verfügung stelle, sondern -- wie viele andere auch -- die Dienste eines Webhosters in Anspruch nehme. Und diese Firma hält mehrere Kundendomains auf dem gleichen Server bereit: Alle gemeinsam dort abrufbaren Domains haben damit die gleiche IP-Adresse, nämlich die des Server-Computers ('Host'). Insofern kann man zwar zu meinem Domainnamen r-krell.de eindeutig eine passende IP-Adresse finden; umgekehrt kann aber der IP-Adresse 81.169.145.75 nicht eindeutig meiner Domain zugeordnet werden, weil ich mir den Server und die IP-Adresse mit mehreren anderen Domains teile. Ein Hamburger Leser wies mich dankenswerterweise auf die Seite http://DedicatedOrNot.com hin: Gibt man dort nach der Aufforderung "Dedicated Hosting or Not? Check it out:" in das Eingabefeld entweder den Domainnamen (www.)r-krell.de oder die IP-Adresse 81.169.145.75 ein, so wird man darüber informiert, dass meine Domain mit den weiteren genannten Internetauftritten auf einem Host namens w0b.rzone.de der Firma Strato AG liegt. Soweit zu den IP-Adressen von Webseiten.

Wenn Sie hingegen wissen wollen, mit welcher IP-Adresse Sie selbst gerade im Internet surfen, so finden Sie die Anzeige der eigenen IP-Adresse bzw. die Anzeige der Ihrem Rechner, Ihrem (Heim-)Netzwerk bzw. Ihrem Router (i.a. von Ihrem Internet-Provider für die Dauer einer Internet-Sitzung oder eines Tages) zugeteilten IP-Adresse z.B. auf http://www.wieistmeineip.de/ oder auf http://www.ip-adresse-ermitteln.de/ (siehe nachfolgender Abschnitt).

Mehr über IPv6 findet sich u.a. auf http://www.tagesschau.de/ausland/ipvday102.html, bei http://de.wikipedia.org/wiki/IPv6, auf http://www.zeit.de/digital/internet/2012-06/ipv6-launch-day oder -- als Test, ob der eigene Provider, Router und die Netzwerkkarte mit IPv6-Adressen zurecht kommen -- unter http://ipv6test.google.com.

Da üblicherweise im Browser die URL bzw. der Domännamen des Ziels in Textform eingetippt wird (also etwa als "www.google.de"), muss der Internet-Browser irgendwoher die zugehörige zahlenmäßige IP-Adresse beziehen (hier z.B. 209.85.135.106), denn diese Zahl-Adresse wird als Zieladresse für die Header aller abgesendeten Pakete benötigt. Dazu fragt -- automatisch und vom Benutzer unbemerkt im Hintergrund -- der Browser bei einem Domain-Name-Server nach, der Tabellen mit Paaren von Text- und Zahlen-URLs vorrätig hält. Tatsächlich werden häufig benötigte URLs im eigenen Rechner oder im eigenen Router gespeichert; seltene bzw. neue Abfragen müssen ins Internet weiter geleitet werden, bis beim Provider oder noch weiter entfernt ein Domain-Name-Server (DNS) mit der passenden Übersetzung gefunden wird. Oft kann der bevorzugt abzufragende Domain-Name-Server im Browser eingestellt werden; so schlägt Google vor, ihren DNS voreinzustellen, weil er schnelle Ergebnisse liefere -- und der Suchmaschinenbetreiber nebenbei erfährt, welche Webseiten aufgerufen werden, ohne dass vorher ausdrücklich danach gesucht wurde.

Auf einer Webseite der Fa. Heise werden Anfragen, Übersetzungen und Auskünfte zu DNS, IP-Adressen (IPv4 und v6!) und Text-URLs genannt und man kann Zusatzinformationen finden. Einfach mal ausprobieren unter http://www.heise.de/netze/tools/dns/. Den Weg, den Daten im Internet nehmen, kann man hingegen auf http://www.heise.de/netze/tools/traceroute verfolgen.


zum Seitenanfang / zum Seitenende

Subnetze und Teilnetzmasken

Grundsätzlich können alle in einem Netz angemeldeten Computer den gesamten Netzverkehr empfangen und ihrerseits Daten ins Netz schicken. Schon in größeren Firmennetzen ergeben sich dabei Probleme im Ethernet, weil dort innerhalb des Netzes zu jedem Zeitpunkt immer nur ein Computer senden kann, während alle übrigen Computer die Datenpakete empfangen. Wenn zwei Computer gleichzeitig senden, vermischen sich im Netz die Datenpakete und die Information wird zerstört. Deshalb wird der Netzzugang in Ethernet-LANs (anders als etwa in Token-Ringen) durch das CSMA/CD-Verfahren geregelt (Carrier-Sense Multiple Access / Collision Detection, also etwa "am Kabel hörendes Vielfach-Zugriffsverfahren mit Kollisions-Erkennung"): jeder sendebereite Rechner prüft erst, ob gerade Datenverkehr im Netz herrscht. Nur wenn Ruhe eingetreten ist, darf er anfangen, ein Datenpaket zu senden. Dabei muss er trotzdem auf Empfang bleiben und prüfen, ob nur seine eigenen Daten im Netz ankommen oder ob die Sendung etwa durch das zufällig gleichzeitig angefangene Senden eines anderen Computers überlagert und gestört wird. Im letzen Fall, also bei einer Kollision, brechen beide Computer ihren misslungenen Sendeversuch ab und versuchen es erst nach jeweils einer zufällig gewählten kurzen Pause erneut. In der Praxis bewährt sich das Verfahren bei nicht allzu hoher Netzauslastung, weil die Pausen meist verschieden gewählt werden und dann ein Computer ungestört zu Senden anfängt, während die anderen Rechner während ihrer Pause merken, dass wieder Netzverkehr herrscht und mit dem erneuten eigenen Sendeversuch warten (müssen). Jeder Computer darf immer nur ein Datenpaket auf einmal senden und muss dann wieder kurz warten, damit zwischendurch auch andere Computer eine Chance haben, ein Stück ihrer Informationen abzugeben. Bei einer großen Zahl beteiligter Rechner mit stärkerem Datenverkehr können allerdings so viele Kollisionen auftreten, dass kaum noch Daten durch das Netz gehen. Deshalb unterteilt man das Netz (z.B. durch die oben genannten Switches, Bridges oder Router) in getrennte Kollisionsdomänen, in denen jeweils nur wenige(r) Rechner um den Datenverkehr in der Domäne konkurrieren. Sollen die Rechner einer Domäne auch gemeinsam angesprochen werden können (etwa, wenn ein weiterer Netzteilnehmer angemeldet wird und automatisch per DHCP die nächste freie IP-Adresse zugewiesen bekommt, die dann allen Netzteilnehmern bekannt gemacht wird -- DHCP = Dynamic Host Configuration Protocol), muss ein Extra-Teilnetz eingerichtet werden. Alle Rechner eines Teilnetzes haben bei der 32-stelligen Dualzahl ihrer IP-Adresse gleiche vordere (höherwertige) Bits und unterscheiden sich nur in den letzten (niederwertigsten) Bits. Je nachdem, wie viele Rechner gemeinsam in ein Teilnetz sollen, müssen für den individuellen Adressteil (den so genannten Host-Teil) ausreichend viele Bits reserviert werden. Soll ein Teilnetz beispielsweise 20 Rechner enthalten, so lassen sich 20 unterschiedliche Dualzahlen nur mit mindestens 5 Bit darstellen (die größte 4-Bit-Zahl ist mit 15 zu klein; die größte 5-Bit-Zahl 31 ließe sogar noch Platz für einige weitere Rechner). Bei insgesamt 32 Bit langen Adressen entfallen also die letzten 5 Bit auf den Host-Teil und es bleiben die vorderen 27 Bit für die gemeinsame Netzadresse. Um deutlich zu machen, wie lang der gemeinsame Netzteil ist und wo die individuellen Rechnernummern anfangen, wird eine so genannte Subnetzmaske verwendet, die im Beispiel einfach aus 27 Einsen (für den Netzwerkteil) und 5 Nullen (für den Host-Teil) besteht (Deshalb spricht man hier auch von einem /27-Teilnetz, obwohl ich die Bezeichnung 27/-Netz logischer fände). Alle Subnetzmasken bestehen immer anfangs nur aus Einsen, gefolgt von Nullen. Einsen nach Nullen sind nicht erlaubt. Auch die Subnetzmasken werden üblicherweise byteweise durch Dezimalzahlen angegeben, die durch Punkte getrennt sind. Im Beispiel wäre die

/27-Subnetzmaske 11111111111111111111111111100000 = 11111111.11111111.11111111.11100000 = 255.255.255.244

In ein durch diese Subnetzmaske charakterisiertes (/27-)Teilnetz können übrigens höchstens 30 Rechner eingebunden werden: die niedrigste Adresse (letzte fünf Bits sind Nullen) wird nicht an einen Rechner vergeben, sondern gilt als Netzadresse. Die höchste Adresse (letzte 5 Bits sind Einsen) gilt ebenfalls nicht für einen speziellen Rechner, sondern ist die Broadcast-Adresse für Nachrichten, die alle Rechner im Netz empfangen sollen und ist sozusagen eine gemeinsame Zusatzadresse aller Hosts. Aber die verbleibenden 30 Möglichkeiten reichen für die 20 Rechner unseres Beispielnetzes ja aus.

Bildschirmansicht: ipconfig für eigene IP-Adresse im LANAls Teilnehmer in einem Windows-Netzwerk können Sie sich übrigens durch Eingabe des Befehls ipconfig (oder ausführlicher noch mit ipconfig /all) überzeugen, dass im normalen Heimnetzwerk bis zu 254 Rechner oder andere Geräte mit eigener IP-Adresse (wie etwa Router, Netzwerkdrucker usw.) angemeldet werden könnten, weil die Subnetzmaske dort standardmäßig auf 255.255.255.0 gesetzt ist, d.h. die ersten 24 Bits für den Netzwerkteil (=/24-Netz) und damit die letzten 8 Bits für Hosts zur Verfügung stehen -- abzüglich Netz- und Broadcastadresse bleiben also 254 Host-Adressen! Als gemeinsame Netzadresse wird im gezeigten Beispiel 192.168.1. verwendet; die niedrigste Hostadresse mit der Nummer 1 reserviert sich der Router (=Standardgateway), der hier auch die übrigen privaten Adressen per DHCP vergibt, während der Rechner, auf dem ich den Befehl ausgeführt habe, gerade als Rechner-Nummer 100 im Heimnetz bezeichnet wird. Dazu müssen nicht weitere 99 Rechner im Netz sein; die DHCP-Nummern werden nicht unbedingt aufsteigend vergeben! Die gezeigte Netzadresse ist übrigens nicht internet-fähig: alle IP-Adressen, die mit 192.168. beginnen, sind für private Netze reserviert (ebenso übrigens wie alle Netzadressen, die mit 10. beginnen oder die mit einer der Zahlen zwischen 172.16. oder 172.31. anfangen. Der Adressbereich, der mit 127. anfängt, verweist hingegen immer auf den eigenen Rechner. Warum es dort über 16000 verschiedene Host-Adressen geben muss, bleibt allerdings fraglich). Alle Rechner eines solchen privaten Netzes gehen nicht mit der eigenen privaten Adresse ins Internet, sondern mit der äußeren, öffentlichen Adresse des Routers! Welche Adresse das ist, kann intern nicht ermittelt werden (weil diese Adresse ja nicht im Heimnetzwerk sichtbar ist, sondern die Adresse des Routers zum Internet hin ist). Allerdings gibt es -- wie im vorigen Abschnitt schon erwähnt -- Internetseiten, die Besuchern die IP-Adresse anzeigen, mit der sie im Internet erscheinen. Solche Dienste bieten etwa http://www.wieistmeineip.de/ oder auchhttp://www.ip-adresse-ermitteln.de/ .


Im Internet findet man natürlich auch zahlreiche Seiten, die das Subnetz-Thema vertiefen und häufig auch Subnetzrechner anbieten, die bei Eingabe einer Anzahl von gewünschten Clients die Subnetzmaske errechnen. Einige Beispiele sind:

Nachdem bisher auf dieser ganzen Seite if-java-i.htm noch keine einzige selbstgeschriebene Codezeile aufgetaucht ist, juckt es sicher jede Programmiererin und jeden Programmierer in den Fingern, jetzt endlich selbst ein Java-Programm oder -Applet zur Verwandlung von 32-Bit-Dualzahlen in die Vier-punktgetrennte-Dezimalzahlen-Schreibweise und zurück und/oder zur Berechnung von Subnetzmasken zu schreiben. Diesen Drang möchte ich nicht bremsen: Speichern Sie diese Seite unter Ihren Lesezeichen/Favoriten, schreiben Sie das Programm und kehren Sie in einigen Stunden oder Tagen hierher zurück, um die folgenden Kapitel zu lesen und noch mehr zu programmieren!


zum Seitenanfang / zum Seitenende

Netzwerkfähige Java-Programme: Server und Client, 1. Versuch



Nach so viel Theorie sollen im Folgenden ein Server und ein Client in Java geschrieben werden. Tatsächlich muss sich die Java-Programmiererin bzw. der Java-Programmierer gar nicht selbst um die niederen Schichten des ISO/OSI-Modells kümmern, weil in der mitgelieferten Java-Bibliothek java.net bereits Klassen und Methoden enthalten sind, die diese Aufgabe übernehmen. Die Programmierung muss also nur die Anwendungsschicht berücksichtigen.

Server und Client sind dabei nicht gleichberechtigte oder gleichartige Programme: Der Server bietet Dienste an, muss dazu ständig laufen und wartet darauf, dass ein Client gestartet wird, die Verbindung aufnimmt und ihm Dienste abfordert. Der Server kann von sich aus keine Verbindung zum Client aufnehmen, wohl aber nach Verbindungsaufnahme vom Client aus beliebig viele Nachrichten an den Client senden, bis dieser die Verbindung abbricht. Umgekehrt kann der Client während einer Verbindung natürlich auch Nachrichten an den Server schicken. Oft erfolgt der Nachrichtenaustausch im Wechsel, was aber keineswegs so sein muss: Beide Programme können grundsätzlich unabhängig von einer Nachricht der Gegenstelle ihrerseits beliebig Nachrichten abschicken.

Nachstehend wird zunächst der kommentierte Quelltext des Servers vorgestellt. Die Objekte der Klassen ServerSocket oder Socket kann man sich als Steckdosen oder Anschlussbuchsen zum Netz vorstellen. Gekapselt vor dem Programmierer erledigt Java mit speziellen Methoden wie accept das Warten oder erlaubt mit über die Steckdosen gestülpten Datenströmen wie einem PrintWriter und dessen Methode println das Senden oder mit einem BufferedReader und dessen Methode readLine das Empfangen von (einzeiligen Text-)Nachrichten.


zum Seitenanfang / zum Seitenende

Der Server

Die typischen Aktionen eines Servers werden hier auf mehrere Methoden verteilt. Die zuerst geschrieben Methode warteAufClientanfragen muss eigentlich genau so in jedem Server stehen; die nächste Methode kommuniziereMitEinemClient enthält bereits ein paar Besonderheiten, die hier vereinbart wurden: etwa, dass nach 10 empfangenen Nachrichten die Verbindung automatisch getrennt wird, dass bei einem Dollarzeichen $ in einer vom Client empfangenen Nachricht die Verbindung mit diesem Client beendet wird und dass bei zwei Dollarzeichen $$ in einer Clientanfrage der Server sogar ganz abgeschaltet wird. In der letzten Methode reaktionAuf steht dann, wie der Server auf eine Clientanfrage reagiert. Hier öffnet sich ein weites Feld: Wer beispielsweise einen Server für eine automatische Fahrplanauskunft aufbauen will, muss in dieser Methode aus dem Text der Clientanfrage Start, Ziel und die gewünschte Reisezeit isolieren, um dann -- am besten aus einer Datenbank (vgl. meine Seiten zu Datenbanken!) -- geeignete Verkehrsmittel und Verbindungen auszuwählen und die Informationen als Funktionswert der Methode reaktionAuf an den Client zurück zu senden. Bei Onlinespielen muss hingegen auf den vom Client gesendeten Zug der Spielstein passend gesetzt, das Spielfeld aktualisiert (und als Funktionswert an den Client geantwortet) und eventuell ein (automatischer) Mit- oder Gegenspieler aktiviert werden.

Der vorgestellte Server kann übrigens nacheinander mit mehreren Clients kommunizieren, aber zu jedem Zeitpunkt immer nur mit einem. Das Programm wurde aber so konzipert, dass später leicht eine Erweiterung auf die quasi-gleichzeitige Bedienung mehrerer Clients möglich ist.

          // Netzwerkprogrammierung: Server -- Krell, 3.9.2010
     // Server kann nacheinander mit bis zu 5 Clients je mehrere (hier max. 10) Nachrichten wechseln
     // (schaltet einen Client nach 10 Nachrichten oder nach Ende-Nachricht des Clients ab)
     // -- noch ohne Threads und damit ohne die Möglichkeit, gleichzeitig mehrere Clients zu bedienen --
     // Geeigneter Client: N10C_ClientOberflaeche(Start)
     
     
import java.io.*;
     
import java.net.*;
     
     
public class N10S_Server
     {
       
public void warteAufClientanfragen()  // der Server wartet auf und stellt
     
    // Verbindungen mit bis zu 5 Clients nacheinander her -- erst nach Ende der
     
    // Kommunikation mit einem/dem aktuellen Client wird ein nächster
     
    // Client akzeptiert! (= noch ohne Multi_Threading)
     
  {
         
try
     
    {
           
// 1. den einen Server-Socket anlegen:
     
      int portNr = 5001// oder eine andere willkürliche Portnummer möglichst > 1023
     
      ServerSocket serv = new ServerSocket(portNr);
     
           
// 2. auf nächsten Client warten und mit diesem kommunizieren:
     
      int verbindungsNr = 0;
           
while (verbindungsNr < 5// hier willkürlich festgelegte Höchstzahl an Verbindungen
     
      {
             
// 2a. Warte auf nächsten Client und stelle Verbindung her:
     
        System.out.println("Server lauscht auf Port "+portNr); // Kontrollmeldung
     
        Socket sock = serv.accept(); // Warten, bis sich ein (der nächste) Client meldet,
     
          // und für diesen bzw. für die neue Verbindung einen eigenen Socket anlegen
     
        verbindungsNr++;
             
             
// 2b. Kommunikation mit dem Client abwickeln:
     
        boolean rückgabewert = kommuniziereMitEinemClient (sock, verbindungsNr);
               
// Soll der Server mit mehreren Clients gleichzeitig kommunizieren können,
     
          // darf die Kommunikation hier nur in einem eigene Thread angestoßen werden
     
          // und nicht in der Schleife auf das Ende der Kommunikation gewartet werden!
     
        if (rückgabewert == true)  // Client wünscht Gesamtabschaltung des Servers
     
        {
               verbindungsNr = 
6;  // Verbindungsnummer hoch, damit Schleife beendet wird.
     
        }
           }
      
           
// 3. Server-Socket schließen:
     
      serv.close();
           System.out.println(
"ServerSocket geschlossen.\n");
         }

         
catch (IOException fehler)
         {
           System.out.println(
"** Fehler: "+fehler);
         }
       }

     
       
private boolean kommuniziereMitEinemClient (Socket verbindung, int vnr)
         
// Wenn der Server später mit mehreren Clients gleichzeitig kommunizieren soll, muss diese
     
    // und die folgende Methode reaktionAuf in eine eigene Klasse geschrieben werden, die
     
    // von Thread erbt (d.h. extends Thread hinterm Klassennamen hat)
     
  {
         
boolean abschalten = false// bei true wird der Server ganz abgeschaltet
     
    try
     
    {
           
// 1. Statusmeldungen zur Kontrolle auf Konsole ausgeben:
     
      System.out.println("Server hat Client-Anfrage empfangen: Verbindung "+vnr+" aufgebaut");
           System.out.println(
" ("+verbindung+")");
     
           
// 2. Input- bzw. Output-Streams für aufgebaute Verbindung anlegen:
     
      BufferedReader vomClient = new BufferedReader(new InputStreamReader(verbindung.getInputStream()));
           PrintWriter anClient = 
new PrintWriter(verbindung.getOutputStream(), true);
     
           
// 3. Nachricht(en) vom Client lesen und darauf reagieren:
     
      anClient.println ("Server an Client: Verbindung klappt -- Herzlich Willkommen.\nZum Beenden bitte '$' senden (=Trennen) oder max. 10 Nachrichten!");
           
boolean aufhören = false;   //bei true wird die Verbindung zum aktuellen Client beendet
     
      int zähler = 0;
           
while (! aufhören)
           {
             String empfangen = vomClient.readLine(); 
// vom Server empfangene Nachricht,
     
          // die vom Client an den Server gesandt wurde
     
        zähler++;  // zählt empfangene Nachrichten, weil nach 10 Nachrichten Verbindung aufgehört wird
     
        System.out.println(vnr+"-#"+zähler+".) empfangen: ["+empfangen+"]");  // Kontroll-Ausgabe
     
     
        if (zähler>10 || empfangen==null || empfangen.indexOf('$')>-1)
               
// hier: max. 10 Nachrichten pro Client; Client kann hier mit $ auch vorher die Verbindung
     
          // beenden oder mit $$ den Server ganz abschalten!
     
        {
               System.out.println(
"Verbindung "+vnr+" endet.");
               
if (empfangen.indexOf("$$")>-1// empfangene Nachricht enthält Abschaltzeichen für Server
     
          {
                 System.out.println(
"Server soll auf Wunsch vom Client "+vnr+" abgeschaltet werden.");
                 abschalten = 
true;
                 anClient.println (
"Server wird auf Wunsch von Client "+vnr+" ganz abgeschaltet! $$");
               }
               
else  // nur Ende der Verbindung zum aktuellen Client
     
          {
                 anClient.println (
"Server hat Verbindung beendet: Und tschüss... $$");
               }
               aufhören = 
true;
             }
             
else  // kein Verbindungs- oder Gesamtende, sondern "normale" Nachricht/Anfrage
     
        {
               anClient.println (reaktionAuf(empfangen, zähler));  
// serverseitige Reaktion abschicken
     
          System.out.println ("- Nachricht "+vnr+"-#"+zähler+" ("+empfangen+") empfangen und bearbeitet"); // Kontroll-Ausgabe
     
        }
           }
     
           
// 4. Verbindung mit dem aktuellen Client beenden:
     
      verbindung.close();
         }
         
catch (IOException fehler)
         {
           System.out.println(
"** Fehler: "+fehler);
         }
     
         
return (abschalten);
       }

     
       
private String reaktionAuf (String nachrichtVomClient, int lfdNr)
       {
         
// hier sollte stehen, wie der Server auf Anfragen reagiert. Im folgenden Beispiel
     
    // wird die Nachricht nur wiederholt und bestätigt.
     
    return ("Server an Client: Nachricht #"+lfdNr+" ("+nachrichtVomClient+") kam an!");
       }
     }

Und gestartet wird der Server am besten durch eine eigene Startdatei:

    // Netzwerkprogrammierung: ServerStart -- www.r-krell.de, 3.9.2010
     
     
public class N10S_ServerStart
     {
       
public static void main (String[] s)
       {
         
new N10S_Server().warteAufClientanfragen();
           
// = anonyme Kurzform für:
     
      //    N10S_Server s = new N10S_Server();
     
      //    s.warteAufClientanfragen();
     
  }
     }

Der Server kommt ohne grafische Oberfläche aus, gibt aber -- im vorstehenden Programmtext immer am System.out.print..erkennbar -- einige Kontrollmeldungen auf der Konsole aus (siehe Bilder weiter unten).


zum Seitenanfang / zum Seitenende

Der Client

Die eigentliche Arbeit des Clients wird in der Klasse ClientFunktion verrichtet. Hier stehen alle Methoden, um eine Verbindung zum Server aufzubauen, eine Nachricht zu senden oder eine Nachricht zu empfangen. Die Verbindung zum Netz erfolgt wie oben über einen Socket; einen speziellen ClientSocket gibt es nicht.

     // Netzwerkprogrammierung: Client (Teil des) -- www.r-krell.de, 3.9.2010
     // Klasse, die die eigentliche Arbeit des Clients übernimmt und von der Oberfläche aufgerufen wird
     
     
import java.io.*;
     
import java.net.*;
     
     
public class N10C_ClientFunktion
     {
       Socket sock;
       PrintWriter writer;
       BufferedReader reader;
       
boolean aktiv = false;
     
       
public String verbinde (String adr, int port)
       {
         
try
     
    {
           sock = 
new Socket(adr, port);
           writer = 
new PrintWriter (sock.getOutputStream(), true);
           reader = 
new BufferedReader (new InputStreamReader(sock.getInputStream()));
           aktiv = 
true;
           
return ("Verbindung hergestellt über "+sock+"\n");
         }
         
catch (IOException ioe)
         {
           
return ("** Fehler: "+ioe+"\n");
         }
       }
     
       
public void sende (String nachricht)
       {
         writer.println(nachricht);
         warte (
100);
       }
     
       
public String empfange()
       {
         String empfangen = 
"";
         String zeile = 
"";
         
try
     
    {
           
while (aktiv && reader.ready())
           {
             zeile = reader.readLine();
             
if (zeile != null && !zeile.equals(""))
             {
               empfangen = empfangen + zeile +
"\n";
               
if (zeile.indexOf("$$")>=0)  // Mit Server vereinbartes Zeichen zur
     
          {                            // Trennung der Verbindung
     
            aktiv = false;
                 empfangen = empfangen + 
"[** Verbindung serverseitig beendet **]\n\n";
               }
             }
             warte (
100);
           }
           
if (empfangen == "")
           {
             empfangen = 
" -- (nichts vom Server empfangen)\n";
           }
           
return (empfangen);
         }
         
catch (IOException ioe)
         {
           
return ("** Fehler: "+ioe+"\n");
         }
       }
     
       
public String beende()
       {
         
try
     
    {
           sende (
"$ - Client trennt Verbindung");  // mit dem Server vereinbartes Trennzeichen $
     
      sock.close();
           aktiv = 
false;
           
return ("[** Verbindung beendet und beim Server abgemeldet. **]\n");
         }
         
catch (IOException ioe)
         {
           
return ("** Fehler: "+ioe+"\n");
         }
       }
       
       
public boolean verbindungLäuft()
       {
         
return (aktiv);
       }
     
       
private void warte (int ms)
       {
         
try
     
    {
           Thread.sleep (ms);
         }
         
catch (InterruptedException e)
         {
           System.out.println(
"** Fehler: "+e);
         }
       }     
     }

Bei meinen Tests kam es gelegentlich zu Problemen, wenn sehr schnell hintereinander gesendet oder nach eingehenden Sendungen gesucht wurde. Deshalb habe ich die Methode warte ergänzt und warte nach/bei jeden Senden und Empfangen 100 ms = 0,1 Sekunden lang. In der Methode empfange wird übrigens darauf geachtet, ob der Server zwei Dollarzeichen $$ sendet, weil er damit -- nach 10 Nachrichten -- die Verbindung beenden will. Umgekehrt kann durch Senden eines Dollarzeichens $ vom Client an den Server die Verbindung vom Client her beendet werden (dafür gibt's sogar eine spezielle Methode beende). Durch Senden einer Nachricht mit zwei Dollarzeichen $$ (durch die normale sende-Methode) kann hier, weil es so mit dem Server vereinbart bzw. beim Schreiben des Servers berücksichtigt wurde (s.o.) -- ein Client sogar das vollständige Abschalten des Servers verlangen.

Der Aufruf der vorstehenden Methoden geschieht aus einer grafischen Oberfläche heraus, die im Wesentlichen durch Klicken und Ziehen im Javaeditor erzeugt und deren Quelltext dabei vom Javaeditor automatisch erstellt wurde (die Namen der Oberflächenkomponenten wurden von mir im Objektinspektor bestimmt -- das Formular des Javaeditors, auf dem die Komponenten angeordnet werden, ist mit anderem Beispiel auf meiner dritten Datenbankseite gezeigt). Von Hand hinzugefügt wurden die im Folgenden grün dargestellten Zeilen:

    // Netzwerkprogrammierung: Client (Teil des) -- www.r-krell.de, 3.9.2010
     // hier: Oberfläche
     
     
import java.awt.*;
     
import java.awt.event.*;
     
import javax.swing.*;
     
import javax.swing.event.*;
     
     
     
public class N10C_ClientOberflaeche extends JFrame {
     
       N10C_ClientFunktion client = new N10C_ClientFunktion();   // bindet ein Objekt der oben beschriebenen Klasse ein
     
       
// Anfang Attribute
     
  private JLabel jLabel1 = new JLabel();
       
private JLabel jLabel2 = new JLabel();
       
private JTextField jTfIPAdresse = new JTextField();
       
private JLabel jLabel3 = new JLabel();
       
private JTextField jTfPort = new JTextField();
       
private JButton jBtVerbinden = new JButton();
       
private JButton jBtTrennen = new JButton();
       
private JLabel jLabel4 = new JLabel();
       
private JTextField jTfNachricht = new JTextField();
       
private JButton jBtLoeschen = new JButton();
       
private JButton jBtSenden = new JButton();
       
private JScrollPane jScrollPane1 = new JScrollPane();
         
private JTextArea jTaAusgabe = new JTextArea("");
       
// Ende Attribute
     
     
  public N10C_ClientOberflaeche(String title) {
         
// Frame-Initialisierung
     
    super(title);
         setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
         
int frameWidth = 591;
         
int frameHeight = 468;
         setSize(frameWidth, frameHeight);
         Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
         
int x = (d.width - getSize().width) / 2;
         
int y = (d.height - getSize().height) / 2;
         setLocation(x, y);
         Container cp = getContentPane();
         cp.setLayout(
null);
         
// Anfang Komponenten
     
     
    jLabel1.setBounds(241625516);
         jLabel1.setText(
"Client für Netzwerk-Kontakt zu einem Server");
         jLabel1.setFont(
new Font("MS Sans Serif", Font.PLAIN, 13));
         cp.add(jLabel1);
         jLabel2.setBounds(
24485616);
         jLabel2.setText(
"Server-IP");
         jLabel2.setFont(
new Font("MS Sans Serif", Font.PLAIN, 13));
         cp.add(jLabel2);
         jTfIPAdresse.setBounds(
884812124);
         jTfIPAdresse.setText(
"127.0.0.1");
         cp.add(jTfIPAdresse);
         jLabel3.setBounds(
224482416);
         jLabel3.setText(
"Port");
         jLabel3.setFont(
new Font("MS Sans Serif", Font.PLAIN, 13));
         cp.add(jLabel3);
         jTfPort.setBounds(
256485724);
         jTfPort.setText(
"5001");
         cp.add(jTfPort);
         jBtVerbinden.setBounds(
3364811325);
         jBtVerbinden.setText(
"Verbinden");
         jBtVerbinden.addActionListener(
new ActionListener() {
           
public void actionPerformed(ActionEvent evt) {
             jBtVerbinden_ActionPerformed(evt);
           }
         });
         cp.add(jBtVerbinden);
         jBtTrennen.setBounds(
4564811325);
         jBtTrennen.setText(
"Trennen");
         jBtTrennen.addActionListener(
new ActionListener() {
           
public void actionPerformed(ActionEvent evt) {
             jBtTrennen_ActionPerformed(evt);
           }
         });
         cp.add(jBtTrennen);
         jLabel4.setBounds(
249614116);
         jLabel4.setText(
"Zu sendende Nachricht:");
         jLabel4.setFont(
new Font("MS Sans Serif", Font.PLAIN, 13));
         cp.add(jLabel4);
         jTfNachricht.setBounds(
2412054524);
         jTfNachricht.setText(
"Hallo an alle!");
         cp.add(jTfNachricht);
         jBtLoeschen.setBounds(
12815215325);
         jBtLoeschen.setText(
"Nachricht löschen");
         jBtLoeschen.addActionListener(
new ActionListener() {
           
public void actionPerformed(ActionEvent evt) {
             jBtLoeschen_ActionPerformed(evt);
           }
         });
         cp.add(jBtLoeschen);
         jBtSenden.setBounds(
30415215325);
         jBtSenden.setText(
"Nachricht absenden");
         jBtSenden.addActionListener(
new ActionListener() {
           
public void actionPerformed(ActionEvent evt) {
             jBtSenden_ActionPerformed(evt);
           }
         });
         cp.add(jBtSenden);
         jScrollPane1.setBounds(
24192545225);
         cp.add(jScrollPane1);
         jTaAusgabe.setBounds(-
2, -2545225);
         jTaAusgabe.setText(
"Ausgabe:\n");
         jScrollPane1.setViewportView(jTaAusgabe);
         
// Ende Komponenten
     
     
    setResizable(false);
         setVisible(
true);
       }
     
       
// Anfang Methoden
     
  public void jBtVerbinden_ActionPerformed(ActionEvent evt) {
         
String ipAdresse = jTfIPAdresse.getText();
         
int port = -1;
         
try
     
    {
           port = Integer.parseInt(jTfPort.getText());
         }
         
catch (NumberFormatException ne)
           { };
         
if (port <= 0)
         {
           jTaAusgabe.append (
"** Falsche Portnummer. Bitte Ganzzahl mögl. zwischen 1024 und 31000 eingeben!\n");
         }
         
else
     
    {
           jTaAusgabe.append (client.verbinde(ipAdresse, port));
         }

       }
     
       
public void jBtLoeschen_ActionPerformed(ActionEvent evt) {
         
jTfNachricht.setText("");
       }
     
       
public void jBtSenden_ActionPerformed(ActionEvent evt) {
         
String nachricht = jTfNachricht.getText();
         client.sende (nachricht);
         jTaAusgabe.append ("< '"+nachricht+
"' wurde an Server gesendet.\n");
         jTaAusgabe.append ("> "+client.empfange());

       }
     
       
public void jBtTrennen_ActionPerformed(ActionEvent evt) {
         
jTaAusgabe.append (client.beende()+"\n");
       }
     
       
// Ende Methoden
     
     //  public static void main(String[] args) {
     //    new N10C_ClientOberflaeche("N10C_ClientOberflaeche");
     //  }
     
}

Außerdem musste die automatisch in dieser Klasse erstellte main-Methode hier auskommentiert und in einer eigenen Klasse untergebracht werden, damit Server und Client beide nacheinander aus der Oberfläche der Javaeditor-Arbeitsumgebung gestartet werden können. Später gelingt so auch der Start mehrerer Clients!

   // Netzwerkprogrammierung: ClientStart -- Krell, 3.9.2010
    
    
public class N10C_ClientOberflaecheStart
    {
      
public static void main (String[] s)
      {
        
new N10C_ClientOberflaeche("N10C_ClientOberflaeche");
          
// = erzeugt ClientOberfläche und führt deren Konstruktor aus
    
  }
    }

Damit sind nun alle Teile für beide Programme -- Server und Client -- fertig und können gemeinsam zu einem Test gestartet werden (die im Client voreingestellte IP-Adresse 127.0.0.1 ist dafür gedacht, dass Client und Server wie im nachfolgenden Test auf dem gleichen Rechner laufen. Auch jede andere mit 127. beginnende Adresse verweist auf den eigenen Rechner ('localhost') mit Ausnahme der Netzadresse 127.0.0.0 -- s.o.)


zum Seitenanfang / zum Seitenende

Server und Client im Test

Mit den getrennten Startdateien lassen sich direkt hintereinander die beiden Programme Server und Client aus einer Javaeditor-Umgebung heraus starten und gleichzeitig betreiben. Wird eine Firewall verwendet, wird möglicherweise der Start des Servers bzw. hoffentlich spätestens der Verbindungswunsch des Clients erkannt und muss zugelassen werden:

Bildschirmansicht: Javaeditor mit gestartetem Server und Client sowie Firewall-Warnung

Der Server meldet im schwarzen Konsolenfenster (rechts oben), dass er auf dem hier gewählten Port 5001 auf die Verbindungsaufnahme wartet. Ist der Client ebenfalls gestartet, sein Zugriff erlaubt und werden Nachrichten gesendet, kann sich z.B. folgendes Bild ergeben:

Bildschirmansicht: Server und Client nach kurzem Dialog

Erst nach dem Trennen der Verbindung wartet der Server auf den nächsten Client ("Server lauscht auf Port 5001") und man kann sich entweder erneut von der selben Clientoberfläche durch nochmaligen Druck auf die Taste "Verbinden" zum zweiten Mal anmelden oder diesen Client schließen (durch den Schließknopf "x" oben rechts in der Kopfleiste der Clientoberfäche), aus dem Javaeditor mit der Startdatei ClientOberflaecheStart einen neuen Client starten und dann von dort aus die nächste Verbindung zum Server aufnehmen. Befindet sich der Server auf einem anderen Computer in einem Heim- oder Schulnetzwerk bzw. in einem LAN, so muss im Client vor dem Verbinden die IP-Adresse des Serverrechners (an Stelle von 127.0.0.1 für den eigenen Rechner) eingegeben werden!

Bei genauerer Betrachtung der Meldungen im Ausgabefenster des Clients fällt auf, dass die Begrüßungsmeldung des Servers ("Server an Client: Verbindung klappt... ") erst nach dem Abschicken der ersten Client-Nachricht angezeigt wird (hier erst nach "'Hallo an alle' wurde an den Server gesendet") und nicht unmittelbar nach dem Verbinden. Der Blick in den Quelltext des Servers zeigt hingegen, dass der Server (in seiner Methode kommuniziereMitEinemClient) diese Willkommens-Nachricht unmittelbar nach erfolgter Verbindung an den Client abschickt. Kontrolliert man daraufhin den Quelltext der Client-Oberfläche, so erkennt man dort in der ziemlich am Schluss zu findenden Methode jBtSenden_ActionPerformed den Grund für das Verhalten: Der Client ist keineswegs dauernd auf Empfang geschaltet, sondern empfängt bzw. liest immer erst nach einem eigenen Senden alle bis dahin aufgelaufenen Meldungen des Servers! Nur innerhalb der Methode des Sende-Knopfes wird überhaupt empfange aufgerufen! Dieses Verhalten ist unbefriedigend. Allerdings kann in der Client-Oberfläche keine Schleife gestartet werden, die immer wieder einen Empfangsversuch startet und dazwischen vielleicht eine Zehntelsekunde wartet: Diese Schleife würde nie enden, aber zwischendurch auch keine anderen Aktionen zulassen: Die Benutzung der Oberfläche wäre nicht möglich, das Programm blockiert. Abhilfe könnte höchstens eine zusätzliche Schaltfläche "empfangene Nachrichten ansehen" bringen: Bei Druck auf diesen Knopf würde die Methode empfange der Client-Funktion aufgerufen und das Ergebnis angezeigt, danach könnte sofort wieder jede andere Schaltfläche bedient werden. Von einem ordentlichen Programm erwartet man jedoch, dass es den Benutzer entlastet und ihm Routineaufgaben abnimmt, statt ihm die Verantwortung für den regelmäßigen Blick auf möglicherweise empfangene Nachrichten aufzubürden. Ankommende Nachrichten sollten sofort und ohne Zutun des Benutzers und unabhängig von dessen Sende-Aktionen angezeigt werden. Insofern besteht hier Verbesserungsbedarf am Client. Außerdem wäre es wünschenswert, wenn der Server gleichzeitig mehrere Clients bedienen könnte.




zum Seitenanfang / zum Seitenende

Nebenläufigkeit bzw. Parallelität mit Threads



Beide gerade geforderten Verbesserungen -- die am Client ebenso wie die am Server -- erfordern Parallelität. Im Client soll parallel zu möglichen Benutzereingaben praktisch im Hintergrund empfangen werden; beim Server sollen Verbindungen zu mehreren Clients aufgebaut und parallel gehalten bzw. bedient werden können. In älteren Programmiersprachen hätte die Programmiererin bzw. der Programmierer in einer geeigneten Schleife selbst dafür sorgen müssen, dass in schnellem Wechsel die verschiedenen Aufgaben immer stückweise -- jetzt Abfrage der Benutzereingabe, dann kurzer Empfangsscheck oder jetzt kurz mit Client1, dann kurz mit Client2, dann schnell mit Client 3 kommunizieren und das dann wieder mit Client 1 beginnen -- erledigt werden und so der Eindruck von Parallelität entsteht. In Java ist ein solcher Mechanismus bereits eingebaut bzw. mitgeliefert in Form von Threads.

Die mögliche Programmierung zeigt der Vergleich der beiden nebeneinander gestellten Bildschirmabdrucke:

Thread-Demo (Quelltext und Ausgabe), 2 Versionen im Vergleich

Links wird herkömmlich gearbeitet. In der Startdatei werden in einer Schleife nacheinander drei Objekte vom Typ T1_Zaehler erzeugt und jeweils deren Methode run aufgerufen. Der Blick auf die zugehörige linke schwarze Ausgabekonsole zeigt, dass jeweils die Erledigung aller Anweisungen in der Methode run abgewartet werden muss (dort wird -- unterbrochen von Pausen der Länge 0,2 Sekunden -- immer von 0 bis 4 gezählt, was insgesamt rund 1 Sekunde dauert), bevor die Schleife in der Startdatei erneut durchlaufen und das nächste Objekt erzeugt und zum Zählen gebracht wird. Die Objekte Nr. 1 bis 3 zählen nacheinander von 0 bis 4.

Der rechte Quelltext wurde gegenüber dem linken nur an wenigen, markierten Stellen abgeändert. Wichtig ist, dass jetzt die Klasse T2_Zaehler von der Java-Klasse Thread abgeleitet (ererbt) wird. Im Konstruktor T2_Zaehler wird sicherheitshalber mit super() auch der Konstruktor der Oberklasse Thread aufgerufen (obwohl es bei Versuchen auch ohne diese Zeile ging). Würde in der Startdatei jetzt weiterhin die Methode run jedes erzeugten T2_Zaehler-Objekts aufgerufen, würde sich am Verhalten und der Ausgabe trotzdem nichts gegenüber der linken Seite ändern. Allerdings stellt der Thread ein Besonderheit bereit: mit der vordefinierten Methode start kann die selbstgeschriebene Methode run angestoßen werden und start meldet sofortigen Vollzug bzw. endet sofort, obwohl run noch beliebig lange weiterlaufen kann. Dadurch kann die Schleife in der rechten Startdatei blitzschnell durchlaufen werden -- so schnell, dass die Startmethode bereits endet, bevor alle drei erzeugten Zählobjekte auch nur ihre erste Zahl 0 genannt haben. Nach den meisten Starts zählen dann ganz schnell hintereinander die Zähler-Objekte Nr. 1 bis Nr. 3 zuerst erst alle 0, dann zählen alle 1, nach 0,2 Sekunden zählen alle 3 usw. Aber es passiert gelegentlich, dass auf Grund minimaler und vom Programmierer unvorhersehbarer Verzögerungen in den einzelnen Threads sich die Threads gegenseitig überholen oder etwas hinter dem Nachbarn zurück bleiben (siehe abgebildete Ausgabe im rechten schwarzen Konsolenfenster). Die Threads laufen eben unabhängig voneinander, nicht synchronisiert, sondern quasi parallel. (thread heißt übrigens Faden und Threads sollen so parallel oder nebeneinander laufen, wie die einzelnen Fäden in einem dicken Tau oder in einem Stahlseil)




zum Seitenanfang / zum Seitenende

Neue, verbesserte Versionen von Client und Server

Der vorstehend illustrierte Thread-Mechanimus wird im Folgenden benutzt, um erst den Client und später den Server zu verbessern.



Client mit nebenläufiger Empfangsabfrage

Eine dauernd aktive Wiederholstruktur könnte im Client z.B. alle Zehntelsekunde nach empfangenen Nachrichten suchen und solche bei Erfolg im entsprechenden JTextArea-Ausgabefenster der Clientoberfläche anzeigen. Damit der Rest des Client-Programms trotzdem noch funktioniert, muss diese Schleife allerdings in eine eigene, von Thread abgeleitete Klasse ausgelagert und statt mit run besser mit start gestartet werden!



Zunächst wird diese zusätzliche Klasse geschrieben:

    // Netzwerkprogrammierung: Client (Teil des) -- www.r-krell.de, 5.9.2010
     // Thread, der von der Oberfläche aus aufgerufen wird, ständig auf Empfang ist und
     // empfangene Nachrichten im übergebenen Textfenster der Oberfläche ausgibt. Es werden die
     // Methoden des ebenfalls übergebenen ClientFunktions-Objekts benutzt.
     
     
import javax.swing.*;          // damit JTextArea bekannt ist
     
     
public class N10Cn_ClientEmpfangsthread extends Thread
     {
       N10Cn_ClientFunktion fkt;    
// Variablen zum Speichern der übergebenen Parameter
     
  JTextArea ausgabefenster;
       
       
public N10Cn_ClientEmpfangsthread (N10Cn_ClientFunktion f, JTextArea a) // Konstruktor
     
  {                            // .. mit Parametern
     
    fkt = f;
         ausgabefenster = a;
       }
       
       
public void run()            // Methode run lässt sich nebenläufig starten
     
  {
         
boolean läuft = true;
         String erhalten = 
"";
         
while (läuft)
         {
           erhalten = fkt.empfange(); 
// weil empfange mit Warteschleife, hier nicht zusätzlich nötig
     
      if (!erhalten.equals(" -- (nichts vom Server empfangen)\n"))
           {
             ausgabefenster.append (
"> "+erhalten);
             
if (erhalten.indexOf("[** Verbindung serverseitig beendet")>=0 ||
               erhalten.indexOf(
"[** Verbindung beendet")>=0)
             {
               läuft = 
false;
               System.out.println(
"Empfangsthread beendet.");
             }
           }
         }
       }
     }

Die Methode run muss paramterlos bleiben, damit der vordefinierte Thread-Mechanismus funktioniert. Andererseits muss ein Objekt dieser Klasse sowohl das Ausgabefenster der ClientOberfläche kennen, damit es dort rein schreiben (bzw. mit ausgabefenster.append Text anhängen) kann, als auch das von der Oberfläche verwendete Objekt vom Typ ClientFunktion kennen, um dessen empfange-Methode zu benutzen. Deshalb werden diese beiden Parameter f und a dem Konstruktor übergeben und in globalen Variablen fkt und ausgabefenster des Objekts abgelegt.

Die Client-Oberfläche erhält dann nur die eine, hier rot geschriebene zusätzliche Zeile zum Einbinden des Objekts dauerempfang nach dem gerade vorgestellten Bauplan der Klasse N10Cn_ClientEmpfangsthread.

    // Netzwerkprogrammierung: Client (Teil des) -- www.r-krell.de, 5.9.2010
     // hier: Oberfläche mit automatischer nebenläufiger Empfangskontrolle
     
     
import java.awt.*;
     
import java.awt.event.*;
     
import javax.swing.*;
     
import javax.swing.event.*;
     
     
     
public class N10Cn_ClientOberflaeche extends JFrame {
     
       N10Cn_ClientFunktion client = 
new N10Cn_ClientFunktion();
     
       
// Anfang Attribute
     
  private JLabel jLabel1 = new JLabel();
       
private JLabel jLabel2 = new JLabel();
       
private JTextField jTfIPAdresse = new JTextField();
       
private JLabel jLabel3 = new JLabel();
       
private JTextField jTfPort = new JTextField();
       
private JButton jBtVerbinden = new JButton();
       
private JButton jBtTrennen = new JButton();
       
private JLabel jLabel4 = new JLabel();
       
private JTextField jTfNachricht = new JTextField();
       
private JButton jBtLoeschen = new JButton();
       
private JButton jBtSenden = new JButton();
       
private JScrollPane jScrollPane1 = new JScrollPane();
         
private JTextArea jTaAusgabe = new JTextArea("");
       
// Ende Attribute
     
  
       N10Cn_ClientEmpfangsthread dauerempfang = new N10Cn_ClientEmpfangsthread (client, jTaAusgabe);
         
// Definition des Threads (geht erst, wenn client und jTaAusgabe erzeugt wurden)
     
     
  public N10Cn_ClientOberflaeche(String title) {
         
// Frame-Initialisierung
     
    super(title);

         ...
usw. wie in N10C_ClientOberflaeche

und erst ganz unten bei den Methoden wird aus jBtSenden_ActionPerformed die letzte Programmzeile mit dem empfange als unnötig entfernt bzw. wie hier auskommentiert, weil das Empfangen jetzt ständig und parallel von dauerempfang übernommen wird:

       public void jBtSenden_ActionPerformed(ActionEvent evt) {
         String nachricht = jTfNachricht.getText();
         client.sende (nachricht);
         jTaAusgabe.append (
"< '"+nachricht+"' wurde an Server gesendet.\n");
         
// jTaAusgabe.append ("> "+client.empfange());  -- jetzt in extra Thread!
     
  }

Tests mit diesem neuen Client am alten Server zeigen, dass jetzt die Willkommensmeldung sofort erscheint und nicht erst nach dem ersten Versenden einer Nachricht (vgl. Bilder weiter unten). Auch weitere zwischendurch einfach so vom Server versandte Meldungen (die bei unserem Server bisher allerdings nicht vorgesehen sind) würden sofort im Client erscheinen.


zum Seitenanfang / zum Seitenende

Server mit gleichzeitiger Verbindung zu vielen Clients

Bei der Verbindung mit einem Client dauert die Serverfunktion kommuniziereMitEinemClient bisher so lange -- nämlich bis zum Ende der Verbindung -- und blockiert damit den erneuten Schleifendurchlauf der mit  while (verbindungsNr < 5) beginnenden Wiederholstruktur innerhalb der Methode warteAufClientanfragen in der Klasse N10S_Server.

kommuniziereMitEinemClient müsste also in einen eigenen Thread (und damit in eine andere, von Thread abgeleitete Klasse) ausgelagert werden und aus der besagten Schleife nur noch mit start in Gang gesetzt werden. Damit das geht, muss die Methode in run umbenannt werden und darf weder Parameter noch Rückgabewert haben. Die Parameter können wie oben beim Client schon dem Konstruktor des zuständigen Objekts mitgegeben werden. Die Sache mit dem Rückgabewert ist etwas komplizierter. Hier wird dafür die boolean-Varaible, die zurückgegeben werden soll, in eine eigene Klasse verpackt und ebenfalls dem Konstruktor übergeben. Da Java bei Objekten nicht Kopien der Inhalte eines Objekts, sondern nur die Speicheradresse des Objekts als Parameter weiter gibt, weiß die neue Klasse also, wo die Variable des Servers im Hauptspeicher liegt und kann sie direkt dort ändern. Und wenn der Server seine Variable abfragt, sieht er die Änderungen bzw. das, was run dort als Ergebnis hinein geschrieben hat. Damit ist also auch ein Informationsfluss zurück möglich.

Zunächst die neue Thread-Klasse mit Einigem, das bisher direkt in der Server-Klasse stand:

          // Netzwerkprogrammierung: SerververbindungZu1Client als Thread ausgelagert -- Krell, 11.9.2010
     
     
import java.io.*;
     
import java.net.*;
     
     
public class N10Sm_SerververbindungZu1Client extends Thread
       
// erbt Thread-Funktionalität vom in Java vordefinierten Thread
     
{
       
// klassenglobale Variable für den Datenaustausch:
     
  Socket verbindung;
       
int vnr;
       N10Sm_Austausch gesamtEnde;
       
       
public N10Sm_SerververbindungZu1Client (Socket verb, int nr, N10Sm_Austausch ge)  // Konstruktor
     
  // Weil die nachfolgende Methode run weder Parameter noch einen Rückgabewert haben darf, müssen die
     
  // Variablen bereits beim Erzeugen des Objekts im Konstruktor übergeben werden. Weil gesamtEnde
     
  // eine Objekt-Variable ist, wird keine Kopie überreicht, sondern die Speicheradresse.
     
  // Wird hier der Wert verändert, ändert sich der Wert an der ursprünglichen Stelle (im Server) und kann
     
  // dort abgefragt werden. Damit erhält der Server Informationen aus dieser Klasse, ohne dass es eines
     
  // Rückgabewerts bedarf.
     
  {
         verbindung = verb;
         vnr = nr;
         gesamtEnde = ge;
       }
     
       
public void run()  // früher: kommuniziereMitEinemClient
     
  // Das hier ist die alte Methode kommuniziereMitEinemClient aus dem Server, nur jetzt public
     
  // (damit trotzdem vom Server aus erreichbar) und ohne Parameter und ohne Rückgabewert.
     
  // Die Methode muss jetzt run() heißen, damit sie in einem neuen Thread mit start() gestartet
     
  // werden kann
     
  {
         
// jetzt ohne abschalten; dafür wird unten der gemeinsameWahrheitswert gesetzt!
     
    try
     
    {
           
// 1. Statusmeldungen zur Kontrolle auf Konsole ausgeben:
     
      System.out.println("Server hat Client-Anfrage empfangen: Verbindung "+vnr+" aufgebaut");
           System.out.println(
" ("+verbindung+")");
     
           
// 2. Input- bzw. Output-Streams für aufgebaute Verbindung anlegen:
     
      BufferedReader vomClient = new BufferedReader(new InputStreamReader(verbindung.getInputStream()));
           PrintWriter anClient = 
new PrintWriter(verbindung.getOutputStream(), true);
     
           
// 3. Nachricht(en) vom Client lesen und darauf reagieren:
     
      anClient.println ("Server an Client "+vnr+": Verbindung klappt -- Herzlich Willkommen.\nZum Beenden bitte '$' senden (=Trennen) oder max. 10 Nachrichten!");
           
boolean aufhören = false;   //bei true wird die Verbindung zum aktuellen Client beendet
     
      int zähler = 0;
           
while (! aufhören)
           {
             String empfangen = vomClient.readLine(); 
// vom Server empfangene Nachricht,
     
          // die vom Client an den Server gesandt wurde -- hier wird immer auf die nächste Nachricht gewartet,
     
          // erst dann geht's in der Schleife weiter. Deshalb erreicht ein Abschaltwunsch eines Clients
     
          // nicht sofort alle parallelen Clients, sondern jeden erst, nachdem der selbst gesendet hat und
     
          // deshalb hier die Schleife weiter geht bis "anClient.println ("Server wird auf Wunsch.." (s.u)
     
          // Abhilfe: in jeder SerververbindungZu1Client zuätzlichen Thread für vomClient.readLine()
     
          // ähnlich wie bei der Verbesserung der Clients von Version N10C zu N10Cn !
     
        zähler++;  // zählt empfangene Nachrichten, weil nach 10 Nachrichten Verbindung aufgehört wird
     
        System.out.println(vnr+"-#"+zähler+".) empfangen: ["+empfangen+"]");  // Kontroll-Ausgabe
     
     
        if (zähler>10 || empfangen==null || empfangen.indexOf('$')>-1)
               
// hier: max. 10 Nachrichten pro Client; Client kann hier mit $ auch vorher die Verbindung
     
          // beenden oder mit $$ den Server ganz abschalten!
     
        {
               System.out.println(
"Verbindung "+vnr+" endet.");
               
if (empfangen.indexOf("$$")>-1// empfangene Nachricht enthält Abschaltzeichen für Server
     
          {
                 System.out.println(
"Server soll auf Wunsch vom Client "+vnr+" abgeschaltet werden.");
                 gesamtEnde.endWunschVon = vnr;
               }
               
else  // nur Ende der Verbindung zum aktuellen Client
     
          {
                 anClient.println (
"Server hat Verbindung beendet: Und tschüss... $$");
               }
               aufhören = 
true;
             }
             
if (gesamtEnde.endWunschVon > 0)  // Zusätzliche Kontrolle, weil auch anderer Client mit paralleler ..
     
        {    //.. SerververbindungZu1Clienet Abschaltwunsch über gesamtEnde-Austausch äußern konnte
     
          anClient.println ("Server wird auf Wunsch von Client "+gesamtEnde.endWunschVon+" ganz abgeschaltet! $$");
               aufhören = 
true;
             }
             
else  // kein Verbindungs- oder Gesamtende, sondern "normale" Nachricht/Anfrage
     
        {
               anClient.println (reaktionAuf(empfangen, zähler));  
// serverseitige Reaktion abschicken
     
          System.out.println ("- Nachricht "+vnr+"-#"+zähler+" ("+empfangen+") empfangen und bearbeitet"); // Kontroll-Ausgabe
     
        }
           }
     
           
// 4. Verbindung mit dem aktuellen Client beenden:
     
      verbindung.close();
         }
         
catch (IOException fehler)
         {
           System.out.println(
"** Fehler: "+fehler);
         }
         
// kein Rückgabewert;
     
  }
     
       
private String reaktionAuf (String nachrichtVomClient, int lfdNr)
       {
         
// hier sollte stehen, wie der Server auf Anfragen reagiert. Im folgenden Beispiel
     
    // wird die Nachricht nur wiederholt und bestätigt.
     
    return ("Server an Client: Nachricht #"+lfdNr+" ("+nachrichtVomClient+") kam an!");     
       }
     }


Und hier die Klasse für das gemeinsam benutzte Austauschobjekt (an Stelle eines Rückgabewerts). Jetzt wird nicht einfach true in eine boolean-Variable für den Abschaltwunsch eingetragen, sondern die Nummer des Clients bzw. der Verbindung, in der dieser Wunsch geäußert wurde, in eine Ganzzahl-Variable.

     // Netzwerkprogrammierung: Klasse/Objekt für Austausch -- Krell, 11.9.2010
     
     
public class N10Sm_Austausch   // Klasse, damit ein Objekt nach diesem Bauplan als
     
  // gemeinsame Variable an alle Client-Verbindungen übergeben kann, um so eine Rückmeldung zum
     
  // Server zu ermöglichen (da run keinen Rückgabewert erhalten darf).
     
{
       
int endWunschVon = 0// 0 = kein Client hat bisher das Ende verlangt;
     
}

     

Dadurch, dass jetzt Vieles vom ehemaligen Server ausgelagert wurde, schrumpft die Klasse auf jetzt neu

          // Netzwerkprogrammierung: Server -- Krell, 11.9.2010
     // Server kann gleichzeitig mit bis zu 5 Clients je mehrere (hier max. 10) Nachrichten wechseln
     // Dazu wurden Teile der usrprünglichen Server-Klasse in die Klasse N10Sm_ServerbindungZu1Client
     // ausgelagert und zum Datenaustausch noch die N10Sm_Ausuatusch angelegt.
     // Geeignete Clients: N10C_* oder N10Cn_*
     
     
import java.io.*;
     
import java.net.*;
     
     
public class N10Sm_Server
     {
       
public void warteAufClientanfragen()  // der Server wartet auf und stellt
     
    // die Verbindungen mit bis zu 5 Clients praktisch gleichzeitig her (multithreading)!
     
  {
         
try
     
    {
           
// 1. den einen Server-Socket anlegen:
     
      int portNr = 5001// oder eine andere willkürliche Portnummer möglichst > 1023
     
      ServerSocket serv = new ServerSocket(portNr);
     
           
// 2. auf nächsten Client warten und mit diesem kommunizieren:
     
      int verbindungsNr = 0;
           N10Sm_Austausch austausch = 
new N10Sm_Austausch();
           
while (verbindungsNr < 5// hier willkürlich festgelegte Höchstzahl an Verbindungen
     
      {
             
// 2a. Warte auf nächsten Client und stelle Verbindung her:
     
        System.out.println("Server (Version m) lauscht auf Port "+portNr); // Kontrollmeldung
     
        Socket sock = serv.accept(); // Warten, bis sich ein (der nächste) Client meldet,
     
          // und für diesen bzw. für die neue Verbindung einen eigenen Socket anlegen
          
             if (austausch.endWunschVon > 0)  // irgendein angemeldeter Client wünscht die Gesamtabschaltung des Servers
             {     // gegenüber Version N10S_Server mit rückgabewert hier mit austausch und nach oben, um unmögliche Verbindungen zu vermeiden
     
          verbindungsNr = 6;  // Verbindungsnummer hoch, damit Schleife beendet wird.
     
        }
             
else  // neue Verbindung nur anlegen, wenn bisher kein Abschaltwunsch geäußert wurde
     
        {
               verbindungsNr++;
     
               
// 2b. Kommunikation mit dem Client abwickeln:
     
          N10Sm_SerververbindungZu1Client nächsteClientVerbindung = new N10Sm_SerververbindungZu1Client (sock, verbindungsNr, austausch);
               nächsteClientVerbindung.start();
                 
// Für jeden angemeldeten Client wird ein neues Objekt nächsteClientVerbindung erzeugt
     
            // und mit den Parametern gefüllt. Dann wird mit start() die Methode run() angestoßen.
     
            // Allerdings endet start() sofort nach dem Anstoß und wartet nicht, bis run() und damit
     
            // die Kommunikation mit dem Client beendet wurde. Dadurch sind mehrere Clients gleichzeitig
     
            // möglich, weil weitere Clients angemeldet und Verbindungen dazu gestartet werden können, während die
     
            // Kommuniktion mit den alten Clients noch läuft!
     
            // Da nächsteClientVerbindung hier nicht weiter verwendet wird, stört es nicht, dass alle Objekte
     
            // den gleichen Namen tragen. Will man später auf eine bestimmte Verbindung zugreifen, ollte z.B. vor
     
            // der while-Schleife eine Reihung N10Sm_SerververbindungZu1Client[] nächsteClientVerbindung
     
            // = new N10Sm_SerververbindungZu1Client[5]; angelegt werden und dann die beiden Java-Zeilen wie folgt lauten:
     
            // N10Sm_SerververbindungZu1Client nächsteClientVerbindung[verbindungsNr] = ...
                 // ... = new N10Sm_SerververbindungZu1Client (sock, verbindungsNr, austausch);
     
            // nächsteClientVerbindung[verbindungsNr].start();    
     
        }
           }
           
           
// 3. Server-Socket schließen:
     
      serv.close();
           System.out.println(
"ServerSocket geschlossen.\n");
         }
         
catch (IOException fehler)
         {
           System.out.println(
"** Fehler: "+fehler);
         }
       }
       
       
// Früher hier privat definierte Methoden jetzt in N10Sm_ServerbingZu1Client !
     
}

Und gestartet wird jetzt natürlich mit

         // Netzwerkprogrammierung: ServerStart -- Krell, 11.9.2010     
     
public class N10Sm_ServerStart
     {
       
public static void main (String[] s)
       {
         
new N10Sm_Server().warteAufClientanfragen();
           
// = anonyme Kurzform für:
     
      //    N10Sm_Server s = new N10Sm_Server();
     
      //    s.warteAufClientanfragen();
     
  }
     }

Damit ist der Server multithreadingfähig verbessert!


zum Seitenanfang / zum Seitenende

Gemeinsamer Test der beiden verbesserten Programmversionen

Der gezeigte Bildschirmabdruck beweist, dass die Ziele der Verbesserung sowohl beim Client wie beim Server erreicht wurden: Mehrere Clients (hier zwei) kommunizieren gleichzeitig mit dem gleichen Server und erhalten ohne eigenes Senden sofort die Willkommensnachricht:

Bildschirmansicht: verbesserte Programme im Test -- Server mit 2 Clients



zum Seitenanfang / zum Seitenende

Ausblick und Erweiterungen



Das Programm ist durchaus betriebsfähig, kann in Details aber noch verbessert werden.

Außerdem laufen die Kommunikationen zwischen dem Server und den Clients bisher völlig voneinander getrennt ab. Etwa für einen Chat-Server oder ähnliche Anwendungen sollen aber die verbundenen Clients nicht nur die Meldungen des Servers, sondern auch die Meldungen anderer Clients hören können. Das ist bisher nicht programmiert. Der oben bereits beschriebene Umweg über ein Austauschobjekt zeigt allerdings einen möglichen Weg: alle Clientverbindungen könnten in einer als bzw. in einem Austauschobjekt übergebenen Variablen -- also alle an der gleichen Speicherstelle -- die öffentlichen Nachrichten hinterlegen, wo sie die einzelnen run-Methoden der Threadobjekte auch wieder lesen und an ihre verbundenen Clients weiter leiten ...

Eine weitere lohnenswerte Möglichkeit besteht sicher auch in der Kombination mit Datenbanken. Meine drei Seiten zu Datenbanken und Java-Programmen mit Datenbankanbindung könnten dazu Anregungen geben.

Aber es sollten hier ja auch nicht alle möglichen Programme vorweg genommen werden, sondern Grundlagen und Ideen für eigenes Programmieren aufgezeigt werden.


zum Seitenanfang / zum Seitenende



zurück zur Informatik-Hauptseite

(Die anderen Java-Seiten werden entweder mit dem Menü hier am Seitenanfang oder
am besten auch auf der Informatik-Hauptseite ausgewählt)


zum Anfang dieser Seite
Willkommen/Übersicht  -  Was ist neu?  -  Software  -  Mathematik  -  Physik  -  Informatik  -   Schule: Lessing-Gymnasium und -Berufskolleg  -  Fotovoltaik  -  & mehr  -  Kontakt: e-Mail, Impressum  -  Grußkarten, site map, Download und Suche

Diese Seite ist Teil des Webangebots http://www.r-krell.de. Sie können diese Seite per e-Mail weiter empfehlen (tell a friend).