communityWir suchen ständig neue Tutorials und Artikel! Habt ihr selbst schonmal einen Artikel verfasst und seid bereit dieses Wissen mit der Community zu teilen? Oder würdet ihr gerne einmal über ein Thema schreiben das euch besonders auf dem Herzen liegt? Dann habt ihr nun die Gelegenheit eure Arbeit zu veröffentlichen und den Ruhm dafür zu ernten. Schreibt uns einfach eine Nachricht mit dem Betreff „Community Articles“ und helft mit das Angebot an guten Artikeln zu vergrößern. Als Autor werdet ihr für den internen Bereich freigeschaltet und könnt dort eurer literarischen Ader freien Lauf lassen.

Battleship - Netzwerkprogrammierung in Java Drucken E-Mail
Benutzerbewertung: / 87
SchwachPerfekt 
Sonntag, den 10. Mai 2009 um 00:00 Uhr
Beitragsseiten
Battleship
Beginn des Projekts
Model
Netzwerkprogrammierung in Java
Netzwerkprotokolle
Routing
View
Computerspieler
Controller
Alle Seiten

Netzwerkprogrammierung in Java

Viele moderne Anwendungen in der Softwareindustrie kommen heute kaum noch ohne Netzwerkfunktionalitäten aus. Kenntnisse zur Netzwerkprogrammierung gehören heutzutage zu den Grundlagen eines ambitionierten Software-Entwicklers. Dank guter Bibliotheken ist es kein allzu kompliziertes Unterfangen mehr, Ihre Software mit einer Netzwerkfunktionalität auszustatten.

Die meisten Daten laufen heute weltweit durch Netzwerke auf Basis des Internet Protokolls (bekannter ist die Protocol-Suite TCP/IP). Das IP teilt die zu versendenden Daten in Pakete auf: Im sogenannten Header stehen Quelle und Ziel des Pakets (IP-Adresse und Port). Der Körper enthält die eigentlichen Daten. Diese Pakete werden dann an einen Router im Netz geschickt. Der Router bestimmt dann anhand der Daten im Header den momentan kürzesten Weg zum Empfänger und sendet das Paket weiter an den nächsten Router entlang diese Weges. Dies wiederholt sich solange, bis ein letzter Router eine direkte Verbindung zum Zielrechner hat und ihm die Daten zustellt.

Die hohen Anforderungen, die die Benutzung eines komplizierten Protokoll-Verbunds wie TCP/IP an den Programmierer einer Netzwerk- Anwendung stellt, führte schließlich, zunächst unter Unix, zur Entwicklung des Socket-Konzepts. Dabei kapseln die Sockets alle TCP- und IP-Verwaltungsaufgaben, so dass der Nutzer nur seine Daten an den Socket übergibt und dieser sie versendet und auf der anderen Seite ein weiterer Socket die Pakete empfängt und verarbeitet und nur die rohen Daten an den Nutzer weitergibt. Ein solches Konzept findet sich heute auf praktisch jedem TCP/IP-fähigen System.

Die Programmiersprache Java unterstützt sowohl TCP- als auch UDP-Sockets, andere vom Internet Protocol vorgesehene Paketformate, wie etwa die für Ping benutzten ICMP-Pakete, kann man mit reinem Java (derzeit) nicht erzeugen.

Eine Netzwerkbibliothek entwerfen

Obwohl Java viele fertige Klassen für den Umgang mit Sockets zur Verfügung stellt, bleibt dem ambitionierten Softwareentwickler eine intensivere Einarbeitung in die Netzwerkprogrammierung nicht erspart. Moderne Anwendungen nutzen umfangreiche Bibliotheken und Protokolle, die es der Anwendung erlauben mit anderen Programmen über Netzwerk zu kommunizieren.

