Ein Chatraum

1. Aufgabe

Vorbemerkung: Diese Fallstudie stellt eine Überarbeitung des gleichnamigen Kapitels aus meinem Buch „Objektorientiertes Programmieren in Visual C#“ dar. An die Stelle der Windows Forms-Klassenbibliothek ist nun die Windows Presentation Foundation getreten. Zusätzlich wurde eine Client-Anwendung als Java-Android-Application erstellt.

Die vorliegende Fallstudie beschäftigt sich am Beispiel eines Chatraums mit dem Zusammenspiel zentraler Mechanismen des .NET-Programmiermodells wie dem sinnvollen Einsatz von Multithreading, der Hantierung von blockierenden Methodenaufrufen und den Klassen aus dem System.Net-Namensraum zur Entwicklung einer verteilten Anwendung.

Ein besonderes Augenmerk wird in dieser Fallstudie auf die Problematik von blockierenden Methodenaufrufen gelegt, da diese in der Praxis sehr häufig anzutreffen sind. Unmittelbar damit in Zusammenhang steht das Problem der Reaktionsfähigkeit einer Anwendung. Es ist leicht einzusehen, dass das versehentliche Übersehen eines blockierenden Methodenaufrufs unmittelbar zum hinlänglich bekannten „Einfrieren“ der Anwendung führt. Das probate Mittel zur Vermeidung von Blockaden sind Threads. Eine reaktionsfähige Anwendung fußt aus diesem Grund stets auf dem Fundament des Multithreadings.

Die vollständige Chatraum-Applikation setzt sich aus drei Anwendungen zusammen. Einem konsolen-basierten Chat-Server, in C# geschrieben, und zwei Chat-Client-Anwendungen, die jeweils eine grafische Benutzerschnittstelle zur Verfügung stellen. Die eine Client-Anwendung ist mit der WPF (Windows Presentation Foundation) erstellt und für den Einsatz auf einem Windows-PC konzipiert. Die zweite Client-Anwendung ist in Java als Android-Smartphone-App geschrieben. Die Aufgabe des Servers besteht in der Verwaltung aller Chat-Teilnehmer und basiert auf folgenden Annahmen:

  • Jeder Client meldet sich – mit regulären Socket-Mechanismen – am Server an.

  • Wenn ein Client eine Nachricht an den Server sendet, wird diese an alle angemeldeten Clients weitergeleitet.

  • Zur Vereinfachung der Implementierung stellt jeder Client bei der Übermittlung einer Nachricht seinen Namen an den Anfang. Der Server ist so von der unangenehmen Aufgabe befreit, bei eingehenden Nachrichten den Namen des Absenders bestimmen zu müssen, um diesen dann bei der Nachrichtenverteilung mit zu berücksichtigen.

  • Nimmt ein Client nicht mehr an der Unterhaltung teil, so meldet er sich (wiederum mit regulären Socket-Mechanismen) vom Chat-Server ab.

Der Chat-Client stellt eine Oberfläche zur Verfügung, die dem Anwender das An- und Abmelden ermöglicht, das Übertragen von einzeiligen Texten unterstützt und den Verlauf der Konversation aller Teilnehmer im Chatraum darstellt.

Beachten Sie bei der Implementierung des Servers die folgenden zwei Hinweise:

  • Reaktionsfähigkeit des Servers:

    Pro Client muss der Server eine Socketverbindung einrichten. Da das Warten auf Nachrichten der Clients den Server blockiert, ist jede Verbindung in einen separaten Thread auszulagern.

  • Verteilung eingehender Nachrichten an alle angemeldeten Clients:

    Trifft im Chat-Server eine Nachricht ein, so ist diese an alle angemeldeten Clients zu verteilen. Eine einfache Möglichkeit besteht darin, direkt beim Eintreffen der Nachricht – also im Kontext des aktuellen Threads – den Rundruf an alle Clients durchzuführen. Ein Nachteil dieses Lösungsansatzes liegt darin, dass während dieser Zeitdauer der Chat-Server keine weiteren Nachrichten für diesen Client empfangen kann, da der clientspezifische Thread in diesem Zeitabschnitt keinen Leseauftrag an der Socket-Schnittstelle abgesetzt hat, also nicht empfangsbereit ist. Um die Reaktionsfähigkeit des Servers zu verbessern, kann man im Server mit der Verteilung eingehender Nachrichten einen separaten Thread beauftragen.

Zur Darstellung eintreffender Mitteilungen des Chat-Servers muss der Client einen Leseauftrag an der Socket-Schnittstelle des Servers aufziehen. Um wiederum eine Blockade des Clients zu verhindern, ist auch dieser Aufruf in einen Thread auszulagern.

Da der Client zu einem nicht vorhersehbaren Zeitpunkt beendet wird, stellt sich die Frage, inwieweit sich ein blockierender ReadLine-Aufruf eines StreamReader-Objekts im Workerthread und der Aufruf der OnClosing-Methode im Primärthread ins Gehege kommen. Die Antwort ist vergleichsweise einfach: Ein Aufruf der Close-Methode am StreamReader-Objekt zieht unmittelbar eine IOException-Ausnahme an einem blockierenden ReadLine-Aufruf nach sich, sprich das Problem der Blockade löst sich auf diese Weise von selbst.

Der Server selbst braucht keine Oberfläche zu besitzen. Allerdings sollten Sie zu Testzwecken mit Console.WriteLine-Ausgaben die Vorgänge im Server protokollieren, siehe zum Beispiel Abbildung 1:

