fv_2007
Agile innovative developer with deep insight into all shapes of lightweight technologies such as ROA, REST and Ruby. Absolute “early adopter” in Web 2.0 technologies, platforms and Framework. Large professional network and fervent speeches at the conference level on architecture, strategy, design patterns, restful services, object-oriented thinking and modeling languages. Special interest in language constructs based on a deep knowledge of programming languages such as Smalltalk, Erlang, Java, Clojure, Scala, Ruby ... read more
View Frank Vilhelmsen's profile on LinkedIn Recommend Me

Server Push May 13, 2008 10:40 7 months ago

Internettet er designet efter klient server princippet. Det betyder at en klient skal laver et request for at skabe en forbindelse til serveren der laver arbejdet og svare tilbage til klienten med et response. Derefter afbrydes forbindelse til klienten. Denne type af kommunikation kaldes forbindelsesløs og betyder at klient og server kun kender hinanden i kontekst af et request/response. Klienten er den aktive part og serveren afholder sig reaktiv.

Derudover ved serveren i teorien intet om klienter men i praksis kan serveren genkende en klient ved hjælp af en ident som placeres hos klienten. Hver gang en klient laver et request må serveren validere klienten og betrakte et request/response som en del applikation. At det for klienten føles som en sammenhængende applikation kan vi takke identen for.

Man siger populært at internettet er broken.

Internettet er ikke designet med det formål at en server skal kunne opdatere en klient når den får nye informationer. Og det ville også skabe en masse nye problemer som skulle af vejen før serveren kunne pushe nye informationer ud til klienterne. Hvem er i det hele taget mine klienter? Protokollen man benytter til at kommunikere på internettet hedder Hypertext Transfer Protocol(HTTP). HTTP er kun kommunikations protokollen til internettet mens transporten over internettet klares af Transmission Control Protocol (TCP). Nok om det.

Teknikker

Når serveren har nye informationer må en klient selv tage initiativ til at få opdateret sine oplysninger. De tre mest almindelige metoder til at opdater information i en klient browser er polling, comet og piggybacking.

  • Polling er den mest benyttede løsning. Polling betyder at klienten selv løbene sender et request til serveren i fastbestemte intervaller. Analogien er barn på bagsæde som konstant spørger, hvornår er vi der?
  • Comet er basseret på lange http forbindelser, eller server push. Metoden går ud på at holde en forbindelse til klienten åben og forsætte med at hælde date i forbindelsen til klienten. Serveren bestemmer hvor hurtigt og hvor lang tid forbindelsen er i brug.
  • Piggyback betyder at man lader klienten opdatere ved at polle serveren med jævne mellemrum. Når serveren har yderlige information hæftes den på næstkommende poll respons.

Hver metode har sine fordele og ulemper. Polling er enkel at implementere men overbebyrder serveren med requests.
Comet er på samme måde som Ajax, et kæmpe hack men kan bedre udnytte serveren ved at holde lange konversationer med klienter og streame data ud. Piggyback forbruger en masse netværkstrafik og selv når serveren ikke har yderlige information. Dog en god løsning når serveren har konstante leverancer.

En gamle teknik(1995) fra netscape er at gemme en iframe i en klientapplikation som konstant loader fra serveren, på den måde kunne man over lang tid streame data. Men i 2002 fik vi med google’s hjælp, nye muligheder ved hjælp fra XMLHttpRequest (XHR). XHR giver os mulighed for at lave HTTP kommunikation fra Javascript i en klientbrowser. Med denne nye teknologi kan man overføre data mellem klient og server på en asynkron måde uden at reloade hele websiden. XMLHttpRequest den vigtigste del af applikationsudviklingen med Ajax. I 2006 specificerede WWW det første fælles oplæg med minimumskrav som alle browsere bør implementere. Det arbejde forgår stadig og er også grunden til at dette eksempel kun virker i udvalgte browsere.

Klienten

Min klient skal udnytte XMLHttpRequest asynkrone egenskaber. Min klient skal hente en webside som viser en tabel med kursnavne. Når klientapplikationen er loadet vil den foretage et Ajax kald til serveren for at hente kurser. Disse kurser skal opdateres når serveren har nye informationer.

Der skal være en langvarig forbindelse mellem klient og server hvorigennem kurser streames. Jeg vil benytte en form for simi-polling fra klienten. Der er flere grunde til dette, dels vil jeg gerne have at ubenyttede tråde dør ved TTL og dels må man tænke på at et langvarig Ajax efterhånden bliver fyldt med data. Især Internet Explorer hober store mængder respons data op internt mens det ser ud til at Mozilla bedre klare at flusher indkommende data. I dette eksempel bruger jeg Sarfari.

Serveren

En webserveren levere websiden med den indbyggede Ajax funktionalitet. XMLHttpRequest tilgår ikke webserven men derimod en mange trådet proces på en anden port idet jeg ikke vil have langvarige tråde spærre min webserver. Processen opsamler kurser og levere når den har ca. 6 styk formateret som JSON objekter. JSON er lækkert, meget lidt versbose, læsbart og kan evalueres direkte af Javascript. Processen kan håndtere n~200+ antal klienter samtidigt.

Først skal jeg have nogle kurser.. Fake selvføli

module OnlineStockTrading

  class Stock
    attr_accessor :name
    attr_accessor :value

    def initialize()
      letters = ('A'..'G').to_a
      @name = "A#{letters[rand(letters.size)]}" 
      @value = rand + rand(12)
    end

    # http://www.jsonlint.com/
    def to_json
      "{ \"name\": \"#{name}\", \"value\": \"#{value}\" }" 
    end

  end

  class StockList
    def initialize()
      @list = Array.new
      6.times do
        @list << Stock.new
      end
    end

    def to_json
      a = String.new
      a << "[" 
      for element in @list
        a << element.to_json
        a << "," unless @list.index(element) == @list.size-1
      end
      a << "]" 
    end
  end  