Auch unser Spiel soll in der Lage sein mit anderen Programmen im Netzwerk zu kommunizieren, schließlich war eine Anforderung die Implementierung eines Chat. Eventuell möchten wir unsere Anwendung zu einem späteren Zeitpunkt auch erweitern, so dass Spieler online gegeneinander spielen können. Dafür brauchen wir ein solides Fundament in unserer Softwarearchitektur. Dieses Fundament basiert auf einer soliden Netzwerkbibliothek, die als Bestandteil des Models Daten aus dem Netzwerk verarbeitet.

Die Gestaltung einer Netzwerkbibliothek erfordert gewisse Kenntnisse. Dem Entwickler müssen die Grundlagen von TCP/IP bekannt sein. Darüberhinaus sollte man ein wenig Erfahrung mit Netzwerkprotokollen haben. Eine sehr bekannte Kommunikationsstruktur im Internet ist P2P.

Peer-to-Peer (P2P)

Peer-to-Peer (P2P) bezeichnet eine Kommunikation unter gleichen Computern. In einem reinen Peer-to-Peer-Netz sind alle Computer gleichberechtigt und können sowohl Dienste in Anspruch nehmen als auch Dienste zur Verfügung stellen. Man spricht deshalb auch von einem Client-Server-Modell, da ein Peer beide Aufgaben zugleich übernimmt. Die Computer in diesem Netzwerk können als Arbeitsstationen genutzt werden, aber auch Aufgaben im Netz übernehmen. Mittels der Such-Operation können die Peers nach Objekten im Netzwerk suchen, die gewisse Kriterien erfüllen. Alle Peers bilden zusammen ein engmaschiges Netz, so dass Daten direkt untereinander, zwischen den Peers, ausgetauscht werden können.

p2p

In einem Peer-to-Peer Netz weisen alle Klienten eine eindeutige Identifikationsnummer auf, zumeist setzt sich diese Nummer aus der IP-Adresse und der Port-Nummer zusammen.

Sockets

Sockets sind die elementaren Bausteine einer Netzwerkanwendung. Ein Socket ist ein Endpunkt einer bi-direktionalen Software-Schnittstelle zur Interprozess- (IPC) oder Netzwerk-Kommunikation zwischen zwei Programmen. Ein Socket ist gebunden an eine Port-Nummer, so dass die TCP Schicht die Anwendung identifizieren kann für die die Informationen bestimmt sind.

Ein Socket kann man sich als "Steckdose" vorstellen. Maschinen können über diese Steckdose Zugang zum Netz erhalten. Im Prinzip ist ein Socket eine Abstraktion die es einer Anwendung ermöglicht sich in ein bestehendes Netzwerk einzuklinken um mit anderen Applikationen zu kommunizieren.

Auch unsere Anwendung basiert auf Sockets und deshalb beginnen wir auch bei der Implementierung geeigneter Socketklassen. Java stellt eine Standardsocketklasse, namens Socket, zur Verfügung. Wir wollen diese Klasse zunächst kapseln, so dass wir später in der Lage sind eigene Erweiterungen hinzuzufügen. Die Klasse StandardSocket implementiert ein einfaches Interface, welches elementare Methoden definiert.

public class StandardSocket implements SocketInterface 
{  
    /**
     * Creates a stream socket and connects it to the specified port number on 
     * the named host.
     * 
     * @param host  the host name, or null for the loopback address
     * @param port  the port number
     * @throws java.io.IOException  if an I/O error occurs when creating the socket
     * @throws java.net.UnknownHostException    if the IP address of the host could not be determined
     */
    public StandardSocket(String host, int port) 
            throws IOException, UnknownHostException 
    {
        this( new Socket( host, port ) );
    }
 
    /**
     * Encapsulates a normal Java API Socket object.
     * 
     * @param socket an already-open socket connection
     * @throws IOException
     */
    public StandardSocket(Socket socket) throws IOException
    {
        s_ = socket;
        is_ = s_.getInputStream();
        os_ = s_.getOutputStream();        
    }   
 