Die Oberfläche des Chat-Servers.

Abbildung 1. Die Oberfläche des Chat-Servers.


Die Oberfläche des Clients könnte wie in Abbildung 2 gezeigt aussehen:

Die Oberfläche des Chat-Clients.

Abbildung 2. Die Oberfläche des Chat-Clients.


Mit der Schaltfläche Connect ist die Verbindung zum Chat-Server aufzunehmen, mit der Disconnect-Schaltfläche wieder abzubauen. Bevor Sie die erste Nachricht an den Chat-Server senden, müssen Sie im Textfeld mit der Name-Beschriftung den Namen des Clients eintragen. Durch die Send-Schaltfläche wird die Nachricht des Clients an den Server gesendet, der diese dann an alle angemeldeten Clients verteilt. Für die Darstellung einer eintreffenden Nachricht wählen Sie eine Instanz der Klasse TextBox, deren Multiline-Eigenschaft den Wert true besitzt. Beachten Sie: Auf die bei Standard-Chat-Clients übliche Liste der augenblicklich verbundenen Clients können Sie aus Aufwandsgründen verzichten.

Die Oberfläche einer Android-Smartphone-App dürfen Sie nach Ihren Vorstellungen entwerfen. Im Großen und Ganzen sollte sie ähnlich zur Anwendung aus Abbildung 2 sein.

2. Lösung

Die Realisierung des Chat-Servers verteilen wir auf mehreren Klassen, deren Aufgaben wir im Folgenden kurz skizzieren:

  • Klasse ChatServer – Rahmenklasse des Chat-Servers. Dient zum Hoch- und Herunterfahren des Servers. Die eigentliche Tätigkeit des Servers wird in einen separaten Thread ausgelagert, um den Server kontrolliert beenden zu können.

  • Klasse ChatSessionManager – Im Chat-Server liegt ein Objekt dieser Klasse vor, es wickelt den Verbindungsaufbau mit allen Clients ab. Die Hauptarbeit wird in der Methode ServeSessions erbracht (Einrichten eines Kommunikationsendpunkts, Warten auf Verbindungsaufbau durch einen Chat-Client). Zur Verwaltung der Clients dient eine List<ChatSession>-Instanz, siehe dazu das in Abbildung 3 gezeigte sessions-Objekt.

  • Klasse ChatSession – Ist für eine Unterhaltung im Chatraum verantwortlich. Pro Chat-Client existiert eine Instanz dieser Klasse. Ihre zentrale Methode ServeSingleSession zum Empfangen eingehender Nachrichten ist wiederum in den Kontext eines separaten Threads ausgelagert, um die Reaktionsfähigkeit des Servers nicht zu beeinträchtigen. Zentrale Daten dieser Klasse sind drei Objekte vom Typ TcpClient, StreamReader und StreamWriter.

  • Klasse ChatMessenger – Das Verteilen ausgehender Nachrichten wird von der Klasse ChatMessenger übernommen. Um die einzelnen Konversationen der Chat-Teilnehmer mit dem Chat-Server nicht unnötig zu belasten, ist die Verteilung ausgehender Nachrichten wiederum in einen separaten Thread ausgelagert. Zu diesem Zweck liegt im Chat-Server eine Instanz der ChatMessenger-Klasse vor, die DistributeMessages-Methode versendet alle eingegangenen Nachrichten an die Clients. Die aktuellen Nachrichten residieren in einem List<String>-Objekt (siehe in Abbildung 3 das messages-Objekt). Die ChatSession-Instanzen tragen eingehende Nachrichten in diese Liste ein, das ChatMessenger-Verteilerobjekt liest die Nachrichten wieder aus, um sie nebenläufig an die Clients zu verteilen.

Die Architektur des Chat-Servers im Überblick.

Abbildung 3. Die Architektur des Chat-Servers im Überblick.


Damit kommen wir zur Realisierung der vorgestellten Klassen, wir beginnen in Listing 1 mit der Klasse ChatServer:

01: class ChatServer
02: {
03:     public static void Main ()
04:     {
05:         // create session manager
06:         ChatSessionManager manager = new ChatSessionManager ();
07:         manager.Start ();
08: 
09:         // keep server alive
10:         Console.WriteLine ("Press any key to close server ...");
11:         Console.ReadLine ();
12: 
13:         // shutdown server
14:         manager.Stop ();
15:         Console.WriteLine ("Server closed.");
16:     }
17: }

Beispiel 1. Die Klasse ChatServer.


Die Klasse ChatServer delegiert die gesamte Arbeit des Servers an ein ChatSessionManager-Objekt. Um den Server reaktionsfähig zu gestalten, bietet die ChatSessionManager-Instanz zwei Methoden Start und Stop an. Der Rumpf der Start-Methode wird in den Kontext eines separaten Threads ausgelagert, sie ist also nicht blockierend. Die Stop-Methode in Zeile 20 von Listing 1 kann somit im Kontext des Primärthreads die Anwendung kontrolliert herunterfahren.

In der ChatSessionManager-Instanz wird mit Hilfe einer TcpListener-Instanz der Server in den empfangsbereiten Zustand versetzt. Die ServeSessions-Methode in diesem Objekt ist die zentrale Drehscheibe des Servers. Die AcceptTcpClient-Methode wartet dort auf eingehende Verbindungen, siehe Zeile 88 in Listing 2:

