Java-Chat selbst gemacht

Status
Nicht offen für weitere Antworten.

L-ectron-X

Gesperrter Benutzer
Weil immer wieder mal danach gefragt wird, möchte ich diesen Grundlagen vermittelnden Artikel aus der offensichtlich nicht mehr gepflegten, ehemals "führenden deutschen Java-Seite" (Kaffee & Kuchen - die führende Java-Seite Deutschlands (Neu)) zitieren und damit erhalten.

Zum Teil hat der Programmierer Fehler im Code und sich nicht an die Coding Conventions gehalten, und es wurden Methoden benutzt, die inzwischen veraltet sind und nicht mehr benutzt werden sollten.
Diese sind aber sehr leicht ersetzbar.

Damit das Ganze auch wirklich funktioniert, sollte zumindest das Applet in eine Jar-Datei gepackt und signiert werden. Für den Rechner auf dem der Server läuft, muss ggf. in der Firewall auch noch der Port geöffnet werden, damit die Datenpakete durch die Firewall gelangen.

Christoph Bergmann hat gesagt.:
Eine der herausragenden Eigenschaften des Internets gegenüber anderen Medien ist die Interaktion. Die schnellste Art mit anderen zu kommunizieren ist dabei der Chat, bei dem sich zwei oder mehr Nutzer direkt miteinander unterhalten können. Einen solchen Chat wollen wir im folgenden programmieren, wobei wir grundlegende Java-Kenntnisse voraussetzen.

Als Erstes eine kurze Einführung in die nötigen Grundlagen:


Sicherheitseinschränkungen

Applets, also Java-Programme die in HTML-Seiten eingebunden werden, unterliegen aus Sicherheitsgründen bestimmten Einschränkungen: Zum Beispiel ist Applets nur der Zugriff auf den Server erlaubt, von dem sie geladen wurden. Wurde ein Applet also z.B. vom Server "java.seite.net" geladen, kann das Applet auch nur zu diesem Server eine Verbindung aufnehmen.
Versucht ein Applet trotzdem einen Zugriff auf einen anderen Server so erscheint in der Statuszeile des Browsers die Fehlermeldung "Applet canít start: Security violation: security.socket.connect" und eine entsprechende Ausnahmebehandlung wird ausgelöst.
Derselbe Fehler tritt auf, wenn auf dem angeforderten Port keine Verbindung hergestellt werden kann, also z.B. der Chat-Server nicht gestartet wurde.


URLs

Um eine Verbindung zu einem anderen Rechner aufzubauen, benötigen wir als erstes dessen Internet-Adresse, die sog. "Universal Ressource Location" (URL), die ja inzwischen jeder dank des WWW kennen dürfte.
Diese wird, wie in Java üblich, als eigene Klasse definiert. Ein Aufruf sieht z.B. so aus:
Java:
try
{
	meineurl= new URL("http://java.seite.net/index.html");
} catch (MalformedURLExceoption e)
{
	getAppletContext().showStatus("Unzulässige URL:" + meineurl);
}
Damit mögliche Fehler abgefangen werden ummanteln wir die Erzeugung mit einem try { } catch { }.
Damit haben wir eine Verbindung zu dem angegebenen Server und in diesem Fall zu einer Datei geöffnet. Jetzt müssen wir nur noch die Daten laden: Dafür bietet Java die manchem schon aus C++ bekannte Möglichkeit der streams.


Streams

