Mittwoch, 21. November 2012

Einfacher Webclient für POST und GET Formulare Senden Tests

Webclient für einfache POST und GET Requests

Wer kennt das nicht. Man will eben ein paar Abfragen an das neue Serverprogramm oder an die neue PHP-App senden und muss dabei die Parameter immer wieder anpassen. Mühselig, kann man die URL entsprechend in den Browser tippen, doch dann muss man plötzlich POST senden.

Damit man nicht immer wieder ein neues HTML-Formular oder ein spezielles PHP-Skript oder gar per Console eine Curl Aktion draus macht, wäre ein komfortables Webseiten-Tool doch ganz nett. Am besten gleich mit History, um einen Request nochmal senden zu können.

Voila:

 <html>  
 <head><title>Testclient für Webaufrufe</title>  
 <meta charset="utf-8">  
 <script type="text/javascript">  
 function makeURIParameterForm(text, form) {  
      // entferne alt lasten  
      // gehe über alle elemente  
      // wenn eines dyn gesetzt wurde entfernen, bevor die neuen kommem  
      var elemente = []; // werden entfernt
      for (var a=0;a<form.elements.length;a++){  
           var elem = form.elements[a];  
           if (elem.tag && elem.tag == "dyn"){  
                elemente.push(elem);  
           }  
      } 
      for (var a=0;a<elemente.lenght;a++)
           form.removeChild(elemente[a]);

      // extrahiere die elemente und  
      var teile = text.split("&");  
      for (var a=0;a<teile.length;a++){  
           var t = teile[a].split("=");  
           var elem = document.createElement("input");  
           elem.type = "hidden";  
           elem.value = t[1];  
           elem.name = t[0];  
           elem.tag = "dyn";   
           form.appendChild(elem);  
      }   
 }  
 function senden(form){  
           var object = {};  
           object.post = form.elements[1].checked;  
           object.target = form.elements[2].value.trim();  
           object.url = form.elements[0].value.trim();  
           object.body = form.elements[3].value.trim();  
           object.form = form;    
           liste.push(object);  
           var elem = document.createElement("option");  
           elem.value = liste.length - 1;  
           elem.text = "["+((object.post)?"POST":"GET")+"] "+object.body;  
           document.getElementById("liste").appendChild(elem);  
           fuelleForm(form, object);             
 }  
 function ladeForm(idx){  
      var object = liste[idx];  
      fuelleForm(object.form, object);  
      document.getElementById("info").innerHTML = "Form geladen: idx: "+idx;  
 }  
 function zeigeForm(idx) {  
      var object = liste[idx];  
      var text = "IDX: "+idx+"<br>";  
      for (var key in object)  
           if (key != "form")  
                text += '<b>'+key+"</b>: "+object[key]+"<br>";  
      document.getElementById("info").innerHTML = text;       
 }  
 function fuelleForm(form, object) {  
      form.target = object.target;  
      form.elements[3].value = object.body;  
      var url = object.url;  
      var body = object.body;
      if (url.indexOf("?")>0){
           var pos = url.indexOf("?");
           body = url.substr(pos+1)+"&"+body;
      }
      makeURIParameterForm(body, form);       
      if (object.post){  
           form.elements[1].checked = true;  
           form.method='POST';  
      } else {  
           form.elements[1].checked = false;  
           form.method='GET';  
      }  
      form.action = url;  
      document.getElementById("info").innerHTML = "Gesendet an: "+form.action;  
 }  
 var liste = [];  
 </script>  
 </head>  
 <body>  
 <div style="width:70%;float:left;">  
 Daten senden
 <form onsubmit="senden(this);return true;">  
 url: <input type=text style="width:70%;" hint="http://localhost/index.php"><br>  
 <input type=checkbox> POST<br>  
 target: <input type=text value="ziel"><br>  
 Body (bsp: para1=1213&amp;para2=2233):<br>  
 <textarea style="width:100%;"></textarea><br>  
 <div style="text-align:right;"><input type=submit></div>  
 </form>  
 </div>  
 <div style="width:25%;float:right;">  
 <form onsubmit="ladeForm(this.elements[0].options[this.elements[0].options.selectedIndex].value);return false;">  
      <select size=10 style="width:100%;" id="liste" onclick="zeigeForm(this.options[this.options.selectedIndex].value);">  
      </select><br>  
      <div style="text-align:right;"><input type=submit value="laden"></div>  
 </form>  
 </div>  
 <div style="clear:both;"></div>  
 <hr>  
 <div id="info"></div>  
 <hr>  
 <iframe name="ziel" style="width:100%;heigth:300px;boder:1px solid blue;"></iframe>  
 </body>  
 </html>  

Warum nur 300px für das Iframe? Weil ich den Debugger noch unten eingeblendet habe. Also nur ein Platz Problem. Könnt Ihr euch ja beliebig anpassen.

Viel Spass damit
Saso Nikolov

Freitag, 9. November 2012

Websockets scheinen tot, bevor diese richtig die Welt erblickt haben

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
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