    /**
     * Creates a stream socket and connects it to the specified port number on 
     * the named host.
     * 
     * @param address  the IP address
     * @param port  the port number
     * @throws java.io.IOException  if an I/O error occurs when creating the socket
     * @throws java.net.UnknownHostException    if the IP address of the host could not be determined
     */
    public StandardSocket(InetAddress address, int port)
            throws IOException, UnknownHostException
    {
        this( new Socket( address, port ) );
    }
 
    /**
     * Closes all streams of this socket.
     * 
     * @throws java.io.IOException
     */
    @Override
    public void close() throws IOException 
    {
        is_.close();
        os_.close();
        s_.close();
    }
 
    /**
     * Reads the next byte of data from the input stream of this socket.
     *
     * @return  the next byte of data, or -1 if the end of the stream is reached.
     * @throws java.io.IOException
     */
    @Override
    public int read() throws IOException
    {
        return is_.read();
    }
 
    /**
     * Reads some number of bytes from the input stream of this socket
     * and stores them into the buffer array b.
     * 
     * @param b the buffer into which the data is read.
     * @return  the total number of bytes read into the buffer, or -1 is there 
     *          is no more data because the end of the stream has been reached.
     * @throws java.io.IOException
     */
    @Override
    public int read(byte[] b) throws IOException
    {
        return is_.read( b );
    }
 
    /**
     * Writes b.length bytes from the specified byte array to the
     * output stream of this socket.
     * 
     * @param b
     * @throws java.io.IOException
     */
    @Override
    public void write(byte[] b) throws IOException
    {
        os_.write( b );
        os_.flush();
    }
 
    /** The socket object */
    private Socket s_;
 
    /** The input stream */
    private InputStream is_;
 
    /** The output stream */
    private OutputStream os_;
}

Da wir die Sockets dynamisch generieren wollen, entwerfen wir eine entsprechende Fabrikmethode. Der Begriff Fabrikmethode oder Factory Method bezeichnet ein Entwurfsmuster aus dem Bereich der Softwareentwicklung. Das Muster beschreibt, wie ein Objekt durch Aufruf einer Methode anstatt durch direkten Aufruf eines Konstruktors erzeugt wird. Es gehört somit zur Kategorie der Erzeugungsmuster.

Auch wir wollen über eine Methode einen eigenen Socket erzeugen und definieren dazu die Klasse StandardSocketFactory, die sich von der abstrakten Klasse SocketFactory ableitet.

public class StandardSocketFactory extends SocketFactory 
{
    /**
     * Generates a new socket object.
     * 
     * @param host  a host nameor ip-address
     * @param port  the port number
     * @return  a new standard socket object
     * @throws java.io.IOException
     * @throws java.net.UnknownHostException
     */
    @Override
    public SocketInterface makeSocket(String host, int port)
            throws IOException, UnknownHostException
    {
        return new StandardSocket( host, port );
    }
 
    /**
     * Generates a new socket object from an existing socket.
     * 
     * @param socket    the initial socket
     * @return  a new standard socket object
     * @throws java.io.IOException
     */
    @Override
    public SocketInterface makeSocket(Socket socket) throws IOException 
    {
        return new StandardSocket( socket );
    }
 
    /**
     * Generates a new socket object.
     *
     * @param address  the IP address
     * @param port  the port number
     * @return  a new standard socket object
     * @throws java.io.IOException
     * @throws java.net.UnknownHostException
     */
    @Override
    public SocketInterface makeSocket(InetAddress address, int port)
            throws IOException, UnknownHostException
    {
        return new StandardSocket( address, port );
    }
}

Die Klasse StandardSocket kapselt lediglich die Java Socket-Klasse. Mit der Methode makeSocket() aus der Klasse SocketFactory lässt sich ein neues Socketobjekt erzeugen.

Wir wollen unserer Anwendung zusätzlich eine etwas ausgefeiltere Klasse zur Verfügung stellen. Daten die über ein Netzwerk übertragen werden verbrauchen natürlich eine bestimmte Bandbreite. Werden viele Daten übertragen, kann sich das spürbar auf die Performance auswirken. Darüberhinaus verfügt nicht jeder Benutzer über eine starke Internetanbindung, so dass es an dieser Stelle ebenfalls zu Problemen kommen kann.