Ein Stream ist, einfach übersetzt, ein Strom von Daten, oder, anders ausgedrückt, eine Verbindung zwischen zwei Punkten, wobei die eine Seite Information liefert und die andere Seite diese Informationen empfängt. Das Schöne an einem Stream ist, dass es für beide Seiten vollkommen unwichtig ist, wer oder was an der anderen Seite hängt: Der Sender der Informationen kann z.B. eine Datei sein, Eingaben über eine Tastatur oder eine Datenverbindung zu einem anderen Rechner, der Empfänger ein Programm, eine Datei usw.
Sogenannte Filter haben sowohl als Eingabe als auch als Ausgabe einen Stream: Sie lesen Daten aus dem Eingabestream, verarbeiten sie und geben sie dann auf dem Ausgabestream wieder aus. Damit lassen sich Streams kombinieren und beliebig hintereinander hängen: Eine sehr mächtige Eigenschaft der Streams.
Um Daten aus der oben angegebenen URL lesen zu können, müssen wir also einen Stream dahin öffnen. Die URL-Klasse definiert eine Methode openStream() die genau dies erledigt. Als Rückgabewert erhält man eine Instanz der Klasse InputStream. Der folgende Codeausschnitt liest Zeile für Zeile aus der obigen URL, einer HTML-Seite, und gibt diese aus:
Java:
try
{
	InputStream is=meineurl.openStream();
	DataInputStream dis=new DataInputStream(is);
} catch (IOException e) {} //Bemerkung java-forum.org: Exceptions nicht unbehandelt lassen, mindestens eine Textausgabe auf der Konsole!

while(true)
{
	try
	{
		line=dis.readLine();
		if(line==null) break;
		System.out.println(line);
	} catch (IOException e) {} //Bemerkung java-forum.org: Exceptions nicht unbehandelt lassen, mindestens eine Textausgabe auf der Konsole!
}

Sockets, Client/Server & das Chat-System

Sockets

Die mit den Streams beschriebene Lösung bietet zwar eine Möglichkeit zur Kommunikation (es werden Daten von einer Stelle zur anderen übertragen), es findet jedoch keine Interaktion, also der Austausch von Daten, statt. Zwar könnte man auch einen Ausgabestream zu einer bestimmten URL öffnen, hinter der sich dann ein CGI-Programm verbirgt, das die Daten entgegennimmt - das dem Internet zugrunde liegende Übertragungsprotokoll TCP/IP bietet für Zweiwegekommunikation jedoch eine bessere Möglichkeit: Sockets.

Eine der Eigenschaften des TCP/IP-Protokolls ist, dass die Daten, in Paketen verpackt, hin- und hergeschickt werden. Damit diese Datenpakete nun den richtigen Empfänger erreichen, wird jedem Paket eine sog. IP-Adresse zugeordnet. Im Internet besitzt jeder Server eine eigene IP-Adresse, mittels derer man jeden beliebigen Server weltweit eindeutig ansprechen kann.

Stellen Sie sich vor, auf diesem Rechner sind, wie z.B. unter UNIX üblich, mehrere User zu Gange und haben sich über das Internet in andere Rechner eingewählt und verrichten dort verschiedene Sachen. Aber auch ein einziger Benutzer kann ja verschiedene Verbindungen zu verschiedenen Servern gleichzeitig aufbauen. Damit sich diese Verbindungen nun nicht gegenseitig in die Quere kommen wird jedem Datenpaket eine weitere Nummer, die sog. Port-Nummer mitgegeben. Anhand dieser Portnummer kann nun jedes Datenpaket genau dem Programm zugeordnet werden, das diese Verbindung nutzt.

Diese Kombination aus Internet-Adresse und Port nennt man eben Sockets.

Einige dieser Ports sind reservierte, standardisierte Ports, die bestimmten Netzdiensten zugeordnet sind, z.B. besitzt Telnet die Portnummer 23 und das WWW die 80. Alle Portnummern unter 256 sind für diese Standarddienste reserviert. Zwischen 256 und 1024 befinden sich UNIX-spezifische Dienste wie z.B. rlogin.
Alle Nummern darüber, bis 16384 sind frei. Unser selbstprogrammiertes Chat-System hat natürlich keine Standard-Portnummer, wir können uns also eine Zahl aussuchen, im Beispiel nehmen wir einfach die 8765.


Client/Server

Mithilfe der Sockets wollen wir nun im folgenden das Chat-System programmieren, das es Nutzern, die ein bestimmtes Java-Applet geladen haben, erlaubt, sich miteinander zu unterhalten (engl. "to chat" = schwatzen).

