RkBlog

Hardware, programming and astronomy tutorials and reviews.

Comet i Orbited

Tworzenie aplikacje www czasu rzeczywistego z wykorzystaniem serwera Comet - Orbited.

Comet to architektura www, w której serwer wysyła do klienta (np przeglądarki) dane, a wszystko to asynchronicznie i bez bezpośredniego żądania klienta. Umożliwia to tworzenie aplikacji internetowych działających na zdarzeniach. Comet wykorzystywany jest w takich aplikacjach jak Gmail/GTalk i coraz częściej wita w aplikacjach mniejszego kalibru (django-dose, grono.net).

W Pythonie dostępne są dwa rozwiązania umożliwiające wykorzystanie tej architektury - Divmod Athena oraz Orbited, któremu poświęcony jest ten artykuł. Orbited to serwer Comet - działający na zdarzeniach, a nie jak zwykły serwer HTTP w oparciu o żądania. Posiada obecnie API dla Pythona, Ruby i PHP.

Instalacja

Zależności (Linux): Część zależności jak Twisted możemy zainstalować z pakietów dystrybucji. Orbited i jego zależności powinny zainstalować się bez problemu poprzez:
easy_install orbited

Konfiguracja serwera orbited

By móc skorzystać z serwera orbited musimy stworzyć plik konfiguracyjny i odpalić serwer wskazując mu ten plik. Z poziomu zwykłego użytkownika stwórz pusty katalog, a w nim plik konfiguracyjny, np. graph.cfg o kodzie:
[listen]
http://:9000
stomp://:61613
 
[access]
* -> localhost:61613
 
[static]
graph=index.html
 
[global]
session.ping_interval = 300
Konfiguracja podzielona jest na kilka sekcji.

Powyższa konfiguracja może posłużyć jako punkt wyjścia w różnych kometowych aplikacjach. W naszym przypadku stworzymy prostą stronę HTML - index.html, która zademonstruje działanie tego serwera i technologii.

Zapisz plik konfiguracji i stwórz plik index.html z np. dowolnym tekstem "powitalnym". By uruchomić serwer Orbited w konsoli wykonaj (w katalogu z konfiguracją):
orbited --config graph.cfg
Serwer powinien wystartować:
orbited --config graph.cfg
03/03/10 20:11:38:297 INFO   orbited.start      proxy protocol active
03/03/10 20:11:38:306 INFO   orbited.start      Listening http@9000
03/03/10 20:11:38:307 INFO   orbited.start      Listening stomp@61613
Pod adresem http://localhost:9000/graph powinniśmy zobaczyć zawartość pliku index.html.

Wykorzystujemy Cometa

Serwer Comet taki jak Orbited działa inaczej niż serwer HTTP. Wchodząc na stronę, która korzysta z tej technologii następuje nawiązanie połączenia z serwerem. Mimo iż strona została załadowana - połączenie jest utrzymywane w tle i serwer może wysyłać dane do przeglądarki w dowolnym momencie (np. pod wpływem jakiegoś zdarzenia). Przypomina to zasadę działania internetowego czata - łączymy się i czekamy na napływające wiadomości.

Teraz zaprezentujmy Cometa w akcji. W tym przykładzie stworzymy prosty wykres, który będzie zmieniał się dynamicznie pod wpływem danych wysyłanych przez serwer. Żeby to wszystko działało musimy dołączyć do strony HTML magiczne biblioteki JavaScript dostarczane automatycznie wraz z Orbited:
<script src="http://localhost:9000/static/Orbited.js"></script>
<script>
TCPSocket = Orbited.TCPSocket
</script>
<script src="http://localhost:9000/static/protocols/stomp/stomp.js"></script>
Teraz zróbmy wykresy za pomocą JavaScriptu. Poniższy kod nie jest powiązany z Orbited, po prostu wrzuca 10 "słupków" do kontenera (DIV):
var bars = [];
var num_bars = 10;
 