Komprimierte Sockets

Idealerweise werden Daten vor der Übertragung komprimiert. Auf diese Weise sparen Sie Bandbreite und können mehr Daten übertragen.

Das Paket java.util.zip, eingeführt mit JDK 1.3, stellt ein Java Interface für den weltweit genutzen ZLIB Kompressionsalgorithmus zur Verfügung. Die Kernklassen dieses Packages sind die beiden Klassen Inflator und Deflator, die eine native Bibliothek kapseln, welche Datenblöcke komprimiert und dekomprimiert. Zwei Paare von Stream-Klassen, ZipInputStream/ZipOutputStream und GZIPInputStream/GZIPOutputStream, verwenden den Inflator und Deflator um komprimierte Daten im bekannten ZIP- und GZIP-Format zu schreiben.

Um Daten über Sockets zu komprimieren, müssen entsprechende Stream-Klassen implementiert werden. Die beiden Klassen CompressedBlockOutputStream und CompressedBlockInputStream sind dafür zuständig.

Bei der Konzeption dieser Klassen sollten bestimmte Aspekte berücksichtigt werden.

  • Die Kompression sollte für die Anwendung unsichtbar sein. Mit anderen Worten, es sollte möglich sein sie um die Input- und Outputstreams des Socket zu wrappen, anschließend die Methoden read() und write() aufzurufen, ohne sich Gedanken über die Kompression selbst machen zu müssen.
  • Ein Datenblock sollte nach einer festgelegten Zahl von Bytes versendet werden, so dass Speicherprobleme vermieden werden.
  • Die flush() Methode sollte den gepufferten Input umgehend komprimieren und versenden, auch wenn die festgelegte Datenblockgröße noch nicht erreicht wurde.
  • Die erforderlichen Metadaten im Header sollten sich auf ein Minimum beschränken. In unserem Beispiel wird ein 8-Byte großer Kopfbereich verwendet, bei dem die ersten 4 Bytes die Länge des komprimierten Blocks identifizieren und die restlichen 4 Bytes die Größe des Datenblocks.

Um einen Überblick über die Kompressionsrate und Wirkungsweise zu bekommen, können wir die Versendung der Daten über einen Packetsniffer, z.B. Wireshark nachvollziehen. Wireshark ist ein leistungsfähiges Programm zur Analyse von Netzwerk- Kommunikationsverbindungen.

Wir versenden zunächst einen Text mithilfe der Klasse StandardSocket. Wie Sie im Screenshot erkennen können, wurden insgesamt 1120 Bytes versendet. Die Daten werden unkomprimiert im Klartext versendet und sind sehr gut lesbar.

Nun verwenden wir statt der Klasse StandardSocket, die Klasse CompressedSocket, die die Daten vor dem Senden komprimiert. Bereits auf den ersten Blick zeigt sich, dass die Datenmenge beinahe halbiert wurde. Im Datenbereich sind die Daten nun nicht mehr lesbar, da sie im komprimierten Format vorliegen.

Bei der Verwendung von komprimierten Sockets ist zu beachten, dass die Daten auch wieder dekomprimiert werden müssen. Die beiden Applikationen im Netzwerk müssen daher dieselbe Methode verwenden und das Protokoll mit den Metadaten verstehen. Darüberhinaus darf die zu komprimierende Blockgröße nicht zu klein gewählt werden, da sonst der Vorteil der Komprimierung verloren geht. Ein Kompressionsalgorithmus arbeitet bei großen Datenmengen deutlich effizienter, da mehr Muster identifiziert werden können. Die Blockgröße sollte aber auch nicht zu groß gewählt werden, weil sich dies auf die Speicherauslastung auswirkt. Das Programm Battleship verwendet eine Blockgröße von 0xFFFF bzw. 65535 Byte. Die Anwendung stellt neben der Klasse StandardSocket die Klasse CompressedSocket zur Verfügung, die Daten vor dem Senden komprimiert.