Da die Applets sich aufgrund der Sicherheitsbeschränkungen nicht direkt miteinander verbinden dürfen und ja vor allem auch erstmal nichts voneinander wissen, ist das Chat-System nach dem Client/Server-Prinzip aufgebaut: Auf einem Hauptrechner, dem Server, wird eine Applikation gestartet die laufend auf eingehende Verbindungen wartet. Auf diesem Rechner muss auch die HTML-Seite liegen, die das Chat-Applet enthält, folglich muss dort auch ein WWW-Server installiert sein. Wird die HTML-Seite von einem anderen Rechner, dem Client, geladen, startet dort das Applet und verbindet sich mit dem Server. Alle Eingaben werden an den Server geschickt, der diese wiederum an alle anderen, die sich mit ihm verbunden haben weiterleitet. Der Server fungiert also als eine Art Vermittler zwischen den Clients.


Das Chat-System

Insgesamt besteht das komplette Chat-System aus 3 Klassen: Dem Chat-Server, der auf dem Internet-Server als Applikation gestartet werden muss und auf eingehende Verbindungen von etwaigen Clients wartet, der Connection-Klasse, die diese Verbindungen dann übernimmt und den Datenaustausch mit den Clients regelt und schliesslich dem Client selbst, dem Chat-Applet, das im Browser des Nutzers läuft.

Der Chat-Server

Zuerst müssen wir die benötigten Standardklassenpakete importieren:
Java:
import java.net.*;
import java.io.*;
import java.util.*;
In java.net befinden sich die zur Internet-Kommunikation benötigten Klassen, wie Socket oder URL. Das Standardpaket java.io beherbergt die zur Ein-/Ausgabe nötigen Klassen, hauptsächlich Streams, und java.util eine Reihe nützlicher "Werkzeuge", z.B. die von uns benötigte Klasse zur Verwaltung von Listen.
Java:
public class chatserver implements Runnable
{
	...
}
Unsere Chat-Server-Klasse soll als eigenständiger Thread laufen und implementiert deshalb die Runnable-Schnittstelle.
Java:
public static final int PORT = 8765;
protected ServerSocket listen;
protected Vector connections;
Thread connect;
Nachdem wir die Klasse definiert haben, folgen die Variablendefinitionen. PORT ist eine willkürlich festgelegte Konstante, ServerSocket brauchen wir um auf dem Server den Socket zum "Lauschen" nach Verbindungen einzurichten. Der "Vektor" connections ist einfach eine Liste, in der wir die Verbindungen verwalten und connect schliesslich speichert den Thread der Chat-Server-Klasse.
Java:
public static void main(String[] args)
{
	new chatserver();
}
Die Einstiegsmethode erzeugt einfach eine Instanz unserer Klasse, deren Konstruktor ausgeführt wird.
Java:
public chatserver()
{
	try
	{
		listen = new ServerSocket(PORT);
	} catch (IOException e)
	{
		System.err.println("Fehler beim Erzeugen der Sockets:"+e);
		System.exit(1);
	}

	connections = new Vector();

	connect = new Thread(this);
	connect.start();
}
Der Konstruktor öffnet einen Socket unter dem angegebenen Port und fängt mögliche Fehler ab. Dann erzeugt er die Liste, die die Verbindungen verwaltet und sich selbst als neuen Thread und startet ihn.
Java:
public void run()
{
	try
	{
		while(true)
		{
			Socket client=listen.accept();

			connection c = new connection(this, client);
			connections.addElement(c);
		}
	} catch (IOException e)
	{
		System.err.println("Fehler beim Warten auf Verbindungen:"+e);
		System.exit(1);
	}
}
Der Laufteil des Programms befindet sich in einer Endlosschleife - ununterbrochen wartet der Server auf mögliche Verbindungsanfragen. Geht eine ein, erzeugt er für diese Verbindung ein neues connection-Objekt, das als eigenständiger Thread implementiert ist und die Behandlung dieser einen Verbindung übernimmt. Diese "Übergabe" ist deshalb notwendig, damit der Server danach wieder auf neue Verbindungsanfragen lauschen kann.
Java:
public void broadcast(String msg)
{
	int i;
	connection you;

	for (i=0; i<connections.size(); i++)
	{
		you = (connection) connections.elementAt(i);
		you.out.println(msg);
	}
}
Die broadcast-Methode durchläuft alle offenen Verbindungen und übermittelt diesen die übergebene Nachricht msg. Da nur die chatserver-Klasse über alle Verbindungen auf dem laufenden ist, muss sie auch als Bestandteil der chatserver-Klasse implementiert sein - obwohl sie von dieser garnicht benötigt wird, sondern von der nachfolgenden connection-Klasse.