end

Nu skal der blot laves en proces som kan hælde kurserne ud over en port. Protokollen er HTTP er to årsager. Dels fordi Ajax komponenten taler HTTP men også fordi det er nemt at benytte en browser til simpelt check af mini-serveren. Det kan være lidt vanskeligt at ramme protokollen de første gange, især de vigtige linjeskift mellem header og body.

Body bliver sendt med to gange. En gang i header med key X-JSON og en gang i selve body’en. JavaScript eksemplet bruger body’en mens eksemplet med JavaScript frameworket Prototype har mulighed for at benytte header til at overføre JSON objekter direkte, endda leveret som evalueret JSON. Fedt.

server = TCPServer.new('localhost', 8080)
loop do
  Thread.start(server.accept) do |session|
    if (session.gets['.json'])
      until time_runs_out do
        s = OnlineStockTrading::StockList.new
        body = s.to_json
        session.print "HTTP/1.0 200 OK\r\n" 
        session.print "Content-type: application/x-json\r\n" 
        session.print "X-JSON: #{body}\r\n\r\n" 
        session.print body
        session.print "\r\n\r\n" 
        sleep rand
      end
    end 
    session.close
  end
end

Her benyttes plain JavaScript til at invokere XMLHttpRequest objektet i klient browseren. HTTP til server på port 8080. Serveren etablere en forbindelse og sender første stribe JSON objekter afsted til klienten. I dette tilfælde modtages data fra body i beskeden i responseText og skal derefter evalueres til JSON.

Når man bruger XMLHttpRequest findes der 4 tilstande som beskriver den aktuelle tilstand. Normalt er et kald færdig med tilstand 4 = loaded. Men jeg kigger slet ikke efter den tilstand idet jeg vil forsætte forbindelsen og kun reagere ved ændring af tilstand, onreadystatechange. Jeg kunne have checket for tilstand 3 = receiving data men den findes slet ikke i IE.

    <script type="text/javascript">
        function callbackFunction(json) {
            for (var i=0;i<json.length;i++) {
                document.getElementById(json[i].name).innerHTML = json[i].value; 
            }        
        }

        function start() {
            var xrequest = new XMLHttpRequest();
            xrequest.open("POST","http://localhost:8080/stocks.json", true);        
            xrequest.onreadystatechange = function() {
                if (xrequest.responseText) {
                    var message = find_latest_messages(xrequest.responseText);
                    var json = eval('(' + message + ')');
                    callbackFunction(json);
                }
            };
            xrequest.send(null);
        }

        window.onload = start;

        function find_latest_messages(message) {
            var unprocessed = message.substring(message_index);
            var message_index = unprocessed.lastIndexOf("[{");
            var message_end_index = unprocessed.lastIndexOf("}]");
            var latest_message = unprocessed.substring(message_index, message_end_index+2);
            return latest_message;
        }    

    </script>

Prototype

Prototype er et klient JavaScript framework hvis mål det er at gøre webudviklingen lettere. For mig handler det mest om tools til at gøre syntaksen nemmere at forstå og minimere antal kodelinjer. Ulempen er naturligvis at hele prototype skal loades(95k) når man kalder siden. Normal er framework lig med indkapsling og forhøjet abstraktion på en måde så man bliver trukket lidt væk fra det essentielle hvilket jeg normalt ikke bryder mig om. Dog virker prototype godt og afskære mig ikke mulighed for at traversere gennem indkommende data selvom jeg benytter prototypes Ajax.Request objekt.

     <script src="prototype.js" type="text/javascript"></script>
     <script src="effects.js" type="text/javascript"></script>
    <script type="text/javascript">
        var stocks = {
            'callbackFunction' : function(json) { 
                for(i = 0; i < json.length; i++) {
                    $(json[i].name).update(json[i].value) 
                    new Effect.Highlight(json[i].name);    
                }
            },
            'callback' : function(transport, json) { 
                $('notice').insert("ReadyState="+transport.readyState+"\n"+ json.toJSON().length +"\n"+json.toJSON());
                if (transport.responseText) {
                    var message = stocks.find_latest_messages(transport.responseText);
                    var json = eval('(' + message + ')');
                    stocks.callbackFunction(json);
                }
            },
            'get' : function() { 
                new Ajax.Request('http://localhost:8080/stocks.json', { method: 'POST',
                    requestHeaders: {Accept: 'text/x-json'},
                    onLoaded:  stocks.callback,
                    onInteractive: stocks.callback,
                    onSuccess: stocks.callback,
                    onFailure: stocks.error
                });
            },
            'error': function() {
                $('error').insert('Something went wrong...')
            },
            'find_latest_messages': function(message) {
                var unprocessed = message.substring(message_index);
                var message_index = unprocessed.lastIndexOf("[{");
                var message_end_index = unprocessed.lastIndexOf("}]");
                var latest_message = unprocessed.substring(message_index, message_end_index+2);
                return latest_message;
            }    
        }

        window.onload=stocks.get;
    </script>

Find den sidste besked.. Nå jeg skal spise..


By Frank Vilhelmsen - 4 tags: ajax architecture concurrency ruby - 1 comment on Server Push - Add comment

Torben Espersen July 17, 2008 10:16 5 months ago

Tak for hjælpen - det var lige den requestheader jeg manglede for at få JSON tilbage.