001: class ChatSessionManager
002: {
003:     private TcpListener server;
004:     private List<ChatSession> sessions;
005:     private ChatMessenger messenger;
006:     private Thread t;
007: 
008:     public ChatSessionManager ()
009:     {
010:         this.sessions = new List<ChatSession> ();
011:     }
012: 
013:     public void Start ()
014:     {
015:         ThreadStart ts = new ThreadStart (this.ServeSessions);
016:         this.t = new Thread (ts);
017:         this.t.Start ();
018:     }
019: 
020:     public void Stop ()
021:     {
022:         this.server.Stop ();
023:         this.Shutdown ();
024:         this.t.Join ();
025:     }
026: 
027:     public void AddSession (ChatSession session)
028:     {
029:         Monitor.Enter (this.sessions);
030:         this.sessions.Add (session);
031:         Monitor.Exit (this.sessions);
032:     }
033: 
034:     public void RemoveSession (ChatSession session)
035:     {
036:         Monitor.Enter (this.sessions);
037:         this.sessions.Remove (session);
038:         Monitor.Exit (this.sessions);
039:     }
040: 
041:     private void Shutdown ()
042:     {
043:         Monitor.Enter (this.sessions);
044:         for (int i = 0; i < this.sessions.Count; i ++)
045:         {
046:             ChatSession session = this.sessions[i];
047:             session.Shutdown ();
048:         }
049: 
050:         this.sessions.Clear ();
051:         Monitor.Exit (this.sessions);
052:     }
053: 
054:     public void DistributeMessage (String s)
055:     {
056:         Monitor.Enter (this.sessions);
057:         for (int i = 0; i < this.sessions.Count; i ++)
058:         {
059:             ChatSession session = this.sessions[i];
060:             Console.WriteLine ("Sending reply to client at slot {0}", i);
061:             session.WriteMessage (s);
062:         }
063:         Monitor.Exit (this.sessions);
064:     }
065: 
066:     private void ServeSessions ()
067:     {
068:         // launch messenger thread
069:         this.messenger = new ChatMessenger (this);
070:         this.messenger.Start ();
071: 
072:         // create a listener for connections from TCP network clients
073:         IPAddress adr = IPAddress.Any;
074:         IPEndPoint ep = new IPEndPoint (adr, 4711);
075:         this.server = new TcpListener (ep);
076: 
077:         // start listening for client requests
078:         this.server.Start ();
079: 
080:         // enter the listening loop
081:         while (true)
082:         {
083:             // perform a blocking call to accept requests
084:             Console.WriteLine ("Waiting for chat client request ...");
085:             TcpClient client;
086:             try
087:             {
088:                 client = this.server.AcceptTcpClient ();
089:             }
090:             catch (SocketException)
091:             {
092:                 Console.WriteLine ("Server is closing ...");
093:                 break;
094:             }
095: 
096:             ChatSession session =
097:                 new ChatSession (this, this.messenger, client);
098: 
099:             this.AddSession (session);
100: 
101:             // serve this chat client - using another thread context
102:             session.Start ();
103:         }
104: 
105:         // terminate notifications thread
106:         this.messenger.Stop ();
107:     }
108: }

Beispiel 2. Die Klasse ChatSessionManager.


Die Klasse ChatSessionManager besitzt zwei weitere Methoden AddSession und RemoveSession: Mit ihrer Hilfe wird ein Chat-Client am Server an- bzw. abgemeldet. Die konkrete Beschreibung eines Chat-Clients erfolgt im Server durch die Klasse ChatSession (siehe Listing 3). Im laufenden Betrieb des Servers werden die aktuellen ChatSession-Objekte in einer Liste des Typs List<ChatSession> abgelegt:

private List<ChatSession> sessions;

Das sessions-Objekt ist nur innerhalb des ChatSessionManager-Objekts erreichbar. Um beispielsweise eine eingehende Nachricht an alle angemeldeten Clients weiterzuleiten, bietet das ChatSessionManager-Objekt eine Methode DistributeMessage an, die in Kenntnis aller Clients die Verteilung durchführt.

Ein ChatSession-Objekt verwaltet für die Konversation mit dem Chat-Client zwei Instanzen vom Typ StreamReader bzw. StreamWriter. Die Hauptarbeit einer Unterhaltung im Chatraum wird in der Methode ServeSingleSession geleistet, siehe die Zeilen 26 bis 59 von Listing 3:

01: class ChatSession
02: {
03:     private ChatSessionManager manager;
04:     private ChatMessenger messenger;
05:     private TcpClient client;
06:     private StreamReader reader;
07:     private StreamWriter writer;
08: 
09:     public ChatSession (
10:         ChatSessionManager manager,
11:         ChatMessenger messenger,
12:         TcpClient client)
13:     {
14:         this.manager = manager;
15:         this.messenger = messenger;
16:         this.client = client;
17:     }
18: 
19:     public void Start ()
20:     {
21:         ThreadStart ts = new ThreadStart (this.ServeSingleSession);
22:         Thread t = new Thread (ts);
23:         t.Start ();
24:     }
25: 
26:     private void ServeSingleSession ()
27:     {
28:         // create comfortable input and output streams
29:         NetworkStream stream = client.GetStream();
30:         this.reader = new StreamReader (stream, Encoding.ASCII);
31:         this.writer = new StreamWriter (stream, Encoding.ASCII);
32:         this.writer.AutoFlush = true;
33: 
34:         while (true)
35:         {
36:             String line;
37:             try
38:             {
39:                 // wait for a message from the client
40:                 line = this.reader.ReadLine ();
41:                 Console.WriteLine ("Received {0}", line);
42: 
43:                 // handle end of conversation
44:                 if (line == null)
45:                     break;
46:             }
47:             catch (IOException)
48:             {
49:                 // corresponding socket has been closed
50:                 break;
51:             } 
52: 
53:             this.messenger.EnterMessage (line);
54:         }
55: 
56:         // shutdown and remove session
57:         this.Shutdown ();
58:         this.manager.RemoveSession (this);
59:     }
60:   
61:     public void WriteMessage (String s)
62:     {
63:         this.writer.WriteLine (s);
64:     }
65: 
66:     public void Shutdown ()
67:     {
68:         // shutdown this connection
69:         this.reader.Close ();
70:         this.writer.Close ();
71:         this.client.Close ();
72:     }      
73: }

Beispiel 3. Die Klasse ChatSession.


Vervollständigt wird der Server durch die Klasse ChatMessenger, die sich exklusiv um die Verteilung aller Nachrichten im Chatraum kümmert, siehe Listing 4:

01: class ChatMessenger
02: {
03:     private ChatSessionManager manager;
04:     private List<String> messages;
05:     private Thread t;
06:     private bool isAlive;
07: 
08:     public ChatMessenger (ChatSessionManager manager)
09:     {
10:         this.manager = manager;
11:         this.messages = new List<String> ();
12:     }
13: 
14:     public void Start ()
15:     {
16:         ThreadStart ts = new ThreadStart (this.DistributeMessages);
17:         this.t = new Thread (ts);
18:         this.isAlive = true;
19:         this.t.Start ();
20:     }
21: 
22:     public void Stop ()
23:     {
24:         Monitor.Enter (this);
25:         this.isAlive = false;
26:         Monitor.Pulse (this);
27:         Monitor.Exit (this);
28:         this.t.Join ();
29:     }
30: 
31:     public void EnterMessage (String s)
32:     {
33:         Monitor.Enter (this);
34:             
35:         // add message
36:         this.messages.Add (s);
37: 
38:         // resume message distribution
39:         Monitor.Pulse (this);
40:         Monitor.Exit (this);
41:     }
42: 
43:     private void DistributeMessages ()
44:     {
45:         // send incoming messages to all chat clients
46:         while (this.isAlive)
47:         {
48:             String s = this.RemoveMessage ();
49:             if (s == null)
50:                 break;
51: 
52:             this.manager.DistributeMessage (s);
53:         }
54:     }
55:     
56:     private String RemoveMessage ()
57:     {
58:         Monitor.Enter (this);
59: 
60:         // queue is empty
61:         while (this.messages.Count == 0)
62:         {
63:             if (! this.isAlive)
64:             {
65:                 // terminate message distribution
66:                 Monitor.Exit (this);
67:                 return null;
68:             }
69: 
70:             // suspend message distribution
71:             Monitor.Wait (this);
72:         }
73: 
74:         // remove entry
75:         String msg = this.messages[0];
76:         this.messages.RemoveAt (0);
77: 
78:         Monitor.Exit (this);
79:         return msg;
80:     }
81: }

Beispiel 4. Die Klasse ChatMessenger.


Die ChatSession-Objekte und das einzelne ChatMessenger-Objekt stehen in einer Produzenten-Verbraucher-Beziehung zueinander (siehe Abbildung 4): Die ChatSession-Objekte produzieren Nachrichten, das ChatMessenger-Objekt konsumiert diese, um sie (nebenläufig) an die Clients zu verteilen.

Das Produzenten-Verbraucher-Problem in einem Chatraum.

Abbildung 4. Das Produzenten-Verbraucher-Problem in einem Chatraum.


Stehen im Nachrichtenverteilerobjekt (ChatMessenger-Instanz) keine Nachrichten zur Verteilung an, suspendiert sich die RemoveMessage-Methode mit Hilfe eines Aufrufs von Monitor.Wait (Zeile 71, Listing 4). Neue Nachrichten laufen jeweils im Kontext der clientspezifischen Threads ein, das Nachrichtenverteilerobjekt wird mit Monitor.Pulse wieder aktiviert (Zeile 39). Ein Aufruf der Monitor.Pulse-Methode ist ebenfalls erforderlich, wenn die Tätigkeit des Nachrichtenverteilerobjekts beendet werden soll. Da das Verteilerobjekt suspendiert sein kann und damit keine Kenntnis über etwaige lokale Zustandänderungen hat, erfolgt in der Stop-Methode ein provisorischer Pulse-Aufruf (Zeile 26). Auf diese Weise wertet das Verteilerobjekt die Kontrollvariable isAlive aktuell aus und verlässt bei Bedarf die Verteilermethode.

Damit wenden wir uns den Client-Anwendungen des Chatraum-Servers zu. Für eine Android-Smartphone-Applikation benötigen wir – selbst im einfachsten Fall – die folgenden Dateien:

  • Datei AndroidManifest.xml – Allgemeine Beschreibung des Projekts (Dateien, Berechtigungen, usw.).

  • Datei MainActivity.java – Implementierung der Hauptaktivität der Anwendung.

  • Datei activity_main.xml – Beschreibung der Oberfläche der Hauptaktivität in XML.

  • Datei strings.xml – Alle Zeichenketten, die in der Anwendung eine Rolle spielen (als Ressourcen in eine separate .XML-Datei ausgelagert).