public class CompressedSocket implements SocketInterface
{
    /**
     * Creates a stream socket and connects it to the specified port number on
     * the named host.
     *
     * @param host  the host name, or null for the loopback address
     * @param port  the port number
     * @throws java.io.IOException  if an I/O error occurs when creating the socket
     * @throws java.net.UnknownHostException    if the IP address of the host could not be determined
     */
    public CompressedSocket(String host, int port)
            throws IOException, UnknownHostException
    {
        this( new Socket( host, port ) );
    }
 
    /**
     * Encapsulates a normal Java API Socket object.
     *
     * @param socket an already-open socket connection
     * @throws IOException
     */
    public CompressedSocket(Socket socket) throws IOException
    {
        s_ = socket;
        cbos_ = new CompressedBlockOutputStream( s_.getOutputStream(),
                DEFAULT_SYNC_SIZE_GZIP_BYTES );
        cbis_ = new CompressedBlockInputStream( s_.getInputStream() );
    }
 
    /**
     * Creates a stream socket and connects it to the specified port number on
     * the named host.
     *
     * @param address  the IP address
     * @param port  the port number
     * @throws java.io.IOException  if an I/O error occurs when creating the socket
     * @throws java.net.UnknownHostException    if the IP address of the host could not be determined
     */
    public CompressedSocket(InetAddress address, int port)
            throws IOException, UnknownHostException
    {
        this( new Socket( address, port ) );
    }
 
    /**
     * Closes all streams of this socket.
     *
     * @throws java.io.IOException
     */
    @Override
    public void close() throws IOException
    {
        s_.close();
        cbos_.close();
        cbis_.close();
    }
 
    /**
     * Reads the next byte of data from the input stream of this socket.
     *
     * @return  the next byte of data, or -1 if the end of the stream is reached.
     * @throws java.io.IOException
     */
    @Override
    public int read() throws IOException
    {
        return cbis_.read();
    }
 
    /**
     * Reads some number of bytes from the input stream of this socket
     * and stores them into the buffer array b.
     *
     * @param b the buffer into which the data is read.
     * @return  the total number of bytes read into the buffer, or -1 is there
     *          is no more data because the end of the stream has been reached.
     * @throws java.io.IOException
     */
    @Override
    public int read(byte[] b) throws IOException
    {
        return cbis_.read( b, 0, b.length );
    }
 
    /**
     * Writes b.length bytes from the specified byte array to the
     * output stream of this socket.
     *
     * @param b byte buffer to write
     * @throws java.io.IOException
     */
    @Override
    public void write(byte[] b) throws IOException
    {
        cbos_.write( b, 0, b.length );
        // Note that if we don't flush the stream, the last
        // block of data may not be sent across the connection. We
        // could also force the last block to be sent by calling
        // close(), which would close the socket connection.
        cbos_.flush();
    }
 
    /** A compressed output stream */
    private CompressedBlockOutputStream cbos_;
 
    /** A compressed input stream */
    private CompressedBlockInputStream cbis_;
 
    /** The socket object */
    private Socket s_;
 
    /** Default size of a compressed block */
    public static int DEFAULT_SYNC_SIZE_GZIP_BYTES = 65535;
}

Standardmäßig verwendet Battleship die Klasse CompressedSocket. Sie können alternativ auf die gewöhnliche Socketklasse StandardSocket zurückgreifen. In diesem Fall müssen Sie natürlich in Ihrem kompletten Netzwerk diese Klasse verwenden oder eine Protokollvereinbarung über den Sockettyp implementieren.

Die untersten Socketschichten sind mit den beiden Socketklassen und den zugehörigen Fabrikklassen implementiert.

Der Server

