How to build a serverside JS module (DE)

Louis Charette edited this page Apr 19, 2014 · 1 revision

[http://www.ape-project.org/wiki/index.php/How_to_build_a_serverside_JS_module/de] = Wie erstelle ich ein JS Modul auf Server-Seite = Category:serverCategory:Tutorial

APE 1.0 bietet nun eine JavaScript Einbindung durch Tracemonkey ([https://developer.mozilla.org/en/SpiderMonkey/ SpiderMonkey] 1.8.1), von Morzilla an .

An die Entwickler , dies ist eine signifikante Änderung. Du brauchst nicht mehr länger zu wissen wie man in C programmiert , um eine hoch anpassbare APE unterstütze Applikation zu erstellen.

In seinem wesen ist es Framework-Agnostisch und mittels scripts/main.ape.js voll konfiguierbar . APE kommt mit dem [http://www.mootools.net MooTools framework] um die Entwicklung und das Verteilen von Modulen mittels seinem lesbaren Code sowie OOP Gestaltung und Serverseitigen Unterstützung zu erleichtern.

== Was kann ich mit serverseitigem JavaScript machen? ==

Manage :

  • Benutzer
  • Kanäle
  • Kommandos
  • RAWs
  • Events
  • Pipes
  • Socket API (beides Client und Server)
  • HTTP Requests API

=== Kommandos ===

==== RegisterCmd ====

Mit dem Folgenden können sie ein neues Server Komanndo registrieren :

	Ape.registerCmd("foocmd", true, function(params, infos) {

	});

Das registriert ein neues APE Kommando daß durch eine APE URL aufgerufen werden kann.

/?[{"cmd":"foocmd","chl":1,"sessid":"ba162c29abfe2126329bbceaf09ae269","params":{"foo":"bar"}}]

''Ape.registerCmd()'' benötigt 3 Argumente.

  • ('''string''') Der Kommando Name
  • ('''bool''') Braucht das Kommando einen eingeloggten Benutzer ? (sessid nötig)
  • ('''function''') Die 'Callback Funktion' die aufgerufen wird.

Die 'Callback Funktion' wird mit 2 Argumenten aufgerufen: *Das erste ist die Paramter Liste die vom Benutzer gesendet wird (in diesem Fall ''{"foo":"bar"}'') *Das zweite ist ein Objekt das einige nützliche Informationen enthält. ** host ('''string''') Http Kopfdaten : "Host :" ** client ('''socket''') Das Socket Objekt von dem Client ** chl ('''number''') Die Aufforderungsnummer ** ip ('''string''') client IP ** user ('''user''') Benutzer Objekt (fals angemeldet.) ** http ('''array''') beinhaltet die Benutzer HTTP Kopfdaten (headers).

Beispiel:

	Ape.registerCmd("foocmd", true, function(params, infos) {
		Ape.log("Die Benutzer IP : ("+infos.ip+"), foo : " + params.foo);
	});

===== Sende einen Fehler =====

Es muss nur ein Array mit '''Zwei''' Elementen zurück gegeben werden.

  • ('''string''') Fehler Code
  • ('''string''') Fehler Beschreibung
	Ape.registerCmd("foocmd", true, function(params, infos) {
		if (!$defined(params.john)) return 0; // sende ein "BAD_PARAMS" RAW zum Benutzer
		if (params.john != "doe") return ["209", "NOT_A_JOHN_DOE"];
		return 1;
	});

Der Benutzer erhält folgendes :

[{"time":"1255399359","raw":"ERR","data":{"code":"209","value":"NOT_A_JOHN_DOE"}}]

==== RegisterHookCmd ====

Einhängen eines existierenden Kommandos (um einige Argumente hinzuzufügen , für Instanzen)

	Ape.registerHookCmd("foocmd", function(params, infos) {
		if (!$defined(params.john)) return 0;
		return 1;
	});

Wie im Fehler-Beispiel , ''return 0'' wird ein "BAD_PARAMS" RAW zum Benutzer zurück senden.

Solange es manchmal kein ''user'' (Bentuzer) Objekt in ''Infos'' gibt (im Fall eines Kommandos ohne Session ID) gibt es einen speziellen Syntax um ein Fehler oder eigenes-RAW zu senden.

===== Sende ein eigenes RAW =====

Du musst ein speziell formatiertes Objekt zurück geben:

  • '''raw''' ** '''name''': ('''string''') Raw Name ** '''data''': ('''object''') Raw Inhalt
	Ape.registerHookCmd("foocmd", function(params, infos) {
		if (!$defined(params.john)) return 0;
		if (params.john != "doe") return ["209", "NOT_A_JOHN_DOE"];
		return {
			'raw':{'name':'CUSTOM_RAW','data':{'foo':'bar'}}
		};
	});

Der Benutzer erhält folgendes:

[{"time":"1255399359","raw":"CUSTOM_RAW","data":{"foo":"bar"}}]

=== RAW ===

Ein RAW ist eine "Nachricht" die du an eine '''Pipe''' senden kannst. (hinzufügen zur Pipe Nachrichten-Warteschlange).

Eine Nachricht ist wie folgt formatiert :

  • RAW.time ('''string''') aktueller Zeitstempel
  • RAW.raw ('''string''') Der raw Name
  • RAW.data ('''mixed''') Daten gesendet vom Server.

Beispiel:

{"time":"1255281320","raw":"FOOBAR","data":{"foo":"bar","irgendwas":["a","b"]}}

==== sendRaw ====

Jede APE Einheit hat eine Pipe (Benutzer , Kanäle , Proxy und so weiter).

pipe.sendRaw("CUSTOM_RAW", {"foo":"bar"});

Das sendet ein "CUSTOM_RAW" RAW zur ''Pipe''.

{"time":"1255281320","raw":"CUSTOM_RAW","data":{"foo":"bar"}}

Zur Ergänzung des vorherigen Beispieles:

	Ape.registerCmd("foocmd", true, function(params, infos) {
		Ape.log("Die Benutzer IP : ("+infos.ip+"), foo : " + params.foo);
		infos.user.pipe.sendRaw("CUSTOM_RAW", {"foo":"bar"});
	});

Sie können eine 'pipe' empfangen mittels seiner '''pubid''' (öffenltichen ID)

	Ape.registerCmd("foocmd", true, function(params, infos) {
		Ape.log("Die Benutzer IP : ("+infos.ip+"), foo : " + params.foo);
		Ape.getPipe(params.pubid).sendRaw("CUSTOM_RAW", {"foo":"bar"});
	});

Das sendet ein RAW zur Pipe mit der übereinstimmenden öffentlichen ID aus params.puid.

==== sendResponse ====

Mit registerCmd oder registerHookCmd mussen Sie manchmal eine Antwort senden.

In der Tat können Sie ein einfaches ''infos.user.pipe.sendRaw'' benutzen, aber der Client kann keine Callback Antwort bearbeiten, auf das Kommando welches es gesendet hat.

Um diese Lücke zu schliessen , kannst folgendes direkt benutzen:

	Ape.registerCmd("foocmd", true, function(params, infos) {
		infos.sendResponse('custom_raw', {'foo':'bar'});
	});

Das fügt das erhaltene ''chl'' vom Kommando zum RAW hinzu.

=== Events ===

Du kannst auf "Spezielle Events" höhren , die da wären :

  • '''adduser''': function(user) wenn sich ein Benutzer zur APE verbindet.
  • '''deluser''': function(user) wenn ein Benutzer die Verbindung aufhebt.
  • '''beforeJoin''' / '''join''' / '''afterJoin''':function(user, channel) wenn ein Benutzer einen Kanal beitritt.
  • '''left''': function(user, channel) wenn ein Benutzer den Kanal verlässt.
  • '''mkchan''': function(channel) wenn ein neuer Kanal erzeugt wird.
  • '''rmchan''': function(channel) wenn ein Kanal gelöscht wird.
Ape.addEvent("adduser", function(user) {
	Ape.log("Neuer Benutzer :)");
});
Ape.addEvent("join", function(user, channel) {
	Ape.log("Neuer Benutzer ist dem Kanal ("+channel.getProperty('name')+") beigetreten :)");
});

Alle Objekte die zu Events geleitet werden sind dauerhaft. Das bedeutet das private Daten innerhalb ''user'', ''channel'', ... gepackt werden können.

Ape.addEvent("adduser", function(user) {
	Ape.log("Neuer Benutzer :)"); 
	user.foo = "bar"; //Like this.
});
Ape.addEvent("join", function(user, channel) {
	Ape.log("Neuer Benutzer "+user.foo+" ist dem Kanal ("+channel.getProperty('name')+") beigetreten. :)");
});

=== Sockets ===

APE JS Server-Seite liefert eine Komplette API für beides , Server und Client Sockets.

Was bedeutet das ?

Du kannst extrem starke Applikationen schreiben , wie diese : *IRC bots *HTTP querying *Web-Einbindungen *schreibe dein eigenes APE Protokoll (durch schreiben eines Server-Socket und verarbeiten von Events)

Alle Sockets sind nicht blockierend und Event gesteuert -> Hohe I/O Leistung ;)

==== Client ====

Schritte;

var socket = new Ape.sockClient(port, host, {flushlf: true});

Hier verbinden wir uns mit host:port.

'''flushlf''' (bool) meint das '''onRead''' Event wird nur ausgelöst wenn ein \n empfangen wurde (Daten werden dort geteilt ) z.b. foo\nbar\n ruft '''onRead''' zweimal mit "foo" und "bar". Andererseits, '''onRead''' wird ausgelöst solange daten kommen.

socket.onConnect = function() {
	Ape.log("Wir sind verbunden !");
	this.write("Hallo\n");
}

Das defeniert ein Callback zum Aufrufen wenn die Verbingung ''ready'' ist. '''this''' Objekt bezieht sich auf den Socket selbst.

socket.onRead = function(data) {
	Ape.log("Data : " + data);
}

Dieser Callback wird aufgerufen wenn neue Daten ''ready'' sind. Fals '''flushlf''' auf ''true'' gesetzt wurde sind führende '\n' Zeichen nicht in ''data'' vorhanden

socket.onDisconnect = function() {
	Ape.log("Wech !");
}

onDisconnect wird aufgerufen wenn die Verbindung geschlossen wird. (Vom Client oder vom Server).

Um eine Verbindung zu schliessen benutze:

socket.close()

Um Daten zu schreiben benutze :

socket.write(data)

==== Server ====

Schritte:

var socket = new Ape.sockServer(port, "0.0.0.0", {flushlf: true});

Das bindet "0.0.0.0" (alle für die Maschine zugewiesenen IP's ) auf den gegebenen Port.

socket.onAccept = function(client) {
	Ape.log("New client !");
	client.write("Hello world\n");
}

'''onAccept''' wird ausgelöst wenn ein neuer Client sich verbindet.

Andere 'Callbacks' (gleiche API wie ''socketClient'') :

  • onRead = function(client, data){}
  • onDisconnect = function(client)
  • client.close()
  • client.write(data);

Denk daran das du private Daten in die Objekte von '''client''' sichern kannst um sie später wieder Aufzurufen.

socket.onAccept = function(client) {
	client.foo = "bar";
	client.write("Hallo Welt\n");
}
socket.onRead = function(client, data) {
	Ape.log(client.foo);
}

=== Benutzer ===

Ein Benutzer wird erzeugt wenn ein Client ein "CONNECT" Kommando sendet und der Login erfolgt ist.

Du kanst einige Aktionen mittels des JavaScript ''user'' Objektes machen:

  • Setzen/Holen von Öffentlichen oder Privaten Eigenschaften
  • senden von RAW's

==== setProperty ====

Diese Funktion setz eine öffentliche Eigenschaft die an jeden Benutzer, in Beziehung zu sich selber (gleicher Kanal , private Nachricht), gesendet wird.

user.setProperty('foo', 'bar');

Die ''foo' Eigenschaft can nun auf der Client-Seite (JSF) empfangen werden .

Der Wert kann entweder ein String oder ein Integer oder ein Objekt / Array sein

==== getProperty ====

Diese Funktion holt die öffentlichen Eigenschaften.

var prop = user.getProperty('foo');

==== Private Eigenschaften ====

Das JavaScript ''user'' Objekt wird erzeugt wenn ein Benutzer sich mit der APE verbindet und wird zerstört wenn die Verbindung verlassen oder durch den Server beendet wird.

Dieses Objekt ist persistent. Das bedeutet das man alles in dem Objekt speichern kann um es später wieder nutzen zu können.

Ape.addEvent("adduser", function(user) {
	user.foo = "bar";
});

Ape.registerCmd("helloworld", function(params, infos) {
	Ape.log(infos.user.foo);
});

==== getUserByPubid ====

Um einen ''user'' mittels seiner ''pubid'' zu erhalten:

var user = Ape.getUserByPubid(pubid);

==== Benutzer Pipe ====

Jedes ''user'' Objekt hat ein ''pipe'' Objekt um ein RAW zu senden.

user.pipe.sendRaw("RAW", {"foo":"bar"});

==== beitreten (join) ====

Erzwingt einen Benutzer einen Kanal beizutreten:

user.join('mychannel'); /* can be either a channel name or a channel object */

==== verlassen (left)====

Erzwingt einen Benutzer einen Kanal zu verlassen:

user.left('mychannel'); /* kann wahlweise ein Kanal oder ein Kanal Objekt sein.  */

=== Kanäle ===

''channel'' Objekte arbeiten genauso wie ''user'' Objekte.

Du kannst private oder öffentliche Eigenschaften setzen oder erhalten.

==== getChannelByName ====

Um ein ''channel'' Objekt mittels seines Namens zu erhalten :

var channel = Ape.getChannelByName('foochannel');
channel.setProperty('foo', 'bar');
channel.myprivate = {'my':'private'}; // kann ein String sein oder was immer du willst
channel.pipe.sendRaw('FOORAW', {'John':'Doe'});

Ape.addEvent('beforeJoin', function(user, channel) {
	Ape.log('Mein private : ' + channel.myprivate);
});

==== getChannelByPubId ====

Um ein ''channel'' Objekt mittels seiner ''pubid'' zu erhalten :

var channel = Ape.getChannelByPubid(pubid);

==== mkChan ====

Erzeuge ein neuen Kanal :

var channel = Ape.mkChan('foo');

==== delChan ====

Lösche einen vorhandenen Kanal :

var channel = Ape.delChan('foo'); /* kann ein String oder ''channel' Objekt sein */

=== Pipes ===

Wie vorher schon gesehen , eine Pipe ist ein Objekt das eine Art Verbinder durch die RAW's gesendet werden.

Jede Pipe hat einen einzigartigen Identifizierer, '''pubid''' , der erhalten werden kann durch Benutzung des Folgenden :

pipe.getProperty('pubid');

Das Server-Seitige JS bietet einen Weg um deine eigene Pipe, mit eigener Umgebung, zu erzeugen :

var mypipe = new Ape.pipe();

Das iniziiert eine neue Pipe wo Benutzer ''data'' mittels des Kommandos '''SEND''' senden können.

{"cmd":"SEND","chl":1,"sessid":"04ad0814f987e5f9891bffd6a73ef5a1","params":{"pipe":"6a3ae905fb508aff6f1e84458038f262","data":{"foo:"bar"}}}
// wo die "pipe" Eigenschaft die öffentliche ID (pubid) von der Pipe ist.

Um das das Kommando handhaben zu können , bietet das ''pipe'' Objekte ein einfachen 'Callback' :

var mypipe = Ape.pipe();
mypipe.onSend = function(user, params) {
	Ape.log(params.data.foo);
	/* tuirgendwas(); */
}

Du kannst genauso eine private/öffentliche Eigenschaft bei einer Pipe setzen wie bei einem Benutzer oder Kanal Objekt.

  • pipe.setProperty('schlüssel', wert);
  • pipe.getProperty('schlüssel');
  • pipe.myprivate

=== MySQL ===

APE Server erkaubt es sich mit einem MySQL Datenbank zu verbinden.

==== Verbinden ====

Tip : Fals du APE's MySQL Funktionalität nutzen willst, beachte das ''libmysqlclient-dev'' für die Debian versions (auch für Ubuntu) installiert ist und bei Redhat Distributionen (auch für CentOS) ''mysql-devel'' installiert ist .
var sql = new Ape.MySQL("ip:port", "benutzer", "Passwort", "Datenbank"); 

Du musst einen Port angeben . In der Grundeinstellung benutzt MySQL 3306

Ab jetzt musst ein Benutzer und ein Passwort festgelegt werden, das MySQL Modul unterstütz das Verbinden mit einem Benutzer ohne Passwort nicht.
Tips : Der locale MySQL Unix Socket kann genutzt werden mit ''/var/run/mysqld/mysqld.sock'' als Hostname

'''Verbindungs Callback'''

  • onConnect : Callback wird ausgelöst wenn eine Verbindung zum Server erfolgreich eingerichtet wurde.
sql.onConnect = function() {
    Ape.log('Verbinde zu meinem MySQL Server');
}
  • onError : Callback ausgelöst wenn ein Verbindungfehler entsteht
sql.onError = function(errorNo) {
    Ape.log('Verbindungsfehler : ' + errorNo + ' : '+ this.errorString()); 
}

==== Senden einer Anfrage (request) ====

SELECT Anfrage :

sql.query("SELECT * FROM table", function(res, errorNo) {
    if (errorNo) Ape.log('Anfrage Fehler : ' + errorNo + ' : '+ this.errorString()); 
    else {
        Ape.log('Abholen ' + res.length);
        res.each(function(data) {
            Ape.log(data.content);//data.<Spalten Name> or data[Spalten_Name]
        }); 
    }
});

INSERT Anfrage :

sql.query("INSERT INTO table VALUES('a','b','c')", function(res, errorNo) {
    if (errorNo) Ape.log('Anfrage Fehler : ' + errorNo + ' : '+ this.errorString()); 
    else Ape.log('Eingefügt ' + this.getInsertId());
});
'''Tips:''' Um den letzten auto-incement Wert zu bekommen , benutze ''this.getInsertId()'' innerhalb der Callback Funktion

==== Auskommentieren ====

Um den Server vor SQL-injections zu schptzen müssen die Input Daten mit '''Ape.MySQL.escape()''' auskommentiert werden:

sql.query("SELECT nick FROM user WHERE login = '"+Ape.MySQL.escape(mylogin)+"'");
Ab jetzt ist eine Callback Funktion unumgänglich , fals es nicht gewollt ist das deine Anfrage eine Callback Funktion haben soll, benutz $empty als zweites Argument

=== Zubehöhr ===

Ape serverseitige JS bieten diese nützlichen Allgemeinen Funktionen:

==== Base64 ====

Kodiert / Dekodiert Daten mit MIME base64

var xxx = Ape.base64.encode('foo');
var foo = Ape.base64.decode(xxx);

==== SHA1 ====

Berechnet den sha1 Hash von einem String (oder Binär).

===== sha1.str() =====

Gibt eine 40-Zeichen Hexadezimalzahl zurück.

var result = Ape.sha1.str("Hallo Welt");

Du kanst auch eine HMAC-SHA1 Ergebnis bekommen durch angeben des geheimen Schlüssels als zweites Argument.

var result = Ape.sha1.str("Hallo Welt", "meingeheimerschlüssel");

===== sha1.bin() =====

Der sha1 digest wird stattdessen in RAW Binär format mit der Länge 20 zurück gegeben.

var result = Ape.sha1.bin("Hallo Welt");

(Anmerkung: Du kannst ein HMAC_SHA1 Ergebnis bekommen indem du sowas wie sha1.str() benutzt ).

==== Xorize ====

Anwenden eines 'XOR' zwischen zwei Strings ( oder Binären) :

var result = Ape.xorize("key1", "key2");

Intern benutzter Algorithmus :

for (i = 0; i < key1_len; i++) {
	returned[i] = key1[i] ^ key2[i];
}

(Anmerkung : Die länge des zweite Argumentes muss höher sein als die des ersten Argumentes.)

==== Zeitmesser ====

Javascript bietet keine Zeitmesser Funktion aus sich selbst heraus. APE bietet eine Zeitmesser API sowie ein Browser es tut (mit der gleichen APi wie FireFox).

===== setTimeout =====

Führt eine Funktion ,nach der angegebenen Verzögerung ( Millisekunden), aus.

var timeoutID = Ape.setTimeout(func, delayms, [param1, param2, ...]);
var timeoutID = Ape.setTimeout(function(a, b) {
	Ape.log("Foo : " + a + " Bar : " + b);
}, 3000, "foo", "bar");

===== setInterval =====

Wiederholter aufruf einer Funktion mit einer festen Zeitverzögerung zwischen jedem Aufruf.

var timeoutID = Ape.setInterval(func, delay[, param1, param2, ...]);

===== clearTimeout =====

Zeitmesser stoppen gesetzt mit Ape.setTimeout() oder Ape.setInterval().

Ape.clearTimeout(timeoutID)
var timeoutID = Ape.setInterval(function(a, b) {
	Ape.log("Foo : " + a + " Bar : " + b);
}, 3000, "foo", "bar");

Ape.clearTimeout(timeoutID);

==== Einbinden (Include) ====

Einbinden und ausführen der angegebene Datei im aktuellen Kontext .

include('./scripts/foo.js');

Ape.log('Einige Variablen setzten in foo.js : ' + myfoovar);

== Fall Studie ==

Proxy.js :

Ape.registerCmd("PROXY_CONNECT", true, function(params, infos) {
	if (!$defined(params.host) || !$defined(params.port)) {
		return 0;
	}
	var socket = new Ape.sockClient(params.port, params.host);
	socket.chl = infos.chl;
	
	socket.onConnect = function() {
		/* "this" bezieht sich aufs Socket Objekt */
		/* Erzeugt eine neue Pipe (mit einer öffentlichen ID (pubid)) */
		var pipe = new Ape.pipe();
		
		infos.user.proxys.set(pipe.getProperty('pubid'), pipe);
		
		/* Setzt einige private Eigenschaften */
		pipe.link = socket;
		pipe.nouser = false;
		this.pipe = pipe;
		
		/* Wird aufgerufen wenn ein Benutzer ein "SEND" Kommando an diese Pipe sendet.*/
		pipe.onSend = function(user, params) {
			/* "this" bezieht sich aufs Pipe Objekt */
			this.link.write(Ape.base64.decode(params.msg));
		}
		
		pipe.onDettach = function() {
			this.link.close();
		}
		
		/* Sendet ein  PROXY_EVENT Raw zu einem Benutzer und hängt an eine Pipe an. */
		infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "connect", "chl": this.chl}, {from: this.pipe});
	}
	
	socket.onRead = function(data) {
		infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "read", "data": Ape.base64.encode(data)}, {from: this.pipe});
	}
	
	socket.onDisconnect = function(data) {
		if ($defined(this.pipe)) {
			if (!this.pipe.nouser) { /* Benutzer ist nicht mehr verfügbar  */
				infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "disconnect"}, {from: this.pipe});
				infos.user.proxys.erase(this.pipe.getProperty('pubid'));
			}
			/* Pipe zerstören */
			this.pipe.destroy();
		}
	}
	
	return 1;
});

Ape.addEvent("deluser", function(user) {
	user.proxys.each(function(val) {
		val.nouser = true;
		val.onDettach();
	});
});

Ape.addEvent("adduser", function(user) {
	user.proxys = new $H;
})