Zur Gestaltung der Oberfläche finden Sie in Abbildung 1 eine Anregung vor:

Chatraum-Client: Layout einer Android-Applikation.

Abbildung 1. Chatraum-Client: Layout einer Android-Applikation.


Wenn Sie sich strikt an das Layout aus Abbildung 1 halten, finden Sie dazu eine entsprechende Umsetzung in Android-XML in Listing 1 vor:

01: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
02:     xmlns:tools="http://schemas.android.com/tools"
03:     android:layout_width="match_parent"
04:     android:layout_height="match_parent"
05:     android:orientation="vertical"
06:     tools:context=".MainActivity" >
07:     
08:     <LinearLayout 
09:         android:orientation="horizontal" 
10:         android:layout_width="match_parent"
11:         android:layout_height="wrap_content">
12:         
13:         <TextView
14:             android:layout_width="wrap_content"
15:             android:layout_height="wrap_content"
16:             android:text="@string/name_caption"
17:             android:textSize="20sp"
18:             android:layout_margin="4dp" />
19:             
20:         <EditText
21:             android:inputType="text"
22:             android:singleLine="true"
23:             android:id="@+id/editTextName"
24:             android:layout_width="match_parent"
25:             android:layout_height="wrap_content"
26:             android:layout_margin="4dp" />              
27:     </LinearLayout>
28: 
29:     <Button
30:         android:id="@+id/buttonConnect"
31:         android:layout_width="match_parent"
32:         android:layout_height="wrap_content"
33:         android:textSize="20sp"
34:         android:text="@string/connect_caption" />
35:     
36:     <Button
37:          android:id="@+id/buttonDisconnect"
38:          android:layout_width="match_parent"
39:          android:layout_height="wrap_content"
40:          android:textSize="20sp"
41:          android:text="@string/disconnect_caption" /> 
42:          
43:     <TextView
44:         android:id="@+id/textViewStatus"
45:         android:singleLine="true"
46:         android:layout_width="match_parent"
47:         android:layout_height="wrap_content"
48:         android:textSize="16sp"
49:         android:layout_margin="4dp"
50:         android:background="#CACACA" />
51:     
52:     <EditText
53:         android:id="@+id/editTextMessages"
54:         android:layout_width="match_parent"
55:         android:layout_height="0dip"
56:         android:layout_margin="4dp"
57:         android:layout_weight="1.0"
58:         android:gravity="top"
59:         android:inputType="textMultiLine|textNoSuggestions"
60:         android:scrollbars="vertical"
61:         android:singleLine="false" />
62:     
63:     <LinearLayout 
64:         android:orientation="horizontal" 
65:         android:layout_width="match_parent"
66:         android:layout_height="wrap_content">
67:         
68:         <Button
69:              android:id="@+id/buttonSend"
70:              android:textSize="20sp"
71:              android:layout_margin="4dp"
72:              android:layout_width="wrap_content"
73:              android:layout_height="wrap_content"
74:              android:text="@string/send_caption" />
75:             
76:         <EditText
77:             android:id="@+id/editTextMessage"
78:             android:inputType="textNoSuggestions"
79:             android:singleLine="true"
80:             android:layout_width="match_parent"
81:             android:layout_height="wrap_content"
82:             android:layout_margin="4dp" />      
83:                
84:     </LinearLayout>
85: </LinearLayout>

Beispiel 1. Datei activity_main.xml.


Die diversen Zeichenketten, die in dieser App zum Einsatz gelangen, sind Android-konform in eine Ressourcen-XML-Datei ausgelagert worden:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">ChatClient</string>
    <string name="action_settings">Settings</string>
    <string name="hello_world">Hello world!</string>

    <string name="name_caption">Name:</string>
    <string name="connect_caption">Connect</string>
    <string name="disconnect_caption">Disconnect</string>
    <string name="send_caption">Send</string>
</resources>

In der Klasse MainActivity (siehe Listing 6) sind im Wesentlichen zwei Aspekte abgehandelt: Zum einen alle Aktivitäten, die auf Grund von Bedienhandlungen in die Wege zu leiten sein (siehe die Ereignishandler für die Schaltflächen) sowie alle Socket-spezifischen Anweisungen, die für den Aufbau und den Abbruch einer Socket-Verbindung notwendig sind. In den Zeilen 74 bis 89 gibt es eine Methode onClick, die für alle drei Schaltflächen („Connect“, „Disconnect“ und „Send“) erster Ansprechpartner ist. Softwaretechnisch wird sie durch die Schnittstelle OnClickListener vorgegeben. Durch den Parameter des Typs View bzw. genauer formuliert durch einen Aufruf der getId-Methode an diesem Objekt lässt sich feststellen, welche Schaltfläche das Click-Ereignis ausgelöst hat.