Die Connection-Klasse

Die Connection-Klasse wird vom Chat-Server erzeugt und behandelt die eigentlichen Verbindungen zu den Clients.
Java:
import java.net.*;
import java.io.*;

class connection extends Thread
{
	...
}
Für jede eingegangene Verbindung wird ein eigenes connection-Objekt erzeugt, das diese übernimmt. Es soll als eigenständiger Thread laufen und wird deshalb von der Thread-Klasse abgeleitet.
Java:
protected Socket client;
protected DataInputStream in;
protected PrintStream out;
protected chatserver server;
client übernimmt den Socket vom Chat-Server, mit dem dann die beiden Ein- und Ausgabestreams in und out verbunden werden. server speichert den Zugriff auf das Chat-Server-Objekt.
Java:
public connection(chatserver server, Socket client)
{
	this.server=server;
	this.client=client;

	try
	{
		in = new DataInputStream(client.getInputStream());
		out = new PrintStream(client.getOutputStream());
	} catch (IOException e)
	{
		try { client.close(); } catch (IOException e2) {} ;  //Bemerkung java-forum.org: Exceptions nicht unbehandelt lassen, mindestens eine Textausgabe auf der Konsole!
		System.err.println("Fehler beim Erzeugen der Streams: " + e);
		return;
	}

	this.start();
}
Der Konstruktor der connection-Klasse merkt sich erst einmal die übergebenen Argumente, einen Verweis auf das Chat-Server-Objekt und den Socket, in dem es sie in eigenen Variablen speichert. Dann versucht er einen Eingabestream und einen Ausgabestream zu bekommen. Dabei möglicherweise auftretende Fehler werden vom catch() abgefangen.
Java:
public void run()
{
	String line;

	try
	{
		while(true)
		{
			line=in.readLine();
			if(line!=null)
				server.broadcast(line);
		}
	} catch (IOException e)
	{
		System.out.println("Fehler:" + e);
	}
}
Der Laufteil des connection-Objekts besteht aus einer Endlosschleife, die ständig alle hereinkommenden Nachrichten in Empfang nimmt und an alle anderen Verbindungen schickt - da er selbst nicht wissen kann, wer das eigentlich ist, muss dazu die broadcast-Methode des Server-Objekts verwendet werden.

Das Chat-Applet

Das Chat-Applet ist die einzige unserer 3 Klassen die grafische Ausgaben macht und benötigt deshalb, neben dem java.applet-Paket, das java.awt-Klassenpaket.
Java:
import java.net.*;
import java.io.*;
import java.awt.*;
import java.applet.*;

