Geschrieben von lsc am 17.12.2005, 03:12

In diesem Tutorial werden wir einen kleinen Online-Chat erstellen, der ohne speziellen Server auskommt. PHP und eine Datenbank genügen.

Anforderungen

Für "blutige" Anfänger ist dieses Tutorial nicht unbedingt geeignet. Grundlagen in PHP und ActionScript werden vorausgesetzt, da eine Erläuterung "from the scratch" den Rahmen eines Tutorials wohl sprengen würde.

Wie eine Datenbankverbindung hergestellt und eine Tabelle angelegt wird, wird vorausgesetzt; ebenso die Unterschiede zwischen $_GET und $_POST und vielleicht das Anlegen einer Funktion in PHP. Was ActionScript betrifft: Man sollte vielleicht schon mal ein XML-Objekt eingelesen haben und wissen, wie man auf die Knoten im Baum zugreift. Auch auf die Grundlagen zum Erstellen von Schaltflächen aus MovieClips und die Behandlung von dynamischen Textfeldern gehe ich nicht im einzelnen ein.

Vorüberlegungen

Es wird ein Chat-Client erstellt, mit dem mehrere User Postings verschicken und anzeigen können. Man meldet sich mit einem Nickname an, wobei eine aktuelle Liste aller angemeldeten Teilnehmer angezeigt wird.

Da wir keine dauerhafte Verbindung zum Server aufrechterhalten, muss der Flash-Client in regelmäßigen Abständen eine Anfrage abschicken und das Ergebnis verarbeiten. Bei jeder Anfrage (unhabhängig, ob ein Posting verschickt oder eine andere Aktion ausgeführt wird) müssen sowohl die aktuelle Benutzerliste als auch die neuesten Beiträge vom Server übermittelt werden. Um nicht bei jeder Anfrage das komplette Chatlog übertragen zu müssen, muss jedem User die zuletzt erhaltene Nachricht zugeordnet werden. So kann ein Client nur die Nachrichten erhalten, die seit der letzten Anfrage gepostet worden sind.

Die Datenbank

Wir benötigen für diesen Chat zwei Tabellen; eine für die aktiven Benutzer (chat_user) und eine für die Beiträge (chat_history):

Code:

Tabelle "chat_user"
--------------------------------------------------
Feldname Datentyp Attribute
--------------------------------------------------
id BIGINT UNSIGNED, AUTOWERT
nickname TEXT
user_id TEXT
last_id BIGINT UNSIGNED
last_action DATETIME
--------------------------------------------------
Primärschlüssel ist das Feld "id"

Code:

Tabelle "chat_history"
--------------------------------------------------
Feldname Datentyp Attribute
--------------------------------------------------
id BIGINT UNSIGNED, AUTOWERT
msg_time DATETIME
user_id TEXT
message TEXT
--------------------------------------------------
Primärschlüssel ist das Feld "id"

Wir speichern die ID der zuletzt abgerufenen Nachricht im Feld "last_id" des Users und halten gleichzeitig die Zeit in "last_action" fest. Kommt von einem Benutzer innerhalb eines bestimmten Intervalls keine Anfrage (z.B. wenn jemand das Browserfenster schließt, ohne sich auszuloggen), so kann der Benutzer anhand dieses Zeitstempels aus der Datenbank gelöscht werden. Genauso verhält es sich mit den Einträgen: Nach einer bestimmten Zeitspanne können alte Einträge aus der "chat_history" gelöscht werden, damit die Tabelle nicht endlos anwächst.

Jeder Benutzer bekommt beim Login eine eindeutige ID zugewiesen. Ich habe dafür die PHP-Funktion uniqid verwendet, die eine auf dem aktuellen Zeitstamp basierende Zeichenfolge zurückgibt. Bei der zu erwartenden (geringen) Anhahl gleichzeitig teilnehmender Clients sollte das vollauf genügen.

Das PHP-Script

Dieser Chat kommt mit einem einzelnen PHP-Script aus, das - je nach Anfrage - verschiedene Aktionen ausführt:
login: Es wird ein Benutzername übergeben und ein neuer User in der Benutzertabelle angelegt.

logout: Es wird eine Benutzer-ID übergeben und der entsprechende User aus der Tabelle entfernt.

posting: Es werden eine Benutzer-ID und ein Text übergeben. Der Text wird als neues Posting in die Tabelle "chat_history" eingetragen.

update: Es wird eine Benutzer-ID übergeben, anhand derer die neuen Nachrichten (sprich: die, die der Benutzer noch nicht erhalten hat) ausgegeben werden (diese Aktion wird bei allen Anfragen durchgeführt)