Die rein Socket-spezifischen Anweisungen in Listing 6 sind bereits in der J2SE (bzw. J2EE) vorhanden, sie stellen also keine Android-spezifischen Erweiterungen dar. Aus diesem Grund gehen wir auf diese Anweisungen nicht näher ein. Die reine Konversation mit dem Server selbst wurde aus Gründen der Übersichtlichkeit in eine separate Klasse ChatConversation ausgelagert, dazu später noch mehr. Da alle Android-Bedienelemente (Schaltflächen, Eingabefelder, etc.) durch XML-Direktiven definiert werden, ist es im Java-Quellcode notwendig, sich Referenzen zu diesen Bedienelementen zu verschaffen. Die entsprechende Anweisungsfolge finden Sie in den Zeilen 47 bis 53 vor. Das Bindeglied zwischen den XML-Direktiven und dem Java-Quellcode sind IDs, die im XML-Beschreibungscode symbolisch durch entsprechende android:id-Angaben erfolgt sind:

001: package com.example.chatclient;
002: 
003: import java.io.BufferedReader;
004: import java.io.IOException;
005: import java.io.InputStreamReader;
006: import java.io.PrintWriter;
007: import java.net.InetSocketAddress;
008: import java.net.Socket;
009: import java.net.UnknownHostException;
010: 
011: import android.os.Bundle;
012: import android.app.Activity;
013: import android.text.Editable;
014: import android.view.Menu;
015: import android.view.View;
016: import android.view.View.OnClickListener;
017: import android.widget.Button;
018: import android.widget.EditText;
019: import android.widget.TextView;
020: import android.widget.Toast;
021: 
022: public class MainActivity extends Activity implements OnClickListener {
023:     
024:     private final int portNumber = 4711;
025:     private final String hostName = "192.168.178.114";
026: 
027:     private Button buttonConnect;
028:     private Button buttonDisconnect;
029:     private Button buttonSend;
030:     private TextView textViewStatus;
031:     private EditText editTextMessages;
032:     private EditText editTextName;
033:     private EditText editTextMessage;    
034:     
035:     private Socket chatSocket;
036:     private PrintWriter writer;
037:     private BufferedReader reader;
038:     
039:     private String clientName;
040: 
041:     @Override
042:     protected void onCreate(Bundle savedInstanceState) {
043:         super.onCreate(savedInstanceState);
044:         setContentView(R.layout.activity_main);
045:         
046:         // retrieve UI references    
047:         this.buttonConnect = (Button) this.findViewById(R.id.buttonConnect);
048:         this.buttonDisconnect = (Button) this.findViewById(R.id.buttonDisconnect);
049:         this.buttonSend = (Button) this.findViewById(R.id.buttonSend);
050:         this.textViewStatus = (TextView) this.findViewById(R.id.textViewStatus);
051:         this.editTextMessages = (EditText) this.findViewById(R.id.editTextMessages);
052:         this.editTextName = (EditText) this.findViewById(R.id.editTextName);
053:         this.editTextMessage = (EditText) this.findViewById(R.id.editTextMessage);
054:                 
055:         // establish event handler
056:         this.buttonConnect.setOnClickListener(this);
057:         this.buttonDisconnect.setOnClickListener(this);
058:         this.buttonSend.setOnClickListener(this);
059:         
060:         // reset controls
061:         this.textViewStatus.setText("");
062:         
063:         // initialize socket
064:         this.chatSocket = null;
065:     }
066: 
067:     @Override
068:     public boolean onCreateOptionsMenu(Menu menu) {
069:         getMenuInflater().inflate(R.menu.main, menu);
070:         return true;
071:     }
072: 
073:     @Override
074:     public void onClick(View v) {
075:         String text;
076:         
077:         switch (v.getId()){
078:         case R.id.buttonConnect:
079:             this.ConnectToServer();
080:             break;
081:         case R.id.buttonDisconnect:
082:             this.DisconnectFromServer();
083:             break;
084:         case R.id.buttonSend:
085:             text = this.editTextMessage.getText().toString();
086:             this.SendMessage(text);
087:             break;
088:         }
089:     }
090:     
091:     // private helper methods
092:     private void ConnectToServer () {
093:         // check for client name
094:         Editable name = this.editTextName.getText();
095:         this.clientName = name.toString();
096:         if (this.clientName.equals(""))
097:         {
098:             Toast toast =
099:                     Toast.makeText(getApplicationContext(), 
100:                     "Please enter name of client!", Toast.LENGTH_LONG);
101:             toast.show();
102:             return;
103:         }
104: 
105:         // establish connection with chat server
106:         try {
107:             InetSocketAddress address = new InetSocketAddress(hostName, portNumber);
108:             this.chatSocket = new Socket();
109:             this.chatSocket.connect(address, 15000); 
110:             
111:             this.writer = new PrintWriter(this.chatSocket.getOutputStream(), true);
112:             InputStreamReader isr = new InputStreamReader(this.chatSocket.getInputStream());    
113:             this.reader = new BufferedReader (isr);
114:             
115:             this.textViewStatus.setText("Socket connected!");
116:             
117:             this.buttonConnect.setEnabled(false);
118:             this.buttonDisconnect.setEnabled(true);
119:         }
120:         catch (UnknownHostException ex) {
121:             this.textViewStatus.setText("InetAddress error!");
122:             return;
123:         }
124:         catch (IOException ex) {
125:             this.textViewStatus.setText("Connect of Socket failed!");
126:             return;
127:         }
128:         catch (Exception ex) {
129:             this.textViewStatus.setText("General exception!");
130:             return;
131:         }
132:         
133:         // create worker thread for non-blocking conversation
134:         ChatConversation conversation = new ChatConversation(this, this.editTextMessages);
135:         conversation.Start(this.reader);
136:     }
137:     
138:     private void DisconnectFromServer () {
139: 
140:         try {
141:             if (this.chatSocket != null) {
142:                 this.chatSocket.close();
143:                 this.reader.close();
144:                 this.writer.close();
145: 
146:                 this.textViewStatus.setText("Socket disconnected!");
147:                 this.buttonConnect.setEnabled(true);
148:                 this.buttonDisconnect.setEnabled(false);
149:             }
150:         }
151:         catch (IOException ex) {
152:             System.out.println ("Disconnect of socket failed ...");
153:             this.textViewStatus.setText("Socket disconnect failed!");
154:         }
155:     }
156:     
157:     private void SendMessage (String raw) {
158:         if (this.writer == null)
159:             return;
160:         
161:         // send message to server
162:         String msg = String.format("%s: %s", this.clientName, raw);
163:         this.writer.println(msg);
164:         
165:         // clear input field
166:         this.editTextMessage.setText("");
167:     }
168: }