public class chatapplet extends Applet implements Runnable
{
	...
}
Das Chat-Applet ist natürlich ein Erbe der Applet-Klasse und implementiert Threads.
Java:
public static final int PORT = 8765;
Socket socket;
DataInputStream in;
PrintStream out;
TextField inputfield;
TextArea outputarea;
Thread thread;
PORT, socket, in und out haben weitgehend dieselbe Bedeutung wie in chatserver und connection beschrieben, ausser dass socket diesmal keinen Socket auf einem Server darstellt. inputfield speichert das Eingabefeld-Objekt und outputarea entsprechend den Ausgabebereich. thread schliesslich speichert den eigenen Thread.
Java:
public void init()
{
	inputfield = new TextField();
	outputarea = new TextArea();
	outputarea.setFont( new Font("Dialog", Font.PLAIN, 12));
	outputarea.setEditable(false);

	this.setLayout(new BorderLayout());
	this.add("South", inputfield);
	this.add("Center", outputarea);
		this.setBackground(Color.lightGray);
	this.setForeground(Color.black);
	inputfield.setBackground(Color.white);
	outputarea.setBackground(Color.white);
}
In der init-Methode wird der grafische Aufbau vorbereitet: Ein Eingabefeld wird erzeugt, ebenso der Ausgabebereich, der Zeichensatz wird gesetzt und das ganze so angeordnet, dass das Eingabefeld unten ist. Dann werden noch die Farben gesetzt.
Java:
public void start()
{
	try
	{
		socket = new Socket(this.getCodeBase().getHost(), PORT);
		in = new DataInputStream(socket.getInputStream());
		out = new PrintStream(socket.getOutputStream());
	} catch (IOException e)
	{
		this.showStatus(e.toString());
		say("Verbindung zum Server fehlgeschlagen!");
		System.exit(1); //Bemerkung java-forum.org: Hat in einem Applet nichts zu suchen!
	}

	say("Verbindung zum Server aufgenommen..."); //Bemerkung java-forum.org: Sollte besser am Ende des try-Blockes stehen

	if (thread == null)
	{
		thread = new Thread(this);
	thread.setPriority(Thread.MIN_PRIORITY);
		thread.start();
	}
}
In der Start-Methode versucht das Applet einen Socket zu öffnen. Die notwendige Server-Adresse wird anhand des HTML-Dokuments ermittelt und ist dadurch immer der Server von dem die HTML-Seite geladen wurde - da sowieso keine anderen Verbindungen erlaubt sind, würden andere Server auch gar keinen Sinn machen. Danach erzeugt es die damit verbundenen Ein- und Ausgabestreams; Fehler werden wie üblich abgefangen. Bei Gelingen wird im Ausgabebereich die entsprechende Nachricht ausgegebenen. Dann erzeugt er eine Instanz seiner Klasse als Thread und startet diese.
Java:
public void stop()
{
	try
	{
		socket.close();
	} catch (IOException e)
	{
		this.showStatus(e.toString());
	}

	if ((thread !=null) && thread.isAlive())
	{
		thread.stop(); //Bemerkung java-forum.org: stop() ist inzwischen deprecated und sollte nicht mehr verwendet werden!
		thread = null;
	}
}
Die Stop-Methode wird automatisch vom Browser aufgerufen, wenn der Nutzer die Seite verlässt. Der Socket wird geschlossen und der Thread gestoppt und freigegeben.
Java:
public void run()
{
	String line;

	try
	{
		while(true)
		{
			line = in.readLine();
			if(line!=null)
	outputarea.appendText(line+'\n' );
		}
	} catch (IOException e) { say("Verbindung zum Server abgebrochen"); }
}
Die Laufmethode des Applets entspricht vom Prinzip her der der connection-Klasse: In einer Endlosschleife werden die hereinkommenden Nachrichten eingelesen und im Ausgabebereich des Applets ausgegeben.
Java:
public boolean action(Event e, Object what)
{
	if (e.target==inputfield)
	{
		String inp=(String) e.arg;
	out.println(inp);
		inputfield.setText("");
		return true;
	}
	return false;
}
Die event-Methode wird aktiviert, wenn der Nutzer eine Eingabe macht. Wir testen ob auch das Eingabefeld der Auslöser war, holen uns die Eingabe und schicken sie an den Server. Das Eingabefeld wird danach zurückgesetzt.
Java:
public void say(String msg)
{
	outputarea.appendText("*** "+msg+" ***\n");
}
Die say-Methode gibt einfach übergebene Nachrichten mit Sternchen umrahmt aus.

Kompilieren & Starten

Übersetzen Sie nun den Chat-Server und das Chat-Applet:
javac chatserver.java
javac chatapplet.java
Die drei class-Dateien werden nun erzeugt. chatserver erzeugt automatisch die connection-Klasse.

Wenn Sie das neue JDK 1.1 benutzen, lassen Sie sich von den Warnmeldungen nicht verunsichern: Unsere Chat-Programme verwenden noch Methoden der Java-Version 1.0, die inzwischen durch neuere ersetzt wurden. Dies hat einen Grund: Da das Chat-Applet für den Einsatz im Browser vorgesehen ist und viele noch Netscape 3 oder gar 2 benutzen, würde das Chat-Applet bei diesen Usern garnicht funktionieren. Umgekehrt ist die Aufwärtskompatibilität jedoch gewährleistet, so dass unser Applet auch mit Java 1.1 funktioniert - trotz der Warnungen.

Vergessen Sie nun nicht den Chat-Server auch zu starten, ansonsten werden die Chat-Clients wenig Erfolg bei ihren Verbindungsversuchen haben:
java chatserver


Eine HTML-Seite

Jetzt brauchen wir nur noch eine kleine HTML-Seite, in die unser Applet eingebettet ist:
HTML:
<html><head><title>Chat</title></head>
<body bgcolor=000000 text=ffffff>

<center>
<applet code="chatapplet.class" width=95% height=60%>
</applet>
</center>

</body></html>
Speichern Sie diese HTML-Seite in demselben Verzeichnis in dem sich auch das Applet befindet ab und öffnen Sie mindestes zwei Browser-Fenster. Laden Sie nun in jedes die obige HTML-Seite: Viel Spass bei der Unterhaltung! ;-)

Wenn Sie sich nicht nur mit sich selbst unterhalten wollen, müssen Sie einen Web-Server installieren und ihren Rechner von aussen zugänglich machen. Auch wenn Sie nur über einen Wählzugang verfügen können Sie zumindest in den Zeiten, in denen Sie online sind andere in ihren Chat einladen.
Ansonsten dürfte es sich als schwierig erweisen den Server irgendwo unterzubringen - die wenigsten Provider werden ohne weiteres die chatserver-Applikation permanent auf ihrem Web-Server laufen lassen. Allerdings gibt es dafür kaum eine Begründung, weder das Netz, noch der Rechner werden durch die wenigen und kurzen Textübertragungen wirklich belastet.
Vielleicht gelingt es Ihnen ja, ihren Provider zu überreden...


Erst der Anfang

Natürlich handelt es sich bei dem vorgestellten Programm nur um ein sehr rudimentäres, wenn auch funktionsfähiges System. Wer z.B. den IRC kennt weiss, dass noch viele Features denkbar und möglich wären. Der erste Wunsch ist sicherlich die Möglichkeit, den einzelnen Nutzern Namen zuordnen zu können und dass die gerade im Chat befindlichen Nutzer angezeigt werden.

WordWrap, also das "Umklappen" zu langer Zeilen drängt sich auf und die Möglichkeit bestimmten Nutzern heimlich Nachrichten "zuzuflüstern" (private messages). Ganz dringend erforderlich wäre ein Mechanismus, der abgebrochene Verbindungen wieder entfernt oder "hängende" Verbindungen herauswirft.
Einen solch ausgebauten Chat, auf dem dieser hier basiert können Sie unter chat.seite.net oder natürlich im "Kaffee & Kuchen"-eigenen Java-Chat ausprobieren.

Das Endstadium markieren dann die grafischen Chats, in denen die Nutzer in virtuellen 3D-Welten umherspazieren und beliebige Charaktere annehmen können - der Phantasie sind keine Grenzen gesetzt.

Kompletter Quellcode (inkl. Fehler!): http://java.seite.net/chat/quellcode.txt
Quelle: Kaffee & Kuchen - Datenbank-Anbindung mit JDBC
 
Status
Nicht offen für weitere Antworten.

Oben