// Make ten bars that are blue
var init_graph = function(num_bars, bars, container_name) {
 
    var container = document.getElementById(container_name);
    for (var i=0; i< num_bars; i++) {
        var bar = document.createElement('div');
        bar.className = "bar";
        bar.style.width = "100px";
        bars.push(bar);
        container.appendChild(bar);
    }
    // some pizazz... one bar gets to be red.
    bars[3].style.backgroundColor = "red";
};
Teraz nieco trudniejsza część, czyli kod JS modyfikujący wykres po otrzymaniu danych:
// Change the length of the bars to match given numerical data
var modify_graph = function(bars, payload) {
    var vals = JSON.parse(payload);
    for (var i=0; i<bars.length; i++) {
        var bar = bars[i];
        bar.style.width = vals[i] + "px";
    }
}
// In production use your js toolkit's onload system, or event listeners
onload = function() {
    init_graph(num_bars, bars, "container");
 
    stomp = new STOMPClient();
    stomp.onopen = function() {
    };
    stomp.onclose = function(c) { alert('Lost Connection, Code: ' + c);};
    stomp.onerror = function(error) {
        alert("Error: " + error);
    };
    stomp.onerrorframe = function(frame) {
        alert("Error: " + frame.body);
    };
    stomp.onconnectedframe = function() {
        stomp.subscribe("/topic/graph");
    };
    stomp.onmessageframe = function(frame) {
        // Presumably we should only receive message frames with the 
        // destination "/topic/graph" because thats the only destination 
        // to which we've subscribed. To handle multiple destinations we 
        // would have to check frame.headers.destination.
        modify_graph(bars, frame.body);
    };
    stomp.connect('localhost', 61613);
}
W onload wykorzystujemy JavaScriptowe API STOMPa, by nawiązać połączenie Comet przy otwieraniu strony i obsługiwać nadchodzące wiadomości (zdarzenia) dzięki metodzie onmessageframe, która wywoła zwykłą funkcję modyfikującą wykres. Nasz index.html wraz z prostymi stylami i DIVem - kontenerem powinien wyglądać teraz tak:
<!DOCTYPE html>
<head>
<title>Orbited + Morbid + js.io Graph</title>
<script src="http://localhost:9000/static/Orbited.js"></script>
<script>
TCPSocket = Orbited.TCPSocket
</script>
<script src="http://localhost:9000/static/protocols/stomp/stomp.js"></script>
<script>
var bars = [];
var num_bars = 10;
 
// Make ten bars that are blue
var init_graph = function(num_bars, bars, container_name) {
 
    var container = document.getElementById(container_name);
    for (var i=0; i< num_bars; i++) {
        var bar = document.createElement('div');
        bar.className = "bar";
        bar.style.width = "100px";
        bars.push(bar);
        container.appendChild(bar);
    }
    // some pizazz... one bar gets to be red.
    bars[3].style.backgroundColor = "red";
};
// Change the length of the bars to match given numerical data
var modify_graph = function(bars, payload) {
    var vals = JSON.parse(payload);
    for (var i=0; i<bars.length; i++) {
        var bar = bars[i];
        bar.style.width = vals[i] + "px";
    }
}
// In production use your js toolkit's onload system, or event listeners
onload = function() {
    init_graph(num_bars, bars, "container");
 
    stomp = new STOMPClient();
    stomp.onopen = function() {
    };
    stomp.onclose = function(c) { alert('Lost Connection, Code: ' + c);};
    stomp.onerror = function(error) {
        alert("Error: " + error);
    };
    stomp.onerrorframe = function(frame) {
        alert("Error: " + frame.body);
    };
    stomp.onconnectedframe = function() {
        stomp.subscribe("/topic/graph");
    };
    stomp.onmessageframe = function(frame) {
        // Presumably we should only receive message frames with the 
        // destination "/topic/graph" because thats the only destination 
        // to which we've subscribed. To handle multiple destinations we 
        // would have to check frame.headers.destination.
        modify_graph(bars, frame.body);
    };
    stomp.connect('localhost', 61613);
}
</script>
<style>
    body, html, h1 {
        margin: 0px;
        padding: 0px;
        font-family: sans-serif;
        background-color: #f3ffe2;
    }
 
    .bar {
        background-color: #38f;
        height: 12px;
        width: 200px;
        margin: 3px 0px;
    }
 
    #container {
        margin: auto;
        border: 2px solid #000;
        width: 400px;
        background-color: #fff;
    }
</style>
</head>
<body>
<div id="container"></div>
</body>
</html>
Strona powinna wyglądać obecnie tak:
new_orbited1
Mamy załatwiony frontend, więc trzeba zając się backendem. Wykorzystamy prosty skrypt w Pythonie do generowania wiadomości na serwerze:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from stompservice import StompClientFactory
from twisted.internet import reactor
from twisted.internet.task import LoopingCall
from random import random
from orbited import json
 
DATA_VECTOR_LENGTH = 10
DELTA_WEIGHT = 0.1
MAX_VALUE = 400 # NB: this in pixels
CHANNEL_NAME = "/topic/graph"
INTERVAL = 1000 # in ms
 
class DataProducer(StompClientFactory):
 
    def recv_connected(self, msg):
        print 'Connected; producing data'
        self.data = [ 
            int(random()*MAX_VALUE) 
            for 
            x in xrange(DATA_VECTOR_LENGTH)
        ]
        self.timer = LoopingCall(self.send_data)
        self.timer.start(INTERVAL/1000.0)
 
    def send_data(self):
        # modify our data elements
        self.data = [ 
            min(max(datum+(random()-.5)*DELTA_WEIGHT*MAX_VALUE,0),MAX_VALUE)
            for 
            datum in self.data
        ]
        self.send(CHANNEL_NAME, json.encode(self.data))
 
reactor.connectTCP('localhost', 61613, DataProducer())
reactor.run()
Wykorzystujemy tutaj Twisted oraz API STOMPa. Odpal skrypt w przeglądarce i obserwuj zmiany wykresów w przeglądarce. Wykresy będą losow zmieniać swoje wartości:
new_orbited2
Powyższy przykład oparty na artykule z cometdaily.com.

Co dalej?

Warto zaznajomić się z:
RkBlog

4 August 2011;

Comment article