Zusätzlich wird bei jeder Anfrage die aktuelle Benutzerliste ausgegeben

Wir verbinden uns zunächst mit der Datenbank. Dafür verwende ich eine eigene Datei, die in die chat.php inkludiert wird:
Code:

// connect.php
function db_connect() {
global $db_link;
$db_link = mysql_connect('localhost', 'username', 'password') or die('Could not connect: '.mysql_error());
}

function db_select() {
mysql_select_db('datenbank_name') or die('Could not select database');
}

In der chat.php (die Hauptdatei) werden zunächst zwei Variablen für die Intervalle, nach denen User oder Einträge gelöscht werden, angelegt:
Code:

$timeout = 30; // Sekunden
$timeout_msg = 2; // Stunden
30 Sekunden sollten für die Benutzertabelle genügen, da der Client periodisch alle paar Sekunden eine Anfrage übermitteln wird. Sparsame können die Eintragstabelle auch in Sekunden- oder Minutenintervallen bereinigen - ich habe hier einen Zeitraum von einigen Stunden gewählt (vielleicht will sich der Admin ja das Chatlog auch noch einige Zeit später ansehen ).

Danach wird überprüft, ob eine gültige Aktion übergeben wurde:
Code:

if (isset($_POST["action"])) {
include("connect.php");
db_connect();
db_select();

checkTimeout();

switch($_POST["action"]) {
case "login": doLogin($_POST["nickname"], $_POST["uid"]); break;
case "logout": doLogout($_POST["uid"], true); break;
case "posting": doPosting($_POST["message"], $_POST["uid"]); break;
case "update": doUpdate($_POST["uid"], false); break;
}
}

Wir inkludieren die "connect.php" und verbinden uns mit der Datenbank. Bei jeder Anfrage wird zunächst die Funktion "checkTimeout()" aufgerufen, die alte Einträge aus den beiden Tabellen entfernt:
Code:

function checkTimeout() {
global $timeout, $timeout_msg;
$query = 'DELETE FROM chat_user WHERE last_action < DATE_SUB(NOW(), INTERVAL '.$timeout.' SECOND)';
$result = mysql_query($query) or doError(mysql_error());
$ar = mysql_affected_rows();
$query = 'DELETE FROM chat_history WHERE msg_time < DATE_SUB(NOW(), INTERVAL '.$timeout_msg.' HOUR)';
$result = mysql_query($query) or doError(mysql_error());
if ($ar > 0 || mysql_affected_rows() > 0) {
initTables();
}
}
Damit werden alle User gelöscht, die seit dem Intervall von $timeout Sekunden nichts mehr haben von sich hören lassen - und ebenfalls alle Einträge, die den Zeitraum $timeout_msg überschritten haben. In gewissen Abständen bereinige ich ebenfalls die Auto-Werte der Primärschlüssel, damit sie nicht ins Endlose anwachsen. Das wird von der Funktion "initTables()" erledigt:
Code:

function initTables() {
$query = 'SELECT * from chat_history WHERE 1';
$result = mysql_query($query) or doError(mysql_error());
$lines = mysql_num_rows($result);
if ($lines == 0) {
$query = 'ALTER TABLE chat_history AUTO_INCREMENT=0';
$result = mysql_query($query) or doError(mysql_error());
}
$query = 'SELECT * from chat_user WHERE 1';
$result = mysql_query($query) or doError(mysql_error());
$lines = mysql_num_rows($result);
if ($lines == 0) {
$query = 'ALTER TABLE chat_user AUTO_INCREMENT=0';
$result = mysql_query($query) or doError(mysql_error());
}
}
Wenn in einer Tabelle keine Einträge vorhanden sind, wird der Autowert (AUTO_INCREMENT) auf 0 zurückgesetzt.

Im ersten Teil des Scriptes werden (je nach übergebener Aktion) unterschiedliche Funktionen ausgeführt: doLogin(nickname [, user_id]), doLogout(user_id), doPosting(message, user_id) oder doUpdate(user_id).

Befassen wir uns zunächst mit dem Login. Der Flash-Client übergibt den vom Benutzer gewählten Nickname (und vielleicht auch eine vorhandene User-ID: Dann wird zunächst ein Logout durchgeführt, gefolgt von einem Login mit dem neuen Namen):
Code:

function doLogin($nickname, $uid) {
if (isset($uid)) {
doLogout($uid, false);
}
if (!nickExists($nickname)) {
$uid = uniqid(rand());
$query = 'SELECT id FROM chat_history WHERE 1 ORDER BY id DESC LIMIT 1';
$result = mysql_query($query) or doError(mysql_error());
if ($line = mysql_fetch_assoc($result)) {
$lid = $line["id"];
} else {
$lid = 0;
}
$query = 'INSERT into chat_user (nickname, user_id, last_id, last_action) VALUES("'.$nickname.'", "'.$uid.'", "'.$lid.'", NOW())';
$result = mysql_query($query) or doError(mysql_error());
doUpdate($uid, false);
} else {
doError("Nick exists");
}
}