Jeder Peer in einem Peer-to-Peer-Netz muss zugleich auch Anfragen aus dem Netz bearbeiten, d.h. er muss als Server laufen um eingehende Verbindungen von Klienten (Peers) entgegen zu nehmen. Bei der Programmierung einer P2P-Netzwerkbibliothek ist der Entwurf der Serverkomponente ein erster Anhaltspunkt. Dazu implementieren wir zunächst einen Service, der im Dauerbetrieb läuft und Verbindungen von anderen Klienten akzeptieren soll. Die Java-Klasse ServerSocket lauscht auf eingehende Verbindungen an einer Portnummer und generiert ggf. eine neue TCP-Verbindung.

Der Service soll in einem separaten Thread in einer Endlosschleife laufen, so dass die Hauptanwendung nicht blockiert wird. Zunächst wird eine abstrakte Klasse Node.java generiert. Diese Klasse stellt die Basisklasse der gesamten P2P-Netzwerkbibliothek dar. Sie repräsentiert einen Knotenpunkt im Netzwerk. In dieser Klasse definieren wir auch unseren Service als innere Klasse, die sich von der Java Thread-Klasse ableitet.

/**
 * Starts the loop which is the primary operation of the Node.
 * The main loop opens a server socket, listens for incoming connections,
 * and dispatches them to registered and threaded handlers appropriately.
 */
public class NodeService extends Thread implements Runnable
{
    public void requestStop()
    {
        stop_ = true;
    }
 
    @Override
    public void run()
    {
        // Creates a thread pool that creates new threads as needed, but
        // will reuse previously constructed threads when they are available.
        final ExecutorService pool = Executors.newCachedThreadPool();
 
        try {
            service_ = true;
            sock = makeServerSocket( myInfo_.getPort() );
            sock.setSoTimeout( SOCKETTIMEOUT );
            while( !stop_ ) {
                try {
                    logger_.finest( "Listening..." );
                    Socket clientsock = sock.accept();
                    clientsock.setSoTimeout( 0 );
 
                    // Execute handler
                    pool.execute( new PeerHandler( clientsock ) );
 
                } catch( SocketTimeoutException e ) {
                    logger_.finer( e.getMessage() );
                    continue;
                } catch( Exception e ) {
                }
            }
            sock.close();
        } catch( SocketException e ) {
            logger_.severe( "SocketException NodeService: " + e );
        } catch( IOException e ) {
            logger_.severe( "IOException NodeService: " + e );
        } finally {
            pool.shutdown();
            service_ = false;   // We are no longer servicing
        }
    }
 
    private volatile boolean stop_ = false;
    private ServerSocket sock;
}

Es ist relativ aufwendig, einen neuen Thread zu konstruieren, da das Betriebssystem beteiligt ist. Wenn unser Programm eine große Anzahl von kurzlebigen Threads erzeugt, sollte es stattdessen einen Thread-Pool verwenden. Ein Thread-Pool enthält eine Anzahl von ausführungsbereiten Leerlauf-Threads. Man gibt Runnable an den Pool und einer der Threads ruft die Methode run auf. Wenn die Methode run endet, stirbt der Thread nicht, sondern bleibt erhalten, um die nächste Anforderung zu bedienen.

Einen Thread-Pool verwendet man auch aus einem anderen Grund: um die Anzahl der konkurrierenden Threads zu drosseln. Wenn eine riesige Anzahl von Threads erzeugt wird, geht die Systemleistung spürbar zurück und die virtuelle Maschine kann sogar abstürzen. Sobald viele Threads generiert werden, sollten Sie auf einen »festen« Thread-Pool zurückgreifen, der die Anzahl der konkurrierenden Threads beschränkt.

Die Klasse Executors besitzt eine Reihe von statischen Fabrikmethoden für die Konstruktion von Thread-Pools. Die Methode newChachedThreadPool in NodeService konstruiert einen Thread-Pool, der jede Task unverzüglich ausführt. Hierfür wird ein Leerlauf-Thread verwendet, falls einer verfügbar ist, andernfalls wird ein neuer Thread generiert.

