Websockets Ade
Wer schon mal versucht hat Websockets einzusetzen, hat bestimmt schon erlebt, wie unnötig kompliziert alles ist. Hat man die Clientseite gemeistert, die zugegebener Massen einfach ist. Wird die Serverseite zum Horrorgang. Wohl gemerkt, immer mit der Prämisse, dass man alles selbst programmiert und keine Software anderer einbindet.
Wozu Websockets?
Man kann Websockets dazu nutzen, dass man im Webbrowser über Aktionen sofort informiert wird. Der gern genommen Chatserver soll hier als Beispiel dienen. Wäre doch cool, ohne Flash und anderem einfach ein Chat in HTML anzubieten. Würde dann auch auf allen mobilen Geräten funktionieren und im Browser deiner Wahl.
Allerdings gibt es keine direkte Verbindung vom Server zurück zum Browser, wenn die Seite ausgeliefert wurde. Wenn nun einer im Chat was schreibt, müssten die anderen Chatter immer wieder nach Aktualisierungen beim Server fragen. Das ganze muss dann noch synchronisiert werden, damit die Antworten passen in der Reihenfolge angezeigt werden. Wenn das Intervall der Abfragen an den Server für Änderungen niedrig ist, dann kommt alles sehr verzögert an. Sind die Intervalle zu hoch, legen wir Browser und Server lahm.
Das alles wird mit den Websockets behoben. Der Browser baut eine Verbindung zum Server auf und hält diese offen. Sobald eine Aktualisierung im Server bekannt wird (ein Chatter hat was gesagt), sendet der Server diese Nachricht an alle offenen Websockets und der Browser schreibt mittels Javascript die Nachricht sofort auf die Webseite.
Das Ganze ist auch Ideal um Spiele und anderes Event bezogenen Überwachungen im Webbrowser zu realisieren. Kein Installieren und dennoch up2date.
Toll, doch was ist nicht gut?
Nicht gut ist, dass Websockets noch nicht von allen Browser unterstützt wird und noch weniger der Otto-Normal-Entwickler dies auf seinem angemieteten Server zusammen programmieren kann.
Wir brauchen eine Lösung, die mit jedem Browser (egal wie alt funktioniert). Allerdings benötigen wir mindestens Javascript bei allen Lösungen.
Alternativen
Ok, was können wir machen. Das übliche Überlegen. Nicht immer sofort das neueste implementieren, sondern sehen, ob wir es dem Kunden nicht so einfach wie möglich machen wollen.
Nach einer kurzen Analyse des Browser sehen wir einen Ansatzpunkt. Der Browser erstellt per Javascript ein Script-Tag im Header. Dieser dynamisch nachgeladene Javascript Code realisiert die Aktualisierungen. Das habe ich im Apicall-Verfahren schon demonstriert. Doch dann muss das Intervall hochgemacht werden?
Nein. Wir erinnern uns an damals, als das Internet noch sehr langsam war. Damals wurde einfach gewartet. Das tun wir in unserer Lösung auch.
Die meisten Browser brechen nach 1 Minute Wartezeit ab. Also setzen wir hier an und wählen 30-40 Sekunden. Der Browser verbindet sich mit dem Server. Der Server antwortet nicht sofort. Sondern speichert diese Verbindung global. Kommt nun eine Aktualisierungsmeldung. Werden alle gehaltenen (verzögerten) Javascript -Code Wünsche durchlaufen und jeder bekommt die Aktualisierung gemeldet.
Der Browser baut daraufhin sofort wieder eine Verbindung mittels Apicall auf. Auf dem Server lassen wir einen Job laufen, der immer wieder die veralteten Verbindungen beantwortet. Einfach eine leere Meldung in die Apicall Antwort einbauen. Dann ist alles sauber. So kann man auch komplett abgemeldete Verbindungen killen.
Das Ganze ist nicht so einfach mit PHP zu lösen. Doch es geht. Hier müsste man einen gemeinsamen Speicher nutzen. Meine Lösung ist in node.js realiesert. Ich bin einfach begeistert, was man mit nodejs machen kann. Schnell und einfach kann man wunderbare Sachen zaubern. Seht euch das Video auf der Seite an!
Code Beispiel für eine Chat-App:
HTML (Der Client):
Die Basics.js ist ein Sammlung von Hilfsfunktionen von Directpaylink:
http://directpaylink.com/javascript/basics.js
http://directpaylink.com/javascript/basics.js
Da ist auch viel unnötiges drin, so dass ich immer eine Kopie herunter lade und diese dann von Ballast befreie.
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
li {list-style-type:none;padding-left:2px;}
.msg:nth-of-type(even) { background: #dedede; }
.msg:nth-of-type(odd) { background: #fefefe; }
</style>
<script type="text/javascript" src="basics.js"></script>
<script type="text/javascript">
var system = {};
system.url = "http://localhost:8080/";
system.nohtml = true;
function konnektieren(adresse, usr) {
system.url = "http://"+adresse+":8080/?usr="+encodeURIComponent(usr);
system.usr = usr;
system.adresse = adresse;
message("Verbinde mit: "+system.url);
verbinden(true);
}
function verbinden(force) {
message("Hole Sessionid");
// wenn kein session code vorhanden
if (!force && system.sessionid)
return true;
var url = system.url;
apicall(url,
function(h) {
// sessionid als antwort
var t = h.responseText.split(";");
system.sessionid = t[0];
message("sessionid bekommen: "+system.sessionid);
document.getElementById("chat").style.display = "block";
message("gehe über auf Events zu warten. Chatten ist möglich");
system.wartesekunden = t[1];
setInhalt("info", system.wartesekunden);
warteAufEventsVomServer(system.wartesekunden);
document.getElementById("tt").select();
},
function(h){
message("fehler sessionid abzuholen. Kein weiterer versuch");
});
}
function warteAufEventsVomServer(wartesekunden) {
if (!wartesekunden)
wartesekunden = 20*1000;
if (!system.sessionid)
return alert("keine session vorhanden");
var url = system.url+"&sessionid="+system.sessionid;
// baue verbindung zum server auf
apicall(url,
function(h) {
if (checkResponse(h.responseText)) {
//message("chattext erhalten [EVENT]: "+h.responseText);
var code = h.responseText.substr(0,h.responseText.indexOf(":")).trim().toLowerCase();
switch(code) {
case "reload":
reload();
case "chat":
if (system.nohtml) {
message('<b>'+Date2Text()+" "+removeHTMLTags(decodeURIComponent(h.responseText.substr(6)))+'</b>');
} else {
message('<b>'+Date2Text()+"</b> "+decodeURIComponent(h.responseText.substr(6)));
}
// bei antwort, erneut verbinden
warteAufEventsVomServer();
}
} else { // error
system.sessionid = false;
verbinden(); // neue session aufbauen
}
},
function(h){
//message("kein event. warte auf den nexten event");
// bei timeout erneut verbinden
warteAufEventsVomServer(wartesekunden);
}, wartesekunden);
}
function reload() {
var t = "http://"+window.location.host+window.location.pathname+"?reload&usr="+
encodeURIComponent(system.usr)+"&sessionid="+
encodeURIComponent(system.sessionid)+"&adresse="+
encodeURIComponent(system.adresse)+"&wartesekunden="+
encodeURIComponent(system.wartesekunden);
document.location = t;
}
function usrlist() {
if (!system.sessionid)
return alert("keine session vorhanden");
var url = system.url+"&sessionid="+system.sessionid+"&cmd=usrlist";
apicall(url,
function(h) {
message(Date2Text()+" User-Liste:");
var liste = h.object;
for (var a=0;a<liste.length;a++)
message(Date2Text()+" " + liste[a]);
});
}
function senden(msg, elem) {
// sende den Text
if (!system.sessionid)
return alert("keine session vorhanden");
elem.select();
var url = system.url+"&sessionid="+system.sessionid+"&chat="+encodeURIComponent(system.usr+": "+msg);
apicall(url,
function(h) {
message(Date2Text()+": " + msg);
},
function(h){
message("chattext konnte nicht gesendet werden");
});
}
function setNOHTML(wert) {
system.nohtml = wert;
}
function message(text) {
var elem = document.getElementById("inhalt");
elem.innerHTML += '<li class="msg">'+text+'</li>';
document.getElementById("container").scrollTop = elem.offsetHeight;
}
function starten() {
system.REQUEST = basics_ermittelURLParameter();
if (system.REQUEST['reload']) {
// system.nohtml = system.REQUEST['nohtml'];
system.usr = system.REQUEST['usr'];
system.sessionid = system.REQUEST['sessionid'];
system.adresse = system.REQUEST['adresse'];
system.wartesekunden = system.REQUEST['wartesekunden'];
system.url = "http://"+system.adresse+":8080/?usr="+encodeURIComponent(system.usr);
document.getElementById("adr").value = system.adresse;
document.getElementById("usr").value = system.usr;
//warteAufEventsVomServer(system.wartesekunden);
}
}
window.onload=starten;
</script>
</head>
<body>
<form onsubmit="konnektieren(this.elements['adr'].value, this.elements['usr'].value);return false;">
chat.server: <input type=text id="adr" name="adr" value=""> <br>
username: <input id="usr" name="usr" type=text> <input type=submit value="connect">
</form>
<div id="chat" style="display:none;">
<input type=checkbox id="cnohtml" onchange="setNOHTML(this.checked)" checked> Keine HTML ausführen
<form onsubmit="this.elements['btn'].click();return false;">
<input type=text id="tt" name="tt"><button name="btn" onclick="senden(document.getElementById('tt').value, document.getElementById('tt'));return false;">senden</button>
</form>
<div id="container" style="height:300px;overflow-y:scroll;">
<div id="inhalt"></div>
</div>
<button onclick="reload()">reload</button>
<button onclick="usrlist()">usrlist</button>
<span id="info"></span>
</div>
<body>
</html>
Javascript für nodejs (Der Server)
var HTTP = require("http");
starten(); // damit kann man testzwecke besser steuern
function starten() {
webserver();
}
function webserver(meinserverport) {
if (!meinserverport)
meinserverport= 8080;
console.log("starte server");
function vb_clean() {
console.log("entferne abgelaufene sessions");
var ablauf = time() - (60*1000); // 1 minuten
for (var key in vb){
if (vb[key].lastconnect < ablauf) {
console.log("entferne", vb[key]['usr'], vb[key].sessionid);
var msg = vb[key].usr+" ist offline";
delete(vb[key]);
schreibeAllen("", msg);
}
}
setTimeout(vb_clean, 30*60*1000);
}
function schreibeAllen(sessionid, msg) {
if (!sessionid)
sessionid = "";
for (var key in vb) {
if (!vb[key].response)
continue;
if (key == sessionid)
continue;
var html = vb[key].callbackdaten['cbf']+"('"+vb[key].callbackdaten['cbid']+"',"+JSON.stringify("Chat: "+msg)+");";
vb[key].response.writeHead(200, {"Content-Type":"text/javascript"});
vb[key].response.write(html);
vb[key].response.end();
vb[key].lastconnect = time();
}
}
vb_clean();
var wartezeit = {'client':40*1000, 'server':45*1000};
var s = HTTP.createServer(function(request, response) {
console.log(request.connection.address());
console.log("Verbindungsaufbau: "+request.connection.remoteAddress, request.connection.remotePort);
var url = require('url');
var rq = url.parse(request.url, true);
//console.log(rq);
console.log(request.method, rq.pathname+rq.search);
var paras = rq.query;
// der weg über a kann später entfallen
// denkbar auch der Weg über die pfad angabe - url.parse(req.url).pathname
if (paras['js']) {
// sende das JS
// header('Content-Type: text/event-stream');
response.writeHead(200, {
"Content-Type":"text/javascript"
}); // verbindung ok
console.log("Starte Live-JS Eingaben:");
// machen wir über die tastatur, die ruft cbfkt auf für die Übermittlung
nehmeTastatur(function(t){
response.writeHead(200, {"Content-Type":"text/javascript", "Content-Length":t.length});
response.write(t);
}, function(){
response.end();
});
} else if (paras['cbf']) {
// apicall kommt rein
if (paras['sessionid']) {
if (paras['chat']) {
// msg ist eingetroffen
// sender ok senden
var html = paras['cbf']+"('"+paras['cbid']+"','OK;"+wartezeit.client+"');";
response.writeHead(200, {"Content-Type":"text/javascript"});
response.write(html);
response.end();
// verteile an alle erstmal
schreibeAllen(paras['sessionid'], paras['chat']);
} else {
if (paras['cmd']) {
console.log("usrliste senden");
response.writeHead(200, {"Content-Type":"text/javascript"});
var html = "";
switch (paras['cmd']) {
case "usrlist":
var liste = [];
for (var key in vb) {
if (!vb[key].response)
continue;
//if (key == paras['sessionid'])
// continue;
liste.push(vb[key].usr);
}
html = paras['cbf']+"('"+paras['cbid']+"',"+JSON.stringify(liste)+");";
break;
}
response.write(html);
response.end();
} else {
// wartet auf events
if (!vb[paras['sessionid']]) {
console.log("sessionid nicht bekannt: "+paras['sessionid']);
var html = paras['cbf']+"('"+paras['cbid']+"','ERROR: session nicht vorhanden');";
response.writeHead(200, {"Content-Type":"text/javascript"});
response.write(html);
response.end();
return;
}
vb[paras['sessionid']].response = response; // der browser wird nach 10sek abbrechen und sich erneut verbinden
vb[paras['sessionid']].lastconnect = time();
vb[paras['sessionid']].callbackdaten = {cbf:paras['cbf'],cbid:paras['cbid']};
setTimeout(function(){
console.log("schliesse veraltet Connection");
if (response) {
response.writeHead(200, {"Content-Type":"text/javascript"});
response.end();
}
}, wartezeit.server); // der client baut die verbindung für 20 Sek auf
}
}
} else {
if (!paras['usr'])
paras['usr'] = "guest_"+request.connection.remoteAddress+'_'+request.connection.remotePort;
// session erstellen und zurück geben
response.writeHead(200, {"Content-Type":"text/javascript"});
var sessionID = require("crypto").createHash("sha224").update("" + new Date().getTime() + Math.random() + '.sasoleindenys').digest("hex");
vb[sessionID] = {
'zeit': time(),
'lastconnect': time(),
'response':null,
'usr': paras['usr']
};
var html = "";
html += paras['cbf']+"('"+paras['cbid']+"','"+sessionID+";"+wartezeit.client+"');";
response.write(html);
response.end();
}
} else {
// normale HTTP verbindung
// sende das HTML
var html = "";
if (rq.pathname.match(/chatapp/)) {
console.log("sende die chatapp HTML datei");
response.writeHead(200, {"Content-Type":"text/html"}); // verbindung ok
html = require("fs").readFileSync('apicall_chat.html', "binary");
} else if (rq.pathname.match(/basics\.js/)) { // javascript hilfsfunktionen
console.log("sende die chatapp HTML datei");
response.writeHead(200, {"Content-Type":"text/javascript"}); // verbindung ok
html = require("fs").readFileSync('basics.js', "binary");
} else {
response.writeHead(200, {"Content-Type":"text/html"}); // verbindung ok
html = '<html><head><script src="?js=1" type="text/javascript"></script></head><body>Versuch es mal mit /chatapp</body></html>';
}
response.write(html, "binary");
response.end();
}
//response.writeHead(404, ""); // du bist hier falsch
//response.end();
}).listen(meinserverport);
function time(onlyseconds) {
var datum = new Date();
var milliseconds = datum.getTime();
if (onlyseconds)
return intval(milliseconds/1000);
return milliseconds;
}
var vb = {}; // webchat client
}
Fazit
Es geht auch ohne Websockets. Gerade wenn man auf Nummer sicher gehen will und auch den Server entsprechen nutzen kann. Mein Versuch hilft euch hoffentlich für viel bessere Lösungen als hier zusammengestellt.
Viel Spass
Saso Nikolov
Keine Kommentare:
Kommentar veröffentlichen