function nickExists($nick) {
$query = 'SELECT id from chat_user WHERE nickname="'.$nick.'"';
$result = mysql_query($query) or doError(mysql_error());
if (mysql_affected_rows() > 0) {
return true;
} else {
return false;
}
}
Wird eine Benutzer-ID ($uid) übergeben, so wird zuerst die Funktion "doLogout()" aufgerufen.

Als nächstes müssen wir feststellen, ob ein gewählter Nickname vielleicht schon existiert. Dafür sorgt die Funktion "nickExists()". Sie bekommt den Nickname übergeben und sucht in der Tabelle nach Benutzern mit identischem Nickname. Man könnte hier zwischen GRoß- und Kleinschreibung keinen Unterschied machen, aber ich habe darauf verzichtet (warumm soll es keine zwei User geben, von denen einer H4XX0R und der andere h4xx0r heist? ).

Existiert der Nickname schon in der Tabelle, so wird eine Funktion "doError()" aufgerufen, die nichts weiter macht, als eine Fehlermeldung in einem XML-Tag zurückzugeben und das Script zu beenden:
Code:

function doError($msg) {
echo '<error msg="'.$msg.'" />';
exit();
}

Existiert der gewählte Name noch nicht, so wird ein entsprechender Eintrag in der Tabelle angelegt und die id der neusten Nachricht in der History abgefragt. Der neue User bekommt diese ID als letzte ID zugewiesen, damit er nach dem Login keine Nachrichten sieht, die vor seinem Eintritt gepostet worden sind. Am Ende wird die Funktion doUpdate() aufgerufen, die die Benutzerliste und die neusten Postings ausgibt (wir kommen später zu dieser Funktion).

Die Funktion doLogout() entfernt einfach den Datensatz mit der übergebenen ID aus der Tabelle:
Code:

function doLogout($uid, $update) {
$query = 'DELETE FROM chat_user WHERE user_id = "'.$uid.'"';
$result = mysql_query($query) or doError(mysql_error());
if ($update) doUpdate($uid, true);
}
Hier wird die Funktion "doUpdate" mit dem Wert "false" für den zweiten Parameter übergeben. In diesem Fall wird nur die Userliste, aber keinen neuen Postings ausgegeben.

Diese Funktion ( "doUpdate()" ) ist im Prinzip die umfangreichste des Scripts:
Code:

function doUpdate($uid, $logout) {
if ($uid) {
// Userliste ausgeben:
$query = 'SELECT * FROM chat_user WHERE 1';
$result = mysql_query($query) or die(mysql_error());
$last_id = 0;
if ($logout) {
$myid = "";
} else {
$myid = $uid;
}
$ustr = '<users myid="'.$myid.'">';
while ($line = mysql_fetch_assoc($result)) {
if ($line["user_id"] == $uid) {
$last_id = $line["last_id"];
}
$ustr .= '<user id="'.$line["user_id"].'" nick="'.$line["nickname"].'" />';
}
$ustr .= '</users>';
// Letzte Postings ausgeben:
if (!$logout) {
$query = 'SELECT * FROM chat_history WHERE id > "'.$last_id.'" ORDER BY msg_time ASC';
$result = mysql_query($query) or doError(mysql_error());
$hstr = '<messages>';
while ($line = mysql_fetch_assoc($result)) {
$hstr .= '<msg uid="'.$line["user_id"].'" time="'.$line["msg_time"].'" ><![CDATA['.$line["message"].']]></msg>';
$last_id = $line["id"];
}
$hstr .= '</messages>';
// Letzte ID speichern:
$query = 'UPDATE chat_user SET last_id="'.$last_id.'", last_action=NOW() WHERE user_id = "'.$uid.'"';
$result = mysql_query($query) or doError(mysql_error());
} else {
$hstr = "<messages />";
}
doOutput($ustr, $hstr);
} else {
doError("not logged in");
}
}
Es werden zwei Paramater übergeben: Eine User-ID und ein Flag, das bestimmt, ob ein Logout durchgeführt wurde.

Zunächst wird die Userliste als XML-Baum zusammengestellt. Die übergebene User-ID wird im Hauptknoten mit ausgegeben. Nach einem Login wird diese nämlich vom Flash-Client übernommen und bei allen weiteren Anfragen mitgesendet. Nur nach einem Logout wird keine ID (bzw. ein leerer String) ausgegeben, damit der Client weiß, dass er seine zugewiesene ID löschen muss. Die komplette Userliste kann als XML-Baum z.B. so aussehen:
Code:

<users myid="3f3basd5fasd2ge2w">
<user id="fwe5g13traa8sd5a2" nick="Ein User" />
<user id="3f3basd5fasd2ge2w" nick="Datic />
</users>
Der "eigene" Benutzer (bei dem "id" und "myid" übereinstimmen) wird in in der Userliste im Client hervorgehoben.

Nun werden ebenfalls alle Postings ausgegeben, die seit der letzten Aktion des Users gepostet worden sind. Dafür sorgt diese Abfrage:
Code:

SELECT * FROM chat_history WHERE id > "'.$last_id.'" ORDER BY msg_time ASC
In der Variable $last_id steht die id des zuletzt erhaltenen Postings. Nach der Ausgabe werden dieser Wert und der aktuelle zeitraum im Benutzereintrag festgehalten:
Code:

UPDATE chat_user SET last_id="'.$last_id.'", last_action="'.$now.'" WHERE user_id = "'.$uid.'"
Der XML-Baum für die Postings kann z.B. so aussehen:
Code:

<messages>
<msg uid="3f3basd5fasd2ge2w" time="2005-07-25 14:38:00"><![CDATA[Ein Eintrag]]></msg>
<msg uid="fwe5g13traa8sd5a2" time="2005-07-25 14:39:20"><![CDATA[Noch ein Eintrag]]></msg>
</messages>
Der einzige Unterschied besteht darin, dass wir die Nachrichten URL-codiert übermitteln und auch so in der Datenbank ablegen. Damit sind wir auch vor SQL-Injektionen gefeit und sparen uns ein mysql_escape_string().

Zuletzt werden die beiden Bäume per echo() ausgegeben. Das übernimmt die Funktion "doOutput()":
Code:

function doOutput($user, $msg) {
echo ($user.$msg);
}

Weiter geht's in Teil 2
.
#2
Datic 27.07.05, 00:15
Der Flash-Client

Wenden wir uns nun dem Flash-Teil des Tutorials zu, der diesmal zugegebenermaßen fast sparsamer als das PHP-Script ausfällt. Ich poste das Tutorial trotzdem im Flashbereich, weil der Flash-Client der eigentliche Sinn dieser Sache ist. Natürlich wäre auch ein html-Client möglich, der mit versteckten iFrames oder AJAX arbeitet - das ist aber nicht wirklich mein Metier. Im Übrigen verstehe ich von Flash mehr als von PHP - soll heissen: Ich hoffe erklärt zu haben, welche Anforderungen an das serverseitige Script gestellt werden müssen; was ich hier aber explizit fabriziert habe, ist im Endeffekt wohl reduntanter als nötig und lässt sich hinsichtlich der Performance (speziell die Anzahl der Abfragen) sicher erheblich verbessern. Wer Lust hat, probiert einmal aus, z.B. die komplette User-Tabelle komplett in ein Array einzulesen und sich die nötigen Daten daraus zu ziehen und vergleicht die Geschwindigkeitsunterschiede in der Vararbeitung.

Wir benötigen im Client hauptsächlich drei "Fenster" (sprich: Textfelder):
Die "History", in der die Nachrichten angezeigt werden

Ein Eingabefeld für eigene Beiträge

Eine Benutzerliste

Dazu kommt noch ein kleines Textfeld als Statusanzeige, z.B. für Fehlermeldungen.

Wir beginnen mit dem Hauptfenster für die Nachrichten:

Die Objekte in den meisten Ebenen dienen zur Verzierung - Rahmen, Hintergrundflächen etc. Wichtig ist hauptsächlich ein großes mehrzeiliges Textfeld (Instanzname "tf_history") im html-Modus sowie ein Rollbalken (Instanzname "scroller"). html-Textfeld deshalb, weil wir z.B. Benutzernamen und Uhrzeit in unterschiedlichen Farben anzeigen wollen. Als Rollbalken verwende ich den Textfeld-Scroller, der auf meiner Seite heruntergeladen werden kann.

Zuerst initialisieren wir den Scrollbalken:
Code:

this.onEnterFrame = function() {
scroller.init(280, tf_history);
delete this.onEnterFrame;
}

Dann fügen wir einen MouseListener hinzu, damit das Textfeld auch mit dem Mausrad gescrollt werden kann:
Code:

