Netzwerkprogrammierung: Unterschied zwischen den Versionen

Aus SibiWiki
Zur Navigation springen Zur Suche springen
 
(49 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
[[Kategorie:Netzwerke(IF)]]
[[Kategorie:Netzwerke(IF)]]
[[Kategorie:Informatik]]
[[Kategorie:Informatik]]
<font color='red'>'''im Abi 2021 und später nicht mehr relevant für Abiturklausuren'''</font>


= Allgemeines =
= Allgemeines =
Netzwerkprogrammierung beschäftigt sich mit der Entwicklung von verteilten Anwendungen.  
Netzwerkprogrammierung beschäftigt sich mit der Entwicklung von verteilten Anwendungen.  


Dabei kommuniziert zumeist ein '''Server''' mit mehreren '''Clients'''.
Dabei kommuniziert zumeist ein '''Server''' mit mehreren '''Clients'''.


= Schnittstellen des Zentralabitur =
* [[Datei:Server.pdf]]
* [[Datei:Client.pdf]]
* [[Datei:Connection.pdf]]


= Best Practices für Netzwerk-Protokolle =
= Best Practices für Netzwerk-Protokolle =
Es gibt meines Wissens keine allgemeingültigen Best Practices für die Erstellung von Protokollen.
siehe: [[Protokoll_(IF)]]
 
Diese Best Practices sind vor allem auf die zuverlässigen Protokoll-Erstellung im Zentralabitur zugeschnitten und so angelegt, dass sich Protokolle einfach mit Java auswerten lassen.
 
# Jeder Befehl wird mit einem einheitlichen Zeichen eingeleitet, z.B. mit "+" (für erfolgreiche Befehle) und "-" (für Fehlermeldung.)
# JEDE Übertragung zwischen Client und Server startet mit einem Befehl, d.h. auch z.B. das einfache Senden von Nachrichtentext.
#Jede Nachricht an den Server wird vom Server entweder bearbeitet oder es kommt eine Fehlermeldung zurück. Die möglichen Fehlermeldungen müssen im Protokoll festgehalten werden.
# Es gibt eine allgemeine Fehlermeldung für unbekannte Befehle.
#Jeder Befehl hat genau 5 Buchstaben, davon ist der erste ein Plus oder Minus. Das hilft Ihnen bei der Programmierung des Servers Befehle und Parameter auszuwerten:
 
<code>
  String befehl = pMessage.substring(0,4);
  String parameter = pMessage.substring(6);
  // wenn Sie mehrere Parameter haben:
  String[] parameterSplits = parameter.split[" "];
</code>
 
 
 


= Client-Programmierung =
= Client-Programmierung =
Zeile 34: Zeile 22:
== Connection.java vs. Client.java ==
== Connection.java vs. Client.java ==
Die Schnittstellen fürs Zentralabitur stellen zwei Klassen zur Verfügung, mit denen auf der Client-Seite gearbeitet werden kann:
Die Schnittstellen fürs Zentralabitur stellen zwei Klassen zur Verfügung, mit denen auf der Client-Seite gearbeitet werden kann:
# '''Connection.java''' hat die folgenden Methode, um Nachrichten vom Server zu empfangen:
* '''Connection.java''' hat die folgenden Methode, um Nachrichten vom Server zu empfangen:
## <code>public String receive()</code>
** <code>public String receive()</code>
# '''Client.java''' hat die folgenden Methode, um Nachrichten vom Server zu empfangen:
* '''Client.java''' hat die folgenden Methode, um Nachrichten vom Server zu empfangen:
## <code>public void processMessage(String pMessage</code>
** <code>public void processMessage(String pMessage)</code>


Die einfachere (=mit weniger Möglichkeiten) ist die Klasse Connection.java.
Die einfachere (=mit weniger Möglichkeiten) ist die Klasse Connection.java.
Diese beiden Klassen werden in unterschiedlichen Szenarien benutzt:
Diese beiden Klassen werden in unterschiedlichen Szenarien benutzt:
# '''Connection.java''' wird genutzt, wenn '''der Server nur auf Anfragen des Clients reagiert''', wie z.B. bei Mailprotokollen (SMTP oder POP3).
* '''Connection.java''' wird genutzt, wenn '''der Server nur auf Anfragen des Clients reagiert''', wie z.B. bei Mailprotokollen (SMTP oder POP3).
# '''Client.java''' wird genutzt, wenn '''der Server von sich aus aktiv werden kann'''. Das ist z.B. bei einem Chat der Fall, denn hier muss der Server Nachrichten eines ''anderen'' Client von sich aus an alle Clients weiterleiten können.
* '''Client.java''' wird genutzt, wenn '''der Server von sich aus aktiv werden kann'''. Das ist z.B. bei einem Chat der Fall, denn hier muss der Server Nachrichten eines ''anderen'' Client von sich aus an alle Clients weiterleiten können.
 
==Connection: Standardvorgehen ==
In der Regel wird wie folgt vorgegangen:
# '''Verbindung herstellen:'''
## Man verbindet sich mit dem Server, indem man ein neues <code>Connection</code>-Objekt erzeugt.
## Man empfängt die Willkommensnachricht des Servers und wertet sie ggf. aus.
# '''Einfache Nachrichten senden/empfangen:'''
## Man baut einen String <code>zumServer</code> zusammen. In <code>zumServer</code> ist die Nachricht enthalten, die man zum Server schicken möchte.
## Man gibt <code>zumServer</code> an die Konsole aus - zur Kontrolle!
## Man schickt <code>zumServer</code> an den Server.
## Man empfängt die Antwort des Servers und speichert sie in <code>vomServer</code>.
## Man gibt <code>vomServer</code> an die Konsole aus - zur Kontrolle!
## Jetzt kann man <code>vomServer</code> auswerten.
# '''Mehrzeilige Nachrichten empfangen:''' Wenn eine Serverantwort mehrzeilig ist, dann wird sie in der Regel mit einem einzelnen Punkt (bzw. einer anderen Endmarke) beendet. Darauf reagiert man wie folgt:
## Man empfängt eine Zeile vom Server und speichert sie in <code>vomServer</code>.
## Eine <code>while</code>-Schleife, die so lange läuft, bis <code>vomServer</code> ein einzelner Punkt (bzw. die Endmarke) ist. In der Schleife passiert folgendes...
### Man gibt <code>vomServer</code> an die Konsole aus - zur Kontrolle, ob die Antwort vom Server wie erwartet ist.
### <code>vomServer</code> auswerten.
### Man empfängt die nächste Zeile vom Server und speichert sie in <code>vomServer</code>.
 
==Connection: Standardvorgehen in Java==
 
# '''Verbindung herstellen:'''
## <code>Connection verbindung = new Connection("192.168.100.100", 4242);</code> //IP und Port sind nur Beispiele!
## <code>String vomServer = verbindung.receive(); </code>
# '''Einfache Nachrichten senden/empfangen:'''
## <code>String zumServer = "LOGIN "+username+" "+passwort;</code> // das ist ein Beispiel für ein Login
## <code>System.out.println(zumServer); </code>
## <code>verbindung.send(zumServer);</code>
## <code>vomServer = verbindung.receive();</code>.
## <code>System.out.println(vomServer); </code>
## <code>if(vomServer.startsWith("+OK")){ </code> //Beispiel für eine Auswertung
# '''Mehrzeilige Nachrichten empfangen:'''
## <code>vomServer = verbindung.receive();</code>
## <code>while(vomServer.equals(".") == false){</code>
### <code>System.out.println(vomServer);</code>
### <code>ergebnis.append(vomServer);</code> // Beispiel für eine Auswertung: An eine ergebnis-Liste anhängen.
### <code>vomServer = verbindung.receive();</code>.
 
== Client.java: Standardvorgehen in Java ==
Von '''Client.java''' dagegen wird eine Unterklasse gebildet, die die Fähigkeiten von Client.java '''erbt'''. Dabei muss die Methode <code>public void processMessage(String pMessage)</code> '''überschrieben''' werden, damit man auf die Nachrichten des Servers reagieren kann.


=== Connection.java vs. Client.java in der Programmierung ===
Beispiel ChatClient:
# '''Connection.java''' wird als '''Attribut''' benutzt, z.B.:
<code>
  public class SMTPClient{
    private Connection verbindung;
   
    public SMTPClient(){
        //Verbindung zum Mailserver herstellen
        verbindung = new Connection("192.168.100.2", 25);
    }
    ....
  }
</code>


# Von '''Client.java''' dagegen wird eine Unterklasse gebildet, die die Fähigkeiten von Client.java '''erbt'''. Dabei muss die Methode <code>public void processMessage(String pMessage)</code> '''überschrieben''' werden, damit man auf die Nachrichten des Servers reagieren kann. Z.B.:
<code>
<code>
     public class ChatClient '''extends Client'''{
     public class ChatClient '''extends Client'''{
Zeile 64: Zeile 80:
         //Konstruktor der Klasse Client aufrufen!
         //Konstruktor der Klasse Client aufrufen!
         //Dadurch wird die Verbindung zum (selbstprogrammierten) Chatserver hergestellt
         //Dadurch wird die Verbindung zum (selbstprogrammierten) Chatserver hergestellt
         super("192.168.100.100", 4444);
         '''super'''("192.168.100.100", 4444);
       }
       }
 
 
     public void processMessage(String pMessage){
     // Überschreiben der Methode processMessage!
    '''public void processMessage(String pMessage)'''{
       // jetzt das Protokoll abarbeiten  
       // jetzt das Protokoll abarbeiten  
       if(pMessage.startsWith("neu")){
       if(pMessage.startsWith("neu")){
Zeile 76: Zeile 93:
   }
   }
</code>
</code>


= Server-Programmierung =
= Server-Programmierung =
Zeile 82: Zeile 98:
<code>
<code>
   public class ChatServer '''extends Server'''{
   public class ChatServer '''extends Server'''{
     public ChatServer(){
     public ChatServer(int pPort){
       // Konstruktor der Klasse Server aufrufen!
       // Konstruktor der Klasse Server aufrufen!
       // Dadurch wird der Server erzeugt.
       // Dadurch wird der Server erzeugt.
       super(4444);
       '''super'''(pPort);
     }
     }
</code>
</code>


Wichtig ist, dass die Klasse ChatServer dann die folgenden Methoden '''überschreibt''', um auf Ereignisse bei den Clients angemessen reagieren zu können:
Wichtig ist, dass die Klasse ChatServer dann die folgenden Methoden '''überschreibt''', um auf Ereignisse bei den Clients angemessen reagieren zu können:
# <code>public void processMessage(String pClientIP, int pClientPort, int pMessage)</code>
# <code>public void processMessage(String pClientIP, int pClientPort, String pMessage)</code>
# <code>public void processNewConnection(String pClientIP, int pClientPort)</code>
# <code>public void processNewConnection(String pClientIP, int pClientPort)</code>
# <code>public void processClosedConnection(String pClientIP, int pClientPort)</code>
# <code>public void processClosedConnection(String pClientIP, int pClientPort)</code>
Zeile 101: Zeile 117:


= Wichtige Methoden der Klasse String =
= Wichtige Methoden der Klasse String =
Für die Verarbeitung der Nachrichten sind folgende Methoden der Klasse String wichtig:
siehe [[String]]


=Beispiel: GossipServer=
Die Techniken der Netzwerkprogrammierung werden hier am Beispiel des GossipServer gezeigt.


<code>int compareTo(String anotherString) </code>
==Spezifikation==
Der GossipServer soll dazu dienen, Tratsch (=Gossip) zu speichern und weiter zu verbreiten. Es gelten die folgenden Anforderungen:
Gossip schreiben:
* '''Gossip schreiben:''' Man kann die Verbindung zum GossipServer herstellen und (anonym!) eine neue Nachricht hinterlassen. Die wird als Text mit Datum/Uhrzeit gespeichert. Es werden nur Nachrichten in SMS-Länge akzeptiert. Dafür soll auf der Client-Seite eine Klasse '''<code>GossipClient.java</code>''' entwickelt werden.
* '''Gossip abonnieren:''' Mithilfe eines geeigneten Befehls kann man den Gossip abonnieren. Der GossipServer schickt dem Client dann jede neu eintreffende Nachricht. Das soll auf der Client-Seite die Klasse '''<code>GossipTicker.java</code>''' erledigen.
* '''Gossip-Archiv lesen:''' Man kann alle Nachrichten eines Zeitraums vom GossipServer abrufen, indem man einen geeigneten Befehl an den GossipServer schickt und dabei das Anfangs- und das Enddatum angibt. Der GossipServer schickt dann alle Nachrichten aus diesem Zeitraum mit Datum zurück. Dies erledigt auf der Client-Seite die Klasse  '''<code>GossipClient.java</code>'''.
* '''Abmelden:''' Mithilfe eines geeigneten Befehls kann man die Verbindung zum GossipServer trennen.
* '''Gossip speichern und die Abonnentenliste verwalten:''' Der <code>GossipServer</code> speichert die Nachrichten und die Abonnenten in Listen und reagiert auf die o.g. Anforderungen des <code>GossipClient</code> und des <code>GossipTicker</code>.


Vergleicht zwei Strings alphabetisch. Als Ergebnis kommt eine Zahl raus.
==Arbeitsschritte==
   
# Protokoll definieren
Die Zahl ist GRÖSSER 0: Der erste String ist im Alphabet NACH dem zweiten String.
# GossipServer:
       
## Implementationsdiagramm
  Beispiel: int ergebnis = "b".compareTo("a") // ergebnis ist GRÖSSER 0.
## Implementierung
# GossipClient und GossipTicker:
    Die Zahl ist KLEINER 0: Der erste String ist im Alphabet VOR dem zweiten String.
## Entscheidung für programmtechnische Grundlage: Client oder Connection?
 
## Implementationsdiagramme
  Beispiel: int ergebnis = "b".compareTo("bb") // ergebnis ist KLEINER 0.
## Implementierungen


    Die Zahl ist GLEICH 0: Die Strings sind genau gleich.
==Protokoll==


  Beispiel: int ergebnis = "b".compareTo("b") // ergebnis = 0.
 


<code>boolean equals(Object anObject) </code>
{| class="wikitable"
|-
! Client sendet !! Server antwortet<br>(einem Client) !!
Server an alle
|-
|<i>Client meldet sich an</i> || +OK Willkommen beim GossipServer
|-
| <i>Gossip schreiben:</i><br>NEU <nachricht> || +OK Nachricht akzeptiert<br><br>-ERR Nachricht zu lang || <i>an alle Abonennten:</i><br>NEU <zeit>--<nachricht><br>---
|-
| <i>Gossip abonnieren:</i><br>ABO || +OK Abo akzeptiert || ---
|-
| <i>Gossip-Archiv lesen: </i><br>ARCHIV <zeit1> <zeit2>
|| +OK GossipArchiv<br><zeit 1>--<nachricht 1><br>...<br><zeit n>--<nachricht n><br><b>.</b><br><br>-ERR Datum falsch || ---
|-
| QUIT || +OK Tschuess<br><i>Trennt die Verbindung </i> || ---
|-
| <i><unbekannter Befehl></i> || -ERR unbekannter Befehl || ---
|-
| <i><Parameter fehlt></i> || -ERR Parameter fehlt || ---
|}


  Vergleicht zwei Strings.  
==Implementationsdiagramm==
* GossipServer erbt von Server.
* GossipServer muss die Methoden <code>processMessage</code>, <code>processNewConnection</code> und <code>processClosedConnection</code> überschreiben.
** <code>processMessage</code> ist notwendig, um Nachrichten von Clients gemäß Protokoll verarbeiten zu können.
** <code>processNewConnection</code> ist notwendig, um neue Clients begrüßen zu können.
** <code>processClosedConnection</code> ist notwendig, um Clients aus der Abonnentenliste streichen zu können, wenn sie sich abmelden
* GossipServer muss Nachrichten und Abonnenten verwalten können; dafür braucht er jeweils eine Liste.
* Die privaten Methoden sind vorteilhaft, damit die Methode <code>processMessage</code> nicht völlig aufgebläht wird.
[[Datei:Implementationsdiagramm-GossipServer.png]]


  '''NIE == verwenden um Strings zu vergleichen!'''
==Implementierung==
 
<code>
 
'''//GossipServer erbt von Server!'''
<code>boolean equalsIgnoreCase(String anotherString)</code>
<b>public class GossipServer extends Server </b>
 
{
  Hier wird verglichen und Unterschiede bei der Groß- und Kleinschreibung werden ignoriert.  
    private static int port = 5555;
 
    '''//Attribute fuer die Listen'''
 
    private ListWithViewer<Abonnent> abonnentenListe;
<code>String[] split(String trennzeichen) </code>
    private ListWithViewer<Nachricht> nachrichtenListe;
     
 
   Zerlegt einen String in Teile, die dann in einem Array gespeichert werden.
    //Konstruktor
 
    '''public GossipServer()'''
  Beispiel: String[] ergebnis = "Das ist ein Satz".split(" ");
    {
        '''//den Konstruktor der Super-Klasse Server aufrufen'''
        super(port);
        System.out.println("GossipServer wird gestartet");
        '''//die Listen als leere Listen erzeugen'''
        abonnentenListe = new ListWithViewer<Abonnent>();
        nachrichtenListe = new ListWithViewer<Nachricht>();
    }
   
    '''public void processNewConnection(String pClientIP, int pClientPort)'''
    {
        send(pClientIP, pClientPort, "+OK Willkommen beim Gossipserver");
        //weiter ist hier nichts zu tun.
        //denn Abonennten werden erst in die Liste geschrieben, wenn sie sich registriert haben.
    }
   
    '''public void processMessage(String pClientIP, int pClientPort, String pMessage)'''
    {
        if(pMessage.startsWith("NEU")){
            String nachricht = pMessage.substring(4);
            send(pClientIP, pClientPort, "+OK Nachricht akzeptiert");
            nachrichtHinzufuegen(pClientIP, pClientPort, nachricht);
            nachrichtAnAbonnentenWeiterleiten(nachricht);
            return;
        }
        if(pMessage.equals("ABO")){
            send(pClientIP, pClientPort, "+OK Abo akzeptiert");
            abonnentHinzufuegen(pClientIP, pClientPort);
            return;
        }
        if(pMessage.startsWith("ARCHIV")){
            String[] splits = pMessage.split(" ");
            String datumVon = splits[1];
            String datumBis = splits[2];
            sendeNachrichten(pClientIP, pClientPort, datumVon, datumBis);
            return;
        }
        if(pMessage.equals("QUIT")){
            send(pClientIP, pClientPort, "+OK Tschuess");
            closeConnection(pClientIP, pClientPort);
            return;
        }
        // kein Befehl trifft zu!
        send(pClientIP, pClientPort, "-ERR unbekannter Befehl: "+pMessage);
    }
    
    
    Dann hat ergebnis die Länge 4, und in jedem Eintrag von ergebnis steht ein Wort.  
    '''public void processClosingConnection(String pClientIP, int pClientPort)'''
 
    {
        abonnentEntfernen(pClientIP, pClientPort);
    }
   
    '''private void nachrichtHinzufuegen(String pClientIP, int pClientPort, String pNachricht)'''
    {
        Nachricht neueNachricht = new Nachricht(pNachricht);
        this.nachrichtenListe.append(neueNachricht);
    }
   
    '''private void sendeNachrichten(String pClientIP, int pClientPort, String pDatumVon, String pDatumBis)'''
    {
        send(pClientIP, pClientPort, "+OK GossipArchiv");
        for(nachrichtenListe.toFirst(); nachrichtenListe.hasAccess(); nachrichtenListe.next()) {
            Nachricht dieNachricht = nachrichtenListe.getContent();
            if(dieNachricht.getZeit().compareTo(pDatumVon)>= 0 && dieNachricht.getZeit().compareTo(pDatumBis) <= 0){
                send(pClientIP, pClientPort, dieNachricht.getZeit()+" "+dieNachricht.getText());
            }
        }
        this.send(pClientIP, pClientPort, ".");
    }
   
    '''private void abonnentHinzufuegen(String pClientIP, int pClientPort)'''
    {
        Abonnent neuerAbonnent = new Abonnent(pClientIP, pClientPort);
        this.abonnentenListe.append(neuerAbonnent);
    }
   
    '''private void abonnentEntfernen(String pClientIP, int pClientPort)'''
    {
        for(abonnentenListe.toFirst(); abonnentenListe.hasAccess(); abonnentenListe.next()){
            Abonnent derAbonnent = abonnentenListe.getContent();
            if(pClientIP.equals(derAbonnent.getIp())&& pClientPort == derAbonnent.getPort()){
                this.abonnentenListe.remove();
                return;
            }
        }
    }
   
    '''private void nachrichtAnAbonnentenWeiterleiten(String pMessage)'''
    {
        for(abonnentenListe.toFirst(); abonnentenListe.hasAccess(); abonnentenListe.next()){
            Abonnent derAbonnent = this.abonnentenListe.getContent();
            this.send(derAbonnent.getIp(),derAbonnent.getPort(), "+OK NEU "+pMessage);
            abonnentenListe.next();
        }
    }
   
    public static void main(String[] args) {
        GossipServer gs = new GossipServer();
        new GUI(gs);
    }
}
</code>


<code>boolean startsWith(String prefix) </code>
==GossipClient / GossipTicker==
Auf der Client-Seite gibt es zwei Anwendungen:
* Der '''GossipClient''' kann Nachrichten an den GossipServer schicken und mit dem Befehl ''ARCHIV'' Nachrichten vom Server abrufen.
* Der '''GossipTicker''' kann Nachrichten abonnieren; er bekommt dann jede neue Nachricht geschickt.


   Testet, ob der String mit einem bestimmten prefix anfängt.  
===Entscheidung: Client.java oder Connection.java?===
* Die Klasse '''GossipClient''' benutzt für die Verbindung zum Server am besten die Klasse '''Connection.java'''. Denn hier findet die Kommunikation nur in '''request-response-Zyklen''' statt. GossipClient '''hat ein Attribut''' vom Typ Connection.
* Die Klasse '''GossipTicker''' benutzt für die Verbindung zum Server am besten die Klasse '''Client.java'''. Denn der GossipServer wird von sich aus aktiv und mit der Klasse Client.java kann der GossipTicker die eintreffenden Nachrichten '''nebenläufig''' verarbeiten (d.h. er kann nebenbei noch andere Dinge tun). Wenn man hier die Klasse Connection.java verwenden würde, dann wäre der GossipClient immer komplett blockiert, während er auf eine neue Nachricht vom Server wartet.  GossipTicker '''erbt''' von der Klasse Client.


  Ist wichtig, um bei der Auswertung von Protokollen die Befehle abzufragen.
===Implementierung GossipClient===
 
Hier ist nur der Teil des GossipClients implementiert, der mithilfe des Befehls ''ARCHIV'' Nachrichten vom Server abruft.
<code>String substring(int beginIndex) </code>
<code>
import javax.swing.JOptionPane;
public class GossipClient {
    private Connection verbindung;
   
    public GossipClient(String pIp, int pPort){
        // verbindung mit pIp und pPort initialisieren
        verbindung = new Connection(pIp, pPort);
        String willkommen = verbindung.receive();
        if(willkommen.equals("+OK Willkommen beim Gossipserver")){
            nachrichtenAbrufen();
        }
        else{
            System.err.println(willkommen);
        }
        // verbindung schliessen
        verbindung.close();
    }
   
    public void nachrichtenAbrufen(){
        String vonZeit = JOptionPane.showInputDialog("Start-Zeit: ", "2013-03-21--10:00:30");
        String bisZeit = JOptionPane.showInputDialog("End-Zeit: ", "2013-04-21--11:00:30");
        //TODO gemaess Protokoll an den Server schicken
        String message = "ARCHIV "+vonZeit+" "+bisZeit;
        verbindung.send(message);
        //TODO Antwort auslesen und an ergebnis anhängen
        ListWithViewer ergebnis = new ListWithViewer();
        String antwort = verbindung.receive();
        if(antwort.equals("+OK GossipArchiv")){
            antwort = verbindung.receive();
            while(!antwort.equals(".")){
                String[] splits = antwort.split(" ");
                // die Zeit der Nachricht ist in splits[0]
                // der Text der Nachricht ist in splits[1]
                ergebnis.append(splits[1]);
                antwort = verbindung.receive();
            }
            return;
        }
        else{
            System.err.println(antwort);
            return;
        }
    }
   
    public static void main(String[] args) {
        new GossipClient("127.0.0.1", 5555);
    }
}
</code>


   Gibt einen Substring zurück, der bei dem Buchstaben beginIndex anfängt.
===Implementierung GossipTicker===
Hier wird vorausgesetzt, dass es eine Klasse  <code>GossipTickerGUI</code> gibt, die über eine Methode <code>nachrichtAnzeigen(String pText)</code> verfügt.


  Beispiel: String nachricht = "ANALLE hallo".substring(7);
<code>
public class GossipTicker extends Client{
    private GossipTickerGUI gui;
    public GossipTicker(String pServerIP, int pServerPort){
        super(pServerIP, pServerPort);
        gui = new GossipTickerGUI();
    }
    public void processMessage(String pMessage) {
        if(pMessage.equals("+OK Willkommen beim Gossipserver")){
            this.send("ABO");
            return;
        }
        if(pMessage.startsWith("+OK NEU")){
            String text = pMessage.substring(8);
            gui.nachrichtAnzeigen(text);
            return;
        }
    }
   
    public static void main(String[] args) {
        GossipTicker gt = new GossipTicker("127.0.0.1", 5555);
    }
}
</code>


  nachricht enthält jetzt "hallo". (Die Buchstaben werden von 0 anfangend nummeriert!)
===Implementierung Abonnent und Nachricht===
* Die Klasse '''Abonnent''' ist eine reine Container-Klasse: Sie dient nur dazu, die Informationen zu einem Abonnenten (d.h. seine Ip und seinen Port) zu speichern.
* In der Klasse '''Nachricht''' wird der Nachrichtentext, das Datum und die Uhrzeit gespeichert. Datum und Uhrzeit werden bei der Erzeugung der Nachricht vom System ausgelesen.


'''Klasse Abonnent:'''
<code>
  public class Abonnent {
    String ip;
    int port;
   
    public Abonnent(String pIp, int pPort) {
        super();
        ip = pIp;
        port = pPort;
    }
   
    public String getIp() {
        return ip;
    }
 
    public int getPort() {
        return port;
    }
    // fuer die Anzeige im ListWithViewer!
    public String toString(){
        return ip+"::"+port;
    }
}
</code>


<code>String substring(int beginIndex, int endIndex) </code>
'''Klasse Nachricht'''
 
<code>
     s.o.; es wird zusätzlich der letzte Buchstabe festgelegt.
  // zu den Import-Statements: vgl. Java Tricks auf sibi-wiki.de
  import java.text.SimpleDateFormat;
  import java.util.Date;
  public class Nachricht {
    private String datum;
    private String uhrzeit;
    private String text;
 
    public Nachricht(String pText){
        this.text = pText;
        // Datum und Uhrzeit bestimmen: Vgl. Java Tricks auf sibi-wiki.de
        Date dasDatum = new Date();
        SimpleDateFormat formatDatum = new SimpleDateFormat("yyyy-MM-dd");
        String datumString = formatDatum.format(dasDatum);
        datum = datumString;
        SimpleDateFormat formatUhrzeit = new SimpleDateFormat("HH:mm");
        String uhrzeitString = formatUhrzeit.format(dasDatum);
        uhrzeit = uhrzeitString;
    }
    public String getZeit() {
        return (datum+" "+uhrzeit);
    }
    public String getText() {
        return text;
    }
 
    // fuer die Anzeige in ListWithViewer!
    public String toString(){
        return datum+" "+uhrzeit+":"+text;
    } 
  }
</code>

Aktuelle Version vom 16. März 2021, 07:18 Uhr

im Abi 2021 und später nicht mehr relevant für Abiturklausuren

Allgemeines

Netzwerkprogrammierung beschäftigt sich mit der Entwicklung von verteilten Anwendungen.

Dabei kommuniziert zumeist ein Server mit mehreren Clients.

Schnittstellen des Zentralabitur

Best Practices für Netzwerk-Protokolle

siehe: Protokoll_(IF)

Client-Programmierung

Für die Client-Programmierung stehen im Zentralabitur die Schnittstellen Connection.java und Client.java zur Verfügung.

Connection.java vs. Client.java

Die Schnittstellen fürs Zentralabitur stellen zwei Klassen zur Verfügung, mit denen auf der Client-Seite gearbeitet werden kann:

  • Connection.java hat die folgenden Methode, um Nachrichten vom Server zu empfangen:
    • public String receive()
  • Client.java hat die folgenden Methode, um Nachrichten vom Server zu empfangen:
    • public void processMessage(String pMessage)

Die einfachere (=mit weniger Möglichkeiten) ist die Klasse Connection.java. Diese beiden Klassen werden in unterschiedlichen Szenarien benutzt:

  • Connection.java wird genutzt, wenn der Server nur auf Anfragen des Clients reagiert, wie z.B. bei Mailprotokollen (SMTP oder POP3).
  • Client.java wird genutzt, wenn der Server von sich aus aktiv werden kann. Das ist z.B. bei einem Chat der Fall, denn hier muss der Server Nachrichten eines anderen Client von sich aus an alle Clients weiterleiten können.

Connection: Standardvorgehen

In der Regel wird wie folgt vorgegangen:

  1. Verbindung herstellen:
    1. Man verbindet sich mit dem Server, indem man ein neues Connection-Objekt erzeugt.
    2. Man empfängt die Willkommensnachricht des Servers und wertet sie ggf. aus.
  2. Einfache Nachrichten senden/empfangen:
    1. Man baut einen String zumServer zusammen. In zumServer ist die Nachricht enthalten, die man zum Server schicken möchte.
    2. Man gibt zumServer an die Konsole aus - zur Kontrolle!
    3. Man schickt zumServer an den Server.
    4. Man empfängt die Antwort des Servers und speichert sie in vomServer.
    5. Man gibt vomServer an die Konsole aus - zur Kontrolle!
    6. Jetzt kann man vomServer auswerten.
  3. Mehrzeilige Nachrichten empfangen: Wenn eine Serverantwort mehrzeilig ist, dann wird sie in der Regel mit einem einzelnen Punkt (bzw. einer anderen Endmarke) beendet. Darauf reagiert man wie folgt:
    1. Man empfängt eine Zeile vom Server und speichert sie in vomServer.
    2. Eine while-Schleife, die so lange läuft, bis vomServer ein einzelner Punkt (bzw. die Endmarke) ist. In der Schleife passiert folgendes...
      1. Man gibt vomServer an die Konsole aus - zur Kontrolle, ob die Antwort vom Server wie erwartet ist.
      2. vomServer auswerten.
      3. Man empfängt die nächste Zeile vom Server und speichert sie in vomServer.

Connection: Standardvorgehen in Java

  1. Verbindung herstellen:
    1. Connection verbindung = new Connection("192.168.100.100", 4242); //IP und Port sind nur Beispiele!
    2. String vomServer = verbindung.receive();
  2. Einfache Nachrichten senden/empfangen:
    1. String zumServer = "LOGIN "+username+" "+passwort; // das ist ein Beispiel für ein Login
    2. System.out.println(zumServer);
    3. verbindung.send(zumServer);
    4. vomServer = verbindung.receive();.
    5. System.out.println(vomServer);
    6. if(vomServer.startsWith("+OK")){ //Beispiel für eine Auswertung
  3. Mehrzeilige Nachrichten empfangen:
    1. vomServer = verbindung.receive();
    2. while(vomServer.equals(".") == false){
      1. System.out.println(vomServer);
      2. ergebnis.append(vomServer); // Beispiel für eine Auswertung: An eine ergebnis-Liste anhängen.
      3. vomServer = verbindung.receive();.

Client.java: Standardvorgehen in Java

Von Client.java dagegen wird eine Unterklasse gebildet, die die Fähigkeiten von Client.java erbt. Dabei muss die Methode public void processMessage(String pMessage) überschrieben werden, damit man auf die Nachrichten des Servers reagieren kann.

Beispiel ChatClient:

   public class ChatClient extends Client{
     public ChatClient(){
       //Konstruktor der Klasse Client aufrufen!
       //Dadurch wird die Verbindung zum (selbstprogrammierten) Chatserver hergestellt
       super("192.168.100.100", 4444);
     }
 
   // Überschreiben der Methode processMessage!
   public void processMessage(String pMessage){
     // jetzt das Protokoll abarbeiten 
     if(pMessage.startsWith("neu")){
        // usw.
     }
     // usw.
   }
 }

Server-Programmierung

Für die Server-Programmierung gibt es die Klasse Server.java. Genauso wie bei Client.java muss die eigene Klasse von Server.java erben, um die Fähigkeiten von Server.java zu übernehmen, z.B.:

 public class ChatServer extends Server{
   public ChatServer(int pPort){
     // Konstruktor der Klasse Server aufrufen!
     // Dadurch wird der Server erzeugt.
     super(pPort);
   }

Wichtig ist, dass die Klasse ChatServer dann die folgenden Methoden überschreibt, um auf Ereignisse bei den Clients angemessen reagieren zu können:

  1. public void processMessage(String pClientIP, int pClientPort, String pMessage)
  2. public void processNewConnection(String pClientIP, int pClientPort)
  3. public void processClosedConnection(String pClientIP, int pClientPort)

Von sich aus kann der Server aktiv werden, indem er...

  1. einem Client eine Nachricht schickt: public void send(String pClientIP, int pClientPort, String pMessage)
  2. allen Clients eine Nachricht schickt: public void sendToAll(String pMessage)
  3. einen Client rausschmeißt: public void closeConnection(String pClientIP, int pClientPort)
  4. den Server zumacht: public void close()

Wichtige Methoden der Klasse String

siehe String

Beispiel: GossipServer

Die Techniken der Netzwerkprogrammierung werden hier am Beispiel des GossipServer gezeigt.

Spezifikation

Der GossipServer soll dazu dienen, Tratsch (=Gossip) zu speichern und weiter zu verbreiten. Es gelten die folgenden Anforderungen: Gossip schreiben:

  • Gossip schreiben: Man kann die Verbindung zum GossipServer herstellen und (anonym!) eine neue Nachricht hinterlassen. Die wird als Text mit Datum/Uhrzeit gespeichert. Es werden nur Nachrichten in SMS-Länge akzeptiert. Dafür soll auf der Client-Seite eine Klasse GossipClient.java entwickelt werden.
  • Gossip abonnieren: Mithilfe eines geeigneten Befehls kann man den Gossip abonnieren. Der GossipServer schickt dem Client dann jede neu eintreffende Nachricht. Das soll auf der Client-Seite die Klasse GossipTicker.java erledigen.
  • Gossip-Archiv lesen: Man kann alle Nachrichten eines Zeitraums vom GossipServer abrufen, indem man einen geeigneten Befehl an den GossipServer schickt und dabei das Anfangs- und das Enddatum angibt. Der GossipServer schickt dann alle Nachrichten aus diesem Zeitraum mit Datum zurück. Dies erledigt auf der Client-Seite die Klasse GossipClient.java.
  • Abmelden: Mithilfe eines geeigneten Befehls kann man die Verbindung zum GossipServer trennen.
  • Gossip speichern und die Abonnentenliste verwalten: Der GossipServer speichert die Nachrichten und die Abonnenten in Listen und reagiert auf die o.g. Anforderungen des GossipClient und des GossipTicker.

Arbeitsschritte

  1. Protokoll definieren
  2. GossipServer:
    1. Implementationsdiagramm
    2. Implementierung
  3. GossipClient und GossipTicker:
    1. Entscheidung für programmtechnische Grundlage: Client oder Connection?
    2. Implementationsdiagramme
    3. Implementierungen

Protokoll

Client sendet Server antwortet
(einem Client)

Server an alle

Client meldet sich an +OK Willkommen beim GossipServer
Gossip schreiben:
NEU <nachricht>
+OK Nachricht akzeptiert

-ERR Nachricht zu lang
an alle Abonennten:
NEU <zeit>--<nachricht>
---
Gossip abonnieren:
ABO
+OK Abo akzeptiert ---
Gossip-Archiv lesen:
ARCHIV <zeit1> <zeit2>
+OK GossipArchiv
<zeit 1>--<nachricht 1>
...
<zeit n>--<nachricht n>
.

-ERR Datum falsch
---
QUIT +OK Tschuess
Trennt die Verbindung
---
<unbekannter Befehl> -ERR unbekannter Befehl ---
<Parameter fehlt> -ERR Parameter fehlt ---

Implementationsdiagramm

  • GossipServer erbt von Server.
  • GossipServer muss die Methoden processMessage, processNewConnection und processClosedConnection überschreiben.
    • processMessage ist notwendig, um Nachrichten von Clients gemäß Protokoll verarbeiten zu können.
    • processNewConnection ist notwendig, um neue Clients begrüßen zu können.
    • processClosedConnection ist notwendig, um Clients aus der Abonnentenliste streichen zu können, wenn sie sich abmelden
  • GossipServer muss Nachrichten und Abonnenten verwalten können; dafür braucht er jeweils eine Liste.
  • Die privaten Methoden sind vorteilhaft, damit die Methode processMessage nicht völlig aufgebläht wird.

Implementationsdiagramm-GossipServer.png

Implementierung

//GossipServer erbt von Server!
public class GossipServer extends Server 
{
   private static int port = 5555;
   //Attribute fuer die Listen
   private ListWithViewer<Abonnent> abonnentenListe;
   private ListWithViewer<Nachricht> nachrichtenListe;
  
   //Konstruktor
   public GossipServer()
   {
       //den Konstruktor der Super-Klasse Server aufrufen
       super(port);
       System.out.println("GossipServer wird gestartet");
       //die Listen als leere Listen erzeugen
       abonnentenListe = new ListWithViewer<Abonnent>();
       nachrichtenListe = new ListWithViewer<Nachricht>();
   }
   
   public void processNewConnection(String pClientIP, int pClientPort)
   {
       send(pClientIP, pClientPort, "+OK Willkommen beim Gossipserver");
       //weiter ist hier nichts zu tun.
       //denn Abonennten werden erst in die Liste geschrieben, wenn sie sich registriert haben.
   }
   
   public void processMessage(String pClientIP, int pClientPort, String pMessage)
   {
       if(pMessage.startsWith("NEU")){
           String nachricht = pMessage.substring(4);
           send(pClientIP, pClientPort, "+OK Nachricht akzeptiert");
           nachrichtHinzufuegen(pClientIP, pClientPort, nachricht);
           nachrichtAnAbonnentenWeiterleiten(nachricht);
           return;
       }
       if(pMessage.equals("ABO")){
           send(pClientIP, pClientPort, "+OK Abo akzeptiert");
           abonnentHinzufuegen(pClientIP, pClientPort);
           return;
       }
       if(pMessage.startsWith("ARCHIV")){
           String[] splits = pMessage.split(" ");
           String datumVon = splits[1];
           String datumBis = splits[2];
           sendeNachrichten(pClientIP, pClientPort, datumVon, datumBis);
           return;
       }
       if(pMessage.equals("QUIT")){
           send(pClientIP, pClientPort, "+OK Tschuess");
           closeConnection(pClientIP, pClientPort);
           return;
       }
       // kein Befehl trifft zu!
       send(pClientIP, pClientPort, "-ERR unbekannter Befehl: "+pMessage);
   }
  
   public void processClosingConnection(String pClientIP, int pClientPort)
   {
       abonnentEntfernen(pClientIP, pClientPort);
   }
   
   private void nachrichtHinzufuegen(String pClientIP, int pClientPort, String pNachricht)
   {
       Nachricht neueNachricht = new Nachricht(pNachricht);
       this.nachrichtenListe.append(neueNachricht);
   }
   
   private void sendeNachrichten(String pClientIP, int pClientPort, String pDatumVon, String pDatumBis)
   {
       send(pClientIP, pClientPort, "+OK GossipArchiv");
       for(nachrichtenListe.toFirst(); nachrichtenListe.hasAccess(); nachrichtenListe.next()) {
           Nachricht dieNachricht = nachrichtenListe.getContent();
           if(dieNachricht.getZeit().compareTo(pDatumVon)>= 0 && dieNachricht.getZeit().compareTo(pDatumBis) <= 0){
               send(pClientIP, pClientPort, dieNachricht.getZeit()+" "+dieNachricht.getText());
           }
       }
       this.send(pClientIP, pClientPort, ".");
   }
   
   private void abonnentHinzufuegen(String pClientIP, int pClientPort)
   {
       Abonnent neuerAbonnent = new Abonnent(pClientIP, pClientPort);
       this.abonnentenListe.append(neuerAbonnent);
   }
   
   private void abonnentEntfernen(String pClientIP, int pClientPort)
   {
       for(abonnentenListe.toFirst(); abonnentenListe.hasAccess(); abonnentenListe.next()){
           Abonnent derAbonnent = abonnentenListe.getContent();
           if(pClientIP.equals(derAbonnent.getIp())&& pClientPort == derAbonnent.getPort()){
               this.abonnentenListe.remove();
               return;
           }
       }
   }
   
   private void nachrichtAnAbonnentenWeiterleiten(String pMessage)
   {
       for(abonnentenListe.toFirst(); abonnentenListe.hasAccess(); abonnentenListe.next()){
           Abonnent derAbonnent = this.abonnentenListe.getContent();
           this.send(derAbonnent.getIp(),derAbonnent.getPort(), "+OK NEU "+pMessage);
           abonnentenListe.next();
       }
   }
   
   public static void main(String[] args) {
       GossipServer gs = new GossipServer();
       new GUI(gs);
   }
}

GossipClient / GossipTicker

Auf der Client-Seite gibt es zwei Anwendungen:

  • Der GossipClient kann Nachrichten an den GossipServer schicken und mit dem Befehl ARCHIV Nachrichten vom Server abrufen.
  • Der GossipTicker kann Nachrichten abonnieren; er bekommt dann jede neue Nachricht geschickt.

Entscheidung: Client.java oder Connection.java?

  • Die Klasse GossipClient benutzt für die Verbindung zum Server am besten die Klasse Connection.java. Denn hier findet die Kommunikation nur in request-response-Zyklen statt. GossipClient hat ein Attribut vom Typ Connection.
  • Die Klasse GossipTicker benutzt für die Verbindung zum Server am besten die Klasse Client.java. Denn der GossipServer wird von sich aus aktiv und mit der Klasse Client.java kann der GossipTicker die eintreffenden Nachrichten nebenläufig verarbeiten (d.h. er kann nebenbei noch andere Dinge tun). Wenn man hier die Klasse Connection.java verwenden würde, dann wäre der GossipClient immer komplett blockiert, während er auf eine neue Nachricht vom Server wartet. GossipTicker erbt von der Klasse Client.

Implementierung GossipClient

Hier ist nur der Teil des GossipClients implementiert, der mithilfe des Befehls ARCHIV Nachrichten vom Server abruft.

import javax.swing.JOptionPane;

public class GossipClient {
   private Connection verbindung;
   
   public GossipClient(String pIp, int pPort){
       // verbindung mit pIp und pPort initialisieren
       verbindung = new Connection(pIp, pPort);
       String willkommen = verbindung.receive();
       if(willkommen.equals("+OK Willkommen beim Gossipserver")){
           nachrichtenAbrufen();
       }
       else{
           System.err.println(willkommen);
       }
       // verbindung schliessen
       verbindung.close();
   }
   
   public void nachrichtenAbrufen(){
       String vonZeit = JOptionPane.showInputDialog("Start-Zeit: ", "2013-03-21--10:00:30");
       String bisZeit = JOptionPane.showInputDialog("End-Zeit: ", "2013-04-21--11:00:30");
       //TODO gemaess Protokoll an den Server schicken
       String message = "ARCHIV "+vonZeit+" "+bisZeit;
       verbindung.send(message);
       //TODO Antwort auslesen und an ergebnis anhängen
       ListWithViewer ergebnis = new ListWithViewer();
       String antwort = verbindung.receive();
       if(antwort.equals("+OK GossipArchiv")){
           antwort = verbindung.receive();
           while(!antwort.equals(".")){
               String[] splits = antwort.split(" ");
               // die Zeit der Nachricht ist in splits[0]
               // der Text der Nachricht ist in splits[1]
               ergebnis.append(splits[1]);
               antwort = verbindung.receive();
           }
           return;
       }
       else{
           System.err.println(antwort);
           return;
       }
   }
   
   public static void main(String[] args) {
       new GossipClient("127.0.0.1", 5555);
   }
}

Implementierung GossipTicker

Hier wird vorausgesetzt, dass es eine Klasse GossipTickerGUI gibt, die über eine Methode nachrichtAnzeigen(String pText) verfügt.

public class GossipTicker extends Client{
   private GossipTickerGUI gui;

   public GossipTicker(String pServerIP, int pServerPort){
       super(pServerIP, pServerPort);
       gui = new GossipTickerGUI();
   }

   public void processMessage(String pMessage) {
       if(pMessage.equals("+OK Willkommen beim Gossipserver")){
           this.send("ABO");
           return;
       }
       if(pMessage.startsWith("+OK NEU")){
           String text = pMessage.substring(8);
           gui.nachrichtAnzeigen(text);
           return;
       }
   }
   
   public static void main(String[] args) {
       GossipTicker gt = new GossipTicker("127.0.0.1", 5555);
   }
}

Implementierung Abonnent und Nachricht

  • Die Klasse Abonnent ist eine reine Container-Klasse: Sie dient nur dazu, die Informationen zu einem Abonnenten (d.h. seine Ip und seinen Port) zu speichern.
  • In der Klasse Nachricht wird der Nachrichtentext, das Datum und die Uhrzeit gespeichert. Datum und Uhrzeit werden bei der Erzeugung der Nachricht vom System ausgelesen.

Klasse Abonnent:

 public class Abonnent {
   String ip;
   int port;
   
   public Abonnent(String pIp, int pPort) {
       super();
       ip = pIp;
       port = pPort;
   }
   
   public String getIp() {
       return ip;
   }
  
   public int getPort() {
       return port;
   }

   // fuer die Anzeige im ListWithViewer!
   public String toString(){
       return ip+"::"+port;
   }

}

Klasse Nachricht

 // zu den Import-Statements: vgl. Java Tricks auf sibi-wiki.de
 import java.text.SimpleDateFormat;
 import java.util.Date;

 public class Nachricht {
   private String datum;
   private String uhrzeit;
   private String text;
  
   public Nachricht(String pText){
       this.text = pText;

       // Datum und Uhrzeit bestimmen: Vgl. Java Tricks auf sibi-wiki.de
       Date dasDatum = new Date();

       SimpleDateFormat formatDatum = new SimpleDateFormat("yyyy-MM-dd");
       String datumString = formatDatum.format(dasDatum);
       datum = datumString;

       SimpleDateFormat formatUhrzeit = new SimpleDateFormat("HH:mm");
       String uhrzeitString = formatUhrzeit.format(dasDatum);
       uhrzeit = uhrzeitString;
   }

   public String getZeit() {
       return (datum+" "+uhrzeit);
   }

   public String getText() {
       return text;
   }
  
   // fuer die Anzeige in ListWithViewer!
   public String toString(){
       return datum+" "+uhrzeit+":"+text;
   }   
 }