Der Server läuft in einer Endlosschleife, solange bis mit requestStop() der Thread explizit angehalten wird oder eine SocketException auftritt. Der Thread-Pool wird daraufhin mit pool.shutdown() heruntergefahren.

Wie man anhand des Quelltextes erkennen kann, wird dem Pool die Klasse PeerHandler zusammen mit dem Socketobjekt übergeben. Diese Klasse PeerHandler implementiert Runnable und kümmert sich um einen neuen Klienten.

/**
 * This class is used to respond to and handle incoming connections
 * in a separate thread.
 * 
 * <pre>
 * 
 *      ------
 *     | MAIN |     ---------
 *      ------     | HANDLER |
 *         |        ---------
 *         |            |
 *         |<>-----------
 *         |
 *     ---------
 *    | EXECUTE |
 *     ---------  
 *  
 * </pre>
 */
private class PeerHandler implements Runnable
{   
    public PeerHandler(Socket socket) throws IOException 
    {
        s_ = SocketFactory.getSocketFactory().makeSocket( socket );
    }
 
    @Override
    public void run() 
    {
        logger_.finest( "New PeerHandler: " + s_ );
 
        // Note, there is no peerinfo when receiving data from stream
        PeerConnection peerconn = new PeerConnection( null, s_ );
        Message peermsg = peerconn.recvData();
 
        // Check if the message exists in the handler list
        if( !handlers_.containsKey( peermsg.getType() ) ) {
            logger_.finest( "Not handled: " + peermsg );
        } else {
            logger_.info( "Handling: " + peermsg );
            // Execute handler
            handlers_.get( peermsg.getType() ).handleMessage( peerconn, peermsg );
        }
 
        // NOTE: log message should indicate null peerconnection host
        logger_.finest( "Disconnecting incoming: " + peerconn );
 
        peerconn.close();
    }
 
    private SocketInterface s_;  
}

Zunächst wird im Konstruktor mit der von uns zuvor definierten Fabrikmethode ein neuer Socket generiert. In run finden nun eine Reihe entscheidender Ereignisse statt. Eine wichtige Klasse in der P2P-Bibliothek ist die Klasse PeerConnection. Diese Klasse stellt eine Verbindung zu einem anderen Peer her und verwaltet Sockets. Zwei elementare Methoden aus dieser Klasse sind die Methoden sendData und recvData.

/**
 * Sends a Message to the connected peer.
 * 
 * @param msg the message object to send
 */
public void sendData(Message msg) 
{
    try {
        s_.write( msg.toBytes() );
    } catch( MessageException e ) {
        logger_.warning( "Error sending message: " + e.getMessage() );
    } catch( IOException e ) {
        logger_.warning( "Error sending message: " + e.getMessage() );
    }
}
/**
 * Receives a PeerMessage from the connected peer.
 * 
 * @return the message object received, or null if error
 */
public Message recvData() 
{
    try {
        Message msg = new PeerMessage( s_ );    // Read from socket
        return msg;
    } catch( IOException e ) {
        // It's normal for EOF to occur if there is no more replies coming
        // back from this connection.
        if( e.getMessage().equals( "PeerMessage IOException: Undefined message type." ) ) {
            // Lowest priority
            logger_.finest( "Error receiving message: " + e.getMessage() );
        } else {
            logger_.warning( "Error receiving message: " + e.getMessage() );
        }
        return null;
    }
}

Die Methode recvData liest und konstruiert eine neue P2P-Nachricht von einem Socket. Auf den Entwurf von Nachrichten wird im nächsten Kapitel detailliert eingegangen.

Nachdem die Konstruktion einer neuen Netzwerknachricht abgeschlossen wurde, wird ein Handler aufgerufen. Mithilfe des Nachrichtentyps wird die Nachricht automatisch an die zugehörige Handlerroutine weitergeleitet. Auch zu den Handlern werden wir später mehr erfahren.



Zuletzt aktualisiert am Montag, den 23. April 2012 um 12:59 Uhr
 
AUSWAHLMENÜ