Beispiel 6. Datei MainActivity.java.


In der Datei ChatConversation.java wird die Unterhaltung eines Chat-Clients mit dem Chat-Server implementiert. Da die readLine-Methode blockierend ist (sie dient dazu, Nachrichten vom Chat-Server entgegenzunehmen), ist ihr Aufruf in einen separaten Thread ausgelagert. Wie in den meisten Frameworks sind wir nun mit dem Problem konfrontiert, dass im Kontext eines Sekundärthreads Zugriffe auf das UI nicht mehr möglich sind. Die wichtigsten Anweisungen von Listing 7 finden wir deshalb in den Zeilen 40 bis 46 vor. Die runOnUiThread-Methode erwartet als Parameter eine Methodeadresse (hier: Methode run, festgelegt durch die Standardschnittstelle Runnable), um diese in den Kontext des UI-Threads einschleusen zu können.

01: package com.example.chatclient;
02: 
03: import java.io.BufferedReader;
04: import java.io.IOException;
05: 
06: import android.app.Activity;
07: import android.util.Log;
08: import android.widget.EditText;
09: 
10: public class ChatConversation implements Runnable {
11:     
12:     private Activity activity;
13:     private EditText editTextMessages;
14:     
15:     private BufferedReader reader;
16:     
17:     public ChatConversation(Activity activity, EditText editTextMessages) {
18:         this.activity = activity;
19:         this.editTextMessages = editTextMessages;
20:     }
21:     
22:     public void Start(BufferedReader reader) {
23:         this.reader = reader;
24:         
25:         Thread t = new Thread (this);
26:         t.start();
27:     }
28: 
29:     @Override
30:     public void run() {        
31:         Log.i("PeLo", "Enter conversation thread");
32:         while (true)
33:         {
34:             try
35:             {
36:                 // read message from chat server, if any
37:                 final String response = this.reader.readLine();
38:                 
39:                 // copy message into text box
40:                 this.activity.runOnUiThread(new Runnable() {
41:                     @Override
42:                     public void run() {
43:                         editTextMessages.append(response);
44:                         editTextMessages.append("\n");
45:                     }
46:                 });
47:             }
48:             catch (IOException ex)
49:             {
50:                 // corresponding socket has been closed
51:                 Log.i("PeLo", ex.toString());         
52:                 break;
53:             }
54:         }
55:         Log.i("PeLo", "Exit conversation thread");
56:     }
57: }

Beispiel 7. Datei ChatConversation.java.


Android-Anwendungen ist es per se nicht gestattet, Verbindungen über die Socket-Schnittstelle aufzubauen. Im Manifest-File der Anwendung ist eine entsprechende Berechtigung freizuschalten:

<uses-permission android:name="android.permission.INTERNET"/>

Damit sind wir auch fast schon am Ziel angekommen. Wir können die App nun in Betrieb nehmen, ein mögliches Szenario könnte wie in Abbildung 6 gezeigt aussehen:

Chatraum-Client: Die Android-App bei der Arbeit.

Abbildung 6. Chatraum-Client: Die Android-App bei der Arbeit.


Das Gegenstück dieser Unterhaltung habe ich ebenfalls aufgezeichnet. Sie finden es in Abbildung 7 vor. Am Aussehen der Anwendung können Sie erkennen, dass es sich nicht um eine zweite Android-App handelt. Wir haben es dieses Mal mit einer Windows-Desktop-Anwendung zu tun, die in C# und den Standard-UI-Klassen aus der Windows Presentation Foundation geschrieben wurde:

Chatraum-Client zum Zweiten: Gegenstück der Unterhaltung.

Abbildung 7. Chatraum-Client zum Zweiten: Gegenstück der Unterhaltung.


Zum Abschluss stellen wir zur Vervollständigung der Fallstudie noch den Quellcode der zweiten Client-Anwendung vor, in Listing 8 und Listing 9 finden Sie die XAML-Direktiven (UI) und den Logikanteil (Codebehind) der WPF-Anwendung vor:

01: <Window x:Class="ChatClient.MainWindow"
02:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:         Title="Chat Client" Height="250" Width="500">
05:     <DockPanel LastChildFill="True">
06:         <UniformGrid DockPanel.Dock="Top" Rows="1" Columns="3">
07:             <DockPanel LastChildFill="True">
08:                 <Label Margin="3">Name:</Label>
09:                 <TextBox Name="TextBoxClient" Margin="3,3,5,3"/>
10:             </DockPanel>
11:             <Button Margin="3,3,5,3" Name="ButtonConnect"
12:                     Click="Button_Click" Content="Connect"/>
13:             <Button Margin="3,3,5,3" Name="ButtonDisconnect"
14:                     Click="Button_Click" Content="Disconnect"/>
15:         </UniformGrid>
16: 
17:         <DockPanel DockPanel.Dock="Bottom" LastChildFill="True">
18:             <Button Margin="3" Width="100" Name="ButtonSend"
19:                     Click="Button_Click" Content="Send"/>
20:             <TextBox Name="TextBoxMessage"></TextBox>
21:         </DockPanel>
22: 
23:         <TextBox Name="TextBoxMessages"></TextBox>
24:     </DockPanel>
25: </Window>

Beispiel 8. Chatraum-Client auf Basis der WPF: XAML-Direktiven.


001: namespace ChatClient
002: {
003:     public partial class MainWindow : Window
004:     {
005:         private const String MainTitle = "Chat Client: Version 1.0";
006: 
007:         // networking utilities
008:         private TcpClient client;
009:         private StreamReader reader;
010:         private StreamWriter writer;
011: 
012:         // threading utilities
013:         private Thread t;
014:         
015:         public MainWindow()
016:         {
017:             this.InitializeComponent();
018:             this.Title = MainTitle;
019:         }
020: 
021:         // event handler
022:         private void Button_Click(Object sender, RoutedEventArgs e)
023:         {
024:             if (sender == this.ButtonConnect)
025:             {
026:                 // check for client name
027:                 if (this.TextBoxClient.Text == "")
028:                 {
029:                     MessageBox.Show("Please enter name of client!", "Error");
030:                     return;
031:                 }
032: 
033:                 if (this.client == null)
034:                 {
035:                     // establish connection to chat server
036:                     this.OpenConnection();
037: 
038:                     // adjust title
039:                     this.Title = String.Format("{0} [{1}]",
040:                         MainTitle, this.TextBoxClient.Text);
041: 
042:                     // create worker thread for non-blocking conversation
043:                     ThreadStart ts = new ThreadStart(this.Chatting);
044:                     this.t = new Thread(ts);
045:                     this.t.Start();
046:                 }
047:             }
048:             else if (sender == this.ButtonDisconnect)
049:             {
050:                 this.CloseConnection();
051: 
052:                 // reset title
053:                 this.Title = MainTitle;
054:             }
055:             else if (sender == this.ButtonSend)
056:             {
057:                 // send message to server
058:                 if (this.client != null)
059:                 {
060:                     String msg = String.Format("{0}: {1}",
061:                         this.TextBoxClient.Text, this.TextBoxMessage.Text);
062: 
063:                     try
064:                     {
065:                         this.writer.WriteLine(msg);
066:                     }
067:                     catch (IOException)
068:                     {
069:                         // corresponding socket has been closed
070:                         return;
071:                     }
072:                     finally
073:                     {
074:                         this.TextBoxMessage.Clear();
075:                     }
076:                 }
077:             }
078:         }
079: 
080:         // private helper methods
081:         private void OpenConnection()
082:         {
083:             // define a network endpoint (IP address and port number)
084:             // IPAddress adr = IPAddress.Loopback;
085:             // or
086:             IPAddress adr = IPAddress.Parse("192.168.178.114");
087:             IPEndPoint ep = new IPEndPoint(adr, 4711);
088: 
089:             // create a TcpClient
090:             this.client = new TcpClient();
091: 
092:             // connect client to a remote TCP host
093:             this.client.Connect(ep);
094: 
095:             // retrieve the NetworkStream used to send and receive data
096:             NetworkStream stream = this.client.GetStream();
097: 
098:             // create comfortable string input and output objects
099:             this.reader = new StreamReader(stream, Encoding.ASCII);
100:             this.writer = new StreamWriter(stream, Encoding.ASCII);
101: 
102:             // flush data to the underlying stream
103:             // after every call to StreamWriter.WriteLine
104:             this.writer.AutoFlush = true;
105:         }
106: 
107:         private void CloseConnection()
108:         {
109:             if (this.client != null)
110:             {
111:                 // close everything
112:                 this.reader.Close();
113:                 this.writer.Close();
114:                 this.client.Close();
115: 
116:                 // wait for the end of worker thread
117:                 this.t.Join();
118: 
119:                 this.reader = null;
120:                 this.writer = null;
121:                 this.client = null;
122:             }
123:         }
124: 
125:         private void Chatting()
126:         {
127:             while (true)
128:             {
129:                 try
130:                 {
131:                     // read message from chat server, if any
132:                     Console.WriteLine("Vor ReadLine");
133:                     String response = this.reader.ReadLine();
134:                     Console.WriteLine("Nach ReadLine: [{0}]", response);
135: 
136:                     // copy message into text box
137:                     if (! this.Dispatcher.CheckAccess())
138:                     {
139:                         this.Dispatcher.BeginInvoke(
140:                             (Action)(() => {
141:                                 this.TextBoxMessages.AppendText(response + "\r\n");
142:                             }),
143:                             DispatcherPriority.SystemIdle, null);
144:                     }
145:                 }
146:                 catch (IOException)
147:                 {
148:                     // corresponding socket has been closed
149:                     break;
150:                 }
151:             }
152:         }
153:     }
154: }

Beispiel 9. Chatraum-Client auf Basis der WPF: Codebehind-Anweisungen.