var l = new Object();
l.onMouseWheel = function(delta) {
if (_xmouse > tf_history._x && _xmouse < tf_history._x + tf_history._width) {
if (_ymouse > tf_history._y && _ymouse < tf_history._y + tf_history._height) {
tf_history.scroll -= delta;
}
}
}
Mouse.addListener(l);

Nun brauchen wir hauptsächlich zwei Funktionen zum Hinzufügen einer Nachricht und zum Löschen des gesamten Textfeldes:
Code:

function addMessage(nick, t, msg) {
var ostr = "<br>";
ostr = '<font color="#AA0000">' + nick + '</font>';
ostr += '<font color="#00AAFF"> [' + t + ']: </font>';
ostr += msg;
tf_history.htmlText += ostr;
tf_history.scroll = tf_history.maxscroll;
}

function clearMessage() {
tf_history.text = "";
}

Die Funktion "addMessage()" bekommt einen Nickname, eine Uhrzeit und eine Nachricht übergeben und fügt diese Daten ins Texfeld ein. Mit der Zeile
Code:

tf_history.scroll = tf_history.maxscroll;
wird dafür gesorgt, dass das Textfeld beständig nach unten scrollt, neue Nachrichten also automatisch sichtbar sind.

Als nächstes erstellen wir einen ähnlichen MovieClip für die Userliste:

Das Textfeld bekommt den Instanznamen "tf_user", der Rollbalken wieder "scroller".

Wir initialisieren den Rollbalken und legen eine Funktion zum Anzeigen der Liste an:
Code:

this.onEnterFrame = function() {
scroller.init(280, tf_user);
delete this.onEnterFrame;
}

function updateList(list, myid) {
var ostr = "";
for (var i=0; i<list.length; i++) {
if (i == myid) {
ostr += '<font color="#33CCFF">' + list[i].nick + '</font>' + '<br>';
} else {
ostr += list[i].nick + '<br>';
}
}
tf_user.htmlText = ostr;
}
Dazu ist zu sagen, dass die Benutzerliste in Form eines Arrays aus Objekten übergeben wird. Jeder Eintrag in dem Array verfügt über die Eigenschaften ".id" und "nick" - nur der Nickname wird hier allerdings verwendet. Zusätzlich übergeben wir der Funktion einen Index "myid", der bestimmt, um welchen User in dem Array es sich um uns selbst handelt. So können wir den "eigenen" Namen in der Liste optisch durch eine andere Textfarbe hervorheben.

Für die Statusanzeige erstellen wir ebenfalls einen MC, wobei hier wahrscheinlich ein einzeiliges Textfeld genügt, wenn nur kurze Meldungen angezeigt werden sollen. Dieser Clip bekommt nur eine Funktion zum Anzeigen einer Meldung:
Code:

function setStatus(msg) {
tf_status.text = msg;
}
(Das Textfeld hat hier den Instanznamen "tf_status")

Zuletzt nehmen wir uns das Eingabefeld und die übrigen nötigen Steuerelemente vor. Ich habe die Buttons zum Ein- und Ausloggen, Posten usw. ebenfalls in dem MovieClip mit dem Eingabefeld untergebracht. Wir erstellen also zuerst ein entsprechendes Schaltflächenobjekt. Wie bei den meisten meiner Menüs ruft der Button beim Klicken auf seinem "Elternobjekt" eine Funktion "pressed()" auf, der eine Referenz auf den Button selber übergeben wird:
Code:

this.onRelease = function() {
_parent.pressed(this);
}

Zudem benötigen Wir ein weiteres Steuerelement mit einem kleinen Eingabefeld, in dem der Benutzer seinen Nickname eingeben kann. Der Inhalt des Eingabefeldes sollte über eine Membervariable namens "_value" ausgelesen werden können. Dafür sorgt man beispielsweise mit einem onChanged-Handler auf dem Eingabefeld:
Code:

var _value = "";

etext.onChanged = function() {
_value = this.text;
}
("etext" sei der Instanzname des Eingabefeldes)

Ausserdem begrenze ich die erlaubten Zeichen für den Nickname:
Code:

etext.restrict = "a-zA-Z0-9._ öäüÖÄÜß";
und setze einen Key-Listener, der bei Betätigung der Eingabetaste direkt einen Login durchführt:
Code:

var lx = new Object();
lx.onKeyUp = function() {
if (Selection.getFocus().indexOf("etext") >= 0 && Selection.getFocus() != null && Key.getCode() == 13) {
that._parent.aimLogin();
}
}
Key.addListener(lx);
Die Funktion "aimLogin()" wird auf dem Elternobjekt angelegt und kann auch über eine Schaltfläche aufgerufen werden.

Nun können wir also unser "Eingabepanel" zusammensetzen:

Wie man sehen kann, habe ich drei Buttons ("but_login", "but_logout", "but_submit") und eine Instanz des kleinen Eingabefeldes ("tf_nickname") verwendet. Ausserdem kommt auch hier wieder ein großes Textfeld ("tf_entry", diesmal natürlich ein Eingabefeld ohne html-Formatierung) und ein Rollbalken ("scroller") zum Einsatz.

Wir initialisieren den Scroller und weisen den Buttons ihre Beschriftungen zu. Dies geschieht über eine onEnterFrame-Methode in meinem Button, die darauf wartet, dass die Variable "_caption" gesetzt ist:
Code:

this.onEnterFrame = function() {
scroller.init(95, tf_entry);
delete this.onEnterFrame;
}

but_login._caption = "LOGIN";
but_logout._caption = "LOGOUT";
but_submit._caption = "SUBMIT";

Hier legen wir auch die Funktion "pressed()" an, die von den Buttons aufgerufen wird. Über einen switch-Block können wir aufragen, welcher Button gedrückt worden ist:
Code:

function pressed(obj) {
switch(obj) {
case but_login: aimLogin(); break;
case but_logout: aimLogout(); break;
case but_submit: aimSubmit(); break;
}
}

Die drei aufgerufenen Funktionen sorgen nun für das Login/out und das Versenden von Nachrichten:
Code:

function aimLogin() {
if (chat_root.chat_userID == "") {
if (tf_nickname._value != "") {
chat_root.doLogin(tf_nickname._value);
} else {
chat_root.setStatus("Enter Nickname");
}
} else {
chat_root.setStatus("Allready logged in");
}
}

Zwischenbemerkung: Ich muss an manchen Stellen auf Funktionen und Variablen eingehen, die im Tutorial erst an späterer Stelle behandelt werden. Ich versuche, die einzelnen Elemente in einer möglichst günstigen Reihenfolge zu beschreiben - allerdings lassen sich Vorgriffe nicht immer vermeiden.

An dieser Stelle daher zur Erklärung:

"chat_root" ist eine globale Variable, die auf die Hauptzeitleiste des Client (_root) zeigt. Ich verwende derartige globale Variablen, damit die Filme problemlos in container geladen werden können.

Der Nickname und die zugewiesene User-ID werden in den Variablen "chat_nickName" und "chat_userID" abgelegt.

Die Funktion "aimLogin()" testest zuerst, ob die User-ID nicht belegt ist (leere Zeichenkette). Nur dann wird ein Login durchgeführt - andernfalls wird eine Fehlermeldung "Allready logged in" ausgegeben. Nun wird der Inhalt des kleinen Eingabefeldes (tf_nickname._value) überprüft. Wenn nichts eingegeben wurde, wird ebenfalls kein Login durchgeführt. Die Funktion "doLogin()" wird auf der Hauptebene verwendet, um dem PHP-SCript den eingegebenen Nickname und die Aktion "login" zu übergeben.

Ähnlich reagiert die Funktion "aimLogout()". Nur wenn eine User-ID existiert, wird der Logout tatsächlich durchgeführt:
Code:

function aimLogout() {
if (chat_root.chat_userID.length > 0) {
chat_root.doLogout();
} else {
chat_root.setStatus("Allready logged out");
}
}

Zuletzt sorgt die Funktion "aimSubmit()" für das Verschicken eines Beitrages - allerdings nur, wenn a) der Benutzer eingeloggt ist (sprich: eine gültige User-ID existiert) und b) auch ein Text in das Haupteingabefeld ("tf_entry") eingegeben wurde:
Code:

function aimSubmit() {
if (chat_root.chat_userID != "") {
if (tf_entry.text.length > 0 && tf_entry.text != chr(13)) {
chat_root.doPosting(tf_entry.text);
} else {
chat_root.setStatus("Enter Message");
}
} else {
chat_root.setStatus("Not logged in");
}
tf_entry.text = "";
}

Um das Posten von Beiträgen zu erleichtern, kann diese Aktion auch durch eine Tastenkombination ausgeführt (Umschalt + Enter) werden. Dafür sorgt auch hier ein Key-Listener:
Code:

var l = new Object();
l.onKeyUp = function() {
if (Selection.getFocus().indexOf("tf_entry") >= 0 && Selection.getFocus() != null) {
if (Key.isDown(Key.SHIFT) && Key.getCode() == 13) {
aimSubmit();
}
}
}
Key.addListener(l);

Nun sind alle Komponenten komplett und wir können uns dem Hauptteil des Scriptes zuwenden. Wir ziehen zunächst alle vier Fensterkomponenten auf die Bühne und geben ihnen entsprechende Instanznamen:
history: Das Fenster für die Nachrichtenanzeige

userlist: Die Benutzerliste

type_win: Das Eingabefeld mit den Schaltflächen

status_win: Die Statusanzeige

Nun legen wir zuerst die schon erwähnten Variablen für Hauptebene, Nickname und User-ID an:
Code:

_global.chat_root = this;

var chat_userID = "";
var chat_nickName = "";

Des weiteren benötigen wir natürlich noch ein paar andere Objekte:
Code:

var checking = false;

var timeout = 2000;

var iv = 0;

var users = new Array();

var xm = new XML();
xm.ignoreWhite = true;
xm.onLoad = function() {
parseUpdate(this);
}
checking: Ein Flag, das angibt, ob nach dem Empfang von Daten weiterhin periodisch Anfragen an den Server gesendet werden sollen. Nach einem Logout soll das natürlich nicht mehr der Fall sein.

timeout: Das Intervall in Milisekunden, in dem die Daten aktualisiert werden sollen.

iv: Ein Intervallhandler für das Abfrageintervall

users: Ein Array für die Benutzerliste

xm: Ein XML-Objekt in das die Antworten des Servers geladen werden

Die Funktionen zum Ein- und Ausloggen sowie zum Posten von Beiträgen machen im Prinzip nichts weiter, als dass sie ein LoadVars-Objekt mit den nötigen Übergabeparametern füllen und an die "chat.php" schicken. Dabei wird die sendAndLoad-Methode verwendet, der das XML-Objekt als Ladeziel übergeben wird:
Code:

function doUpdate() {
var lv = new LoadVars();
lv.action = "update";
lv.uid = chat_userID;
clearInterval(iv);
lv.sendAndLoad("chat.php", xm, "POST");
}

function doLogin(nick) {
setStatus("login: " + nick);
var lv = new LoadVars();
lv.action = "login";
lv.nickname = nick;
checking = true;
history.clearMessage();
clearInterval(iv);
lv.sendAndLoad("chat.php", xm, "POST");
chat_nickName = nick;
}

function doLogout() {
setStatus("logout");
var lv = new LoadVars();
lv.action = "logout";
lv.uid = chat_userID;
checking = false;
clearInterval(iv);
lv.sendAndLoad("chat.php", xm, "POST");
}

function doPosting(msg) {
setStatus("posting...");
var lv = new LoadVars();
lv.action = "posting";
lv.uid = chat_userID;
lv.message = escape(msg);
checking = true;
clearInterval(iv);
lv.sendAndLoad("chat.php", xm, "POST");
}

Die setStatus-Funktion ruft einfach die gleichnamige Methode im Statusfenster auf, um uns ein kleines Feedback über unsere Aktionen zu geben. Die Variable "checking" wird in allen Funktionen ausser der doLogout() auf "true" gesetzt, damit nach dem Laden der Daten das Intervall wieder gesetzt werden kann. Ich lösche das Intervall vor jeder Anfrage per clearInterval(), damit es bei Verzögerungen (vielleicht antwortet der Server nicht schnell genug oder die Verbindung bricht kurzzeitig ein) nicht zu Überschneidungen kommt. Immerhin habe ich ein Stantardintervall von 2 Sekunden gewählt, was bei langsamen Verbindungen schon mal in den Bereich der Antwortzeit rücken kann.

In der onLoad-Methode des XML-Objektes ist die Funktion "parseUpdate()" aufgerührt, die die Antwort des Servers verarbeitet:
Code:

function parseUpdate(obj) {
var ulist = obj.childNodes[0];
var messages = obj.childNodes[1];
parseUsers(ulist);
parseMessages(messages);
if (checking) iv = setInterval(function() { doUpdate(); }, timeout);
}

Wir bekommen ja einen XML-Baum mit zwei Hauptknoten (<users> und <messages>) übergeben. Diese beiden Knoten übergeben wir an getrennte Funktionen für die Benutzerliste und die Nachrichten ( parseUsers() und parseMessages() ) und setzen das Intervall neu auf die Funktion doUpdate(), die die Daten aktualisiert. Wurde ein Logout durchgeführt, ist "checking" gleich "false" und das Intervall wird nicht wieder gestartet.

Die Funktion "parseUsers()" liest die übergebenen Benutzerdaten aus und schreibt sie in das Array "users":
Code:

function parseUsers(obj) {
if (obj.nodeName == "error") {
setStatus("ERROR: " + obj.attributes.msg);
} else {
users = new Array();
if (obj.attributes.myid != chat_userID) {
chat_userID = obj.attributes.myid;
}
var mid = -1;
for (var i=0; i<obj.childNodes.length; i++) {
users[i] = new Object();
users[i].nick = obj.childNodes[i].attributes.nick;
users[i].id = obj.childNodes[i].attributes.id;
if (chat_userID == obj.childNodes[i].attributes.id) mid = i;
}
userlist.updateList(users, mid);
}
}

Wir erinnern uns: Bei Abfragefehlern im PHP-Script wird lediglich ein Knoten mit einer Fehlermeldung ausgegeben, den wir an dieser Stelle abfangen. Die Meldung wird im Statusfenster ausgegeben.

Zunächst wird die zurückgegebene User-ID übernommen - nach einem Login muss der Client seine ID natürlich kennen. Nach einem Logout wird an dieser Stelle eine leere Zeichenkette übergeben, was der Client als Bestätigung auffassen kann. Besteht die Variable "chat_userID" aus einer leeren Zeichenkette, so ist der Benutzer nicht eingeloggt. Wir schreiben die Attribute der Childknoten in das Benutzerarray und merken uns den Index des "eigenen" Benutzers, den wir der updateList-Methode der Userliste übergeben (dort wird anhand dieses Indexes der betreffende Name optisch hervorgehoben).

Die Funktion parseMessages() arbeitet ähnlich und übergibt alle empfangenen Nachrichten an das History-Fenster:
Code:

function parseMessages(obj) {
if (obj.nodeName == "error") {
setStatus("ERROR: " + obj.attributes.msg);
} else {
for (var i=0; i<obj.childNodes.length; i++) {
var node = obj.childNodes[i];
var user = getUserName(node.attributes.uid);
var mtime = niceDate(node.attributes.time);
var msg = parseEt(node.firstChild);
history.addMessage(user, mtime, msg);
}
}
}

Wir bekommen eine User-ID übergeben, wollen aber den dazugehörigen Benutzernamen anzeigen. Dafür sorgt die Funktion "getUserName()":
Code:

function getUserName(id) {
for (var i=0; i<users.length; i++) {
if (users[i].id == id) {
return users[i].nick;
}
}
return "&lt;logged out&gt;"
}
Wir haben ja die aktuelle Userliste zuvor ebenfalls übergeben bekommen und können dort nach der entsprechenden ID suchen. Wird der zur ID gehörende Benutzer nicht gefunden (das kann sein, wenn er sich in dem Moment ausgeloggt hat, in dem die Daten übergeben wurden), so wird die Zeichenkette "<logged out>" zurückgegeben - natürlich mit < und > als html-Entitäten, damit sie in unserem html-Textfeld nicht als Tag interpretiert werden.

Das Datum wird (Datentyp DATETIME) im Format YYYY-MM-DD HH:MM:SS ausgegeben - uns interessiert aber im Moment nur die Uhrzeit, was die Funktion "niceDate()" für uns erledigt:
Code:

function niceDate(str) {
var t = str.split(" ")[1];
return t;
}
Wer mag, kann hier auch noch die Sekunden herauskürzen.

ZUletzt müssen wir die eigentliche Nachricht, die per escape() an den Server geschickt wurde (und auch in dieser Form zurückgegeben wird) wieder in ein anständiges Format bringen. Dafür schreiben wir noch fix eine Funktion namens "parseEt()" (fragt mich bitte nicht, warum die so heisst):
Code:

function parseEt(obj) {
var str = unescape(obj.toString());
var et = new Array('<', '>', chr(10));
var sg = new Array('&lt;', '&gt;', '');
for (var i=0; i<et.length; i++) {
str = str.split(et[i]).join(sg[i]);
}
return str;
}
Diese Funktion entfernt doppelte Zeilenumbrüche ( chr(10) ) und wandelt die Zeichen < und > in html-Entitäten um. Wir wollen ja nicht, dass Benutzer im html-Quelltext des Textfeldes herumpfuschen können.

Das War es auch schon fast... der Vollstädigkeit halber darf natürlich die Funktion setStatus() nicht fehlen, die lediglich ihr Pendant im Statusfenster aufruft:
Code:

function setStatus(msg) {
status_win.setStatus(msg);
}
Ich habs gerne übersichtlich - man kann natürlich auch direkt die Methode des Objekts "status_win" aufrufen.

Nun ist tatsächlich alles komplett und ich hoffe, dass Ihr mit diesem Tutorial
etwas anfangen könnt. Die Sourcedateien gibts wie immer im Anhang (eine kleine Chatlog-Anzeige ist auch noch dabei)."Anhang:" www.insidercoders.de/minichat.zip

Viel Spaß!

Bewertung Anzahl
6
83,3 %
10 Bewertungen
5
8,3 %
1 Bewertungen
1
8,3 %
1 Bewertungen