POE, das ``Perl Object Environment'', bietet kooperatives Multi-Tasking ohne dass mehrere Prozesse oder Threads die Applikation verkomplizieren. Zusammen mit GTK, dem Grafik-Toolkit des Gimp lässt sich so ein simpler aber ruckfrei operierender Aktienticker mit graphischer Oberfläche implementieren.
Graphische Oberflächen arbeiten typischerweise Event-basiert: In einer Hauptschleife wartet das Programm auf Ereignisse wie Mausklicks oder Tastatureingaben des Benutzers. Es ist wichtig, dass das Programm diese Events verzögerungsfrei bearbeitet und sofort wieder in die Haupt-Eventschleife eintritt -- sonst ist die Oberfläche ``tot'' und der Benutzer kriegt den Rappel.
|
| Abbildung 1: Der Gtk-basierte Aktienticker in Aktion |
Die heute vorgestellte Aktienticker-Applikation (Abbildung 1) kontaktiert in regelmäsigen Abständen die Yahoo-Finance-Webseite, um die neuesten Börsenkurse einzuholen. So ein Webrequest kann je nach Internetwetterlage inklusive der DNS-Auflösung des Servernamens schon schon mal ein paar Sekunden dauern -- während dieser Zeit muss allerdings die Oberfläche der Applikation weiterackern.
Derartige Anforderungen kann man mit Multi-Processing oder Multi-Threading lösen. Damit erhöht sich allerdings die Komplexität eines Programms beträchtlich: Kritische Sektionen müssen vor parallelen Zugriffen geschützt werden, um die Integrität der Daten zu gewährleisten und es schleichen sich oft schwer zu analysierende und kaum zu reproduzierende Fehler ein. Wer sich schon einmal die Haare gerauft hat, weil er einen Core-Dump mit 200 laufenden Threads untersuchen musste, weiss, wovon ich rede.
Eine weitere Möglichkeit ist kooperatives Multitasking mit POE, dem Perl Object Enviroment. In dieser als ``State-Machine'' implementierten Umgebung läuft zu jedem Zeitpunkt genau ein Prozess mit nur einem Thread, aber ein auf Benutzerebene realisierter ``Kernel'' sorgt dafür, dass verschiedene Aufgaben scheinbar gleichzeitig abgearbeitet werden.
Um Aktienpreise einzuholen, nimmt man in Perl üblicherweise das CPAN-Modul Yahoo::FinanceQuote, aber das hat wegen unserer Ruckfrei-Anforderung genau ein Problem: Es arbeitet synchron.
use Finance::YahooQuote;
my @quote = getonequote($symbol);
Die Funktion getonequote setzt einen HTTP-Request an den Yahoo-Server
ab, wartet auf die Antwort und kehrt erst dann mit einem Ergebnis zurück.
Während der Wartezeit hätten wir allerdings lieber unsere Oberfläche
in Schuss gehalten -- wie's der Teufel will hat womöglich gerade jemand
ein anderes Fenster über den Ticker gezogen, sodass dieser einen Teil seines
Zuständigkeitsbereichs
neu zeichnen muss. Aber der gerade laufende Thread zieht es vor,
untätig herumzusitzen, was ein gräuliches Loch auf dem Desktop des
Benutzers erzeugt. So geht's nicht.
Es wäre geschickter, einen Web-Request auszuschicken, und uns, ohne auf das Ergebnis zu warten, einfach wieder um die graphische Oberfläche zu kümmern. Kommt die Antwort vom Yahoo-Server, wollen wir geweckt werden, schnell das Aktientickerfenster aktualisieren und ebenfalls sofort wieder zurück in die GUI-Hauptschleife springen.
Genau dies erledigt das POE-Framework. Es besteht aus einem ``Kernel'', in dem einzelne Applikationen ``Sessions'' registrieren. Dort springen State-Maschinen von Zustand zu Zustand und schicken sich gegenseitig Nachrichten. I/O-Aktivitäten erfolgen asynchron: Statt blockierend über ein File-Handle Daten einzulesen, sagt man: ``Hey, Kernel, ich will die Datei einlesen, wecke mich, wenn die Daten verfügbar sind.'' Zwar findet kein ``richtiges'' asynchrones Schreiben oder Lesen statt (POE nutzt unter der Haube lediglich nicht-blockierende syswrite/sysread-Funktionen), aber die bereitstehenden Häppchen werden mit Volldampf rausgepustet oder reingepfiffen.
Der ``kooperative'' Aspekt bei POE ist, dass die einzelnen Sessions sich darauf verlassen, dass niemand herumtrödelt, sondern sofort, wenn eine Aufgabe den Prozessor nicht voll auslastet, die Kontrolle freiwillig an den Kernel zurückgibt. Eine einzige unkooperative Stelle im Programm, und schon leidet das ganze System.
Dieses Multi-Tasking mit einem einzigen Thread erleichtert die Programmentwicklung erheblich -- kein Lock muss her, keine bösen Überraschungen mit Race-Conditions passieren, und wenn doch mal ein Fehler passiert, kann man ihn üblicherweise einfach aufspüren.
Und POE arbeitet auch schön mit den Haupt-Event-Schleifen einiger graphischer Umgebungen zusammen: Perl/Tk und Gtkperl erkennt POE automatisch und bindet sie und ihre Anforderungen nahtlos in den kooperativen Reigen ein.
Um den Aktienticker zu implementieren, wird ein Zustandsautomat
POE::Session nach Abbildung 2 benötigt. Nach dem Initialisierungszustand
_start, der unter anderem die Gtk-Oberfläche aufbaut und
den Alias-Namen ticker setzt, damit wir die Session später leicht
identifizieren können, geht die
Kontrolle an den Kernel. Alle 60 Sekunden (per Alarm)
oder sofort, wenn jemand den
``Update''-Knopf der Oberfläche drückt, kommt der ``wake_up'' Zustand an
die Reihe, der einen weiteren Zustandsautomaten vom Typ
POE::Component::Client::HTTP anstösst und dann sofort wieder die Kontrolle
an den Kernel zurückgibt. PoCoCli::HTTP ist eine sogenannte Komponente
(Component) aus dem POE-Framework, ein Zustandsautomat,
der seine eigene Session
(im Listing "useragent" genannt) definiert, im request-Zustand
Web-Anfragen entgegennimmt und dann im
POE-Framework mitspielt,
bis er eine HTTP-Antwort vollständig erhalten hat.
Dann teilt er dem Kernel
mit, dass die Session, die ihn aufgerufen hat (ticker), einen ihm
vorher mitgeteilten
Zustand (yhoo_response) anspringen soll. Veranlasst der Kernel
die ticker Session dazu,
nimmt sie die bereitliegende HTTP-Antwort entgegen,
frischt die Aktien-Widgets in der Anzeige auf und gibt die Kontrolle
sofort wieder an den Kernel zurück.
Ab Zeile 59 startet die POE::Component::Client::HTTP-Komponente mit
spawn() und legt fest, dass beim Server UserAgent-String
gtkticker/0.01 auftaucht und und hängende Anfragen nach 60 Sekunden
abgebrochen werden.
|
| Abbildung 2: Die POE::Session des gtktickers. |
In Listing gtkticker definiert Zeile 9 den URL von Yahoos Aktienkursservice. Dessen CGI-Schnittstelle nimmt einen Formatparameter (f=) mit
den geforderten Feldern (s: Symbol, l1: Aktienkurs c1:
Veränderung in Prozent seit dem letzten Börsentag) und einen
Symbolparameter entgegen, der die Börsenkürzel der geforderten
Aktiengesellschaften kommasepariert enthält (z.B. "YHOO,MSFT,TWX").
In der Antwort des Yahoo-Servers steht dann etwas wie
"YHOO",45.38,+0.35
"MSFT",27.56,+0.19
"TWX",18.21,+0.75
was gtkticker einfach zeilenweise und an den Kommas auseinandernimmt
und in die grafische Oberfläche einschleust.
Zeile 11 legt mit .gtkicker im Heimatverzeichnis des
Benutzers die Datei fest, in der die vom Ticker anzuzeigenden
Symbole stehen. Die Zeilen 25 bis 31 lesen sie ein und verwerfen
mit '#' beginnende Kommentarzeilen. Die implizite for-Schleife
... for /(\S+)/g;
führt den links von ihr stehenden Ausdruck für alle Wörter einer
Zeile aus und setzt jeweils das Börsensymbol in $_ -- so dürfen
auch mehrere Symbole durch Leerzeichen getrennt in einer Zeile stehen.
Listing 1 zeigt zeigt eine Beispieldatei. Trotz POE-Frameworks verwendet
gtkticker hier die regulären synchronen I/O-Funktionen, denn die
Konfigurationsdatei ist kurz und der Kernel läuft noch gar nicht.
Zeile 33 definiert den Zustandsautomaten des Tickers.
Der Parameter inline_states weist mit einer Hashreferenz
den Zuständen Funktionen zu, die der Kernel anspringt, falls sie
erreicht sind. Dann schiebt Zeile 44 mit
$poe_kernel->post("ticker", "wake_up");
über die von POE exportierte Variable $poe_kernel
den Zustand ``wake_up'' für die ``ticker''-Session in den Kernel und Zeile
45 startet mit
$poe_kernel->run();
die Kernel-Hauptschleife, die das Programm bis zum Shutdown nie mehr verlässt. Das war's!
Die vorher gezeigte Konstruktion des POE::Session-Objekts hatte
noch einen Seiteneffekt: Die dem _start-Zustand zugewiesene
und ab Zeile 48 definierte Routine start() wurde ausgeführt. Sie
setzt den Alias-Namen der Session auf ticker, und springt sodann
in my_gtk_init(), eine ab Zeile 82 definierte Funktion, die
die Gtk-Oberfläche zusammenbaut.
Gtk ist ein CPAN-Modul von Marc Lehmann (der freundlicherweise diesen
Artikel korrekturlas!), und ist eigentlich schon von Gtk2 abgelöst.
Allerdings spielen POE und Gtk2 noch nicht so recht zusammen, und
das ehrwürdige Gtk erledigte den Job hervorragend.
Ein Objekt der Klasse Gtk::Window ist das Hauptfenster der
Applikation, in dem oben ein typisches Pull-Down-Menü hängt. Dieses besteht
aus einem Menübalken mit einem Eintrag File, dessen ausziehbares
Menü nur den Eintrag Quit enthält, der die Applikation mit
Gtk->exit(0) beendet. Dass der Benutzer auch mit
der Tastenkombination CTRL-Q das Programm verlassen kann, dafür
sorgt ein Gtk::AccelGroup-Objekt, das die Menüsteuerung mit
sogenannten Accelerators bestückt.
Aufgebaut wird das Menü mittels einer Gtk::ItemFactory,
die zunächst einen Menübalken vom Typ Gtk::MenuBar erzeugt
und dort mittels create_items() die Einträge und hängt ihm untergeordnete
ausklappbare Menüs ein. Der path-Parameter gibt dabei einfach die Lage
des Menüpunktes an -- so spezifiziert /_File/_Quit den
den Eintrag Quit unter dem File-Eintrag im Menübalken.
Der callback-Parameter setzt eine Funktion, die Gtk
anspringt, falls der Benutzer den Eintrag mit der Maus anwählt oder
die über den accelerator-Parameter definierte Tastenkombination
anspringt.
Um Widgets geometrisch anzuordnen, kommen zwei verschiedene Verfahren
zum Einsatz: Gtk::VBox und Gtk::Table.
Das Container-Element Gtk::VBox ordnet in ihm enthaltene
Widgets vertikal an. Seine pack_start()-Methode platziert dabei
die Elemente vom oberen Rand nach unten, während pack_end() seine
Widgets von unten nach oben packt.
Der Aufruf
$vb->pack_start($mb, $expand, $fill, $padding);
packt den Menübalken $mb (dessen Widget gtkticker in Zeile 108 per
Namenseintrag mit $factory->get_widget('<main>') aus
der Factory holt) oben in die VBox. $expand gibt an, ob die Fläche,
in der das Widget ``schwimmt'', sich vergrößert, falls
das Hauptfenster mit der Maus vergrößert wird. Falls ja, gibt
$fill an, ob das Widget sich selbst ausdehnt -- so können kleine
Druckknöpfe riesengroß werden. Und $padding schließlich spezifiziert
die Anzahl der Pixel, die das Widget mindestens vertikal zu seinen Nachbarn
hält.
Statusmeldungen zeigt gtkticker in einem unauffälligen
Gtk::Label-Widget direkt
über dem Update-Knopf an. Die set_alignment()-Methode gibt mit
$STATUS->set_alignment(0.5, 0.5);
an, dass der Text horizontal und vertikal zentriert wird. Ein Wert von 0.0 wäre hingegen links, 1.0 rechtszentriert.
Das Container-Element Gtk::Table hingegen erlaubt es, andere Widgets
bequem in Tabellenform zu arrangieren. Die attach_defaults()-Methode
nimmt das anzuordnende Widget entgegen und jeweils zwei Spalten- und zwei
Reihenkoordinaten, zwischen denen das Widget liegen soll.
$table->attach_defaults($label,
0, 1, 1, 2);
Legt zum Beispiel fest, dass das mit $label referenzierte
Gtk::Label-Objekt in der ersten Reihe (``zwischen 0 und 1'') und
in der zweiten Spalte (``zwischen 1 und 2'') der Tabelle $table
aufgehängt ist.
Widgets vom Typ Gtk::Button kann man Aktionen zuordnen, die Gtk ausführt,
falls der Knopf vom Benutzer gedrückt wird. Die in Zeile 144
aufgerufene Methode signal_connect() legt fest, dass Gtk einen
"wake_up"-Event an den POE-Kernel schickt, falls der Benutzer auf den
Update-Knopf klickt. Auch das Hauptfenster verknüpft eine Aktion
mit dem Ergeignis, das der Benutzer auslöst, wenn er auf das ``X'' rechts
oben klickt, um das Fenster zu schließen:
$w->signal_connect('destroy',
sub {Gtk->exit(0)});
Dies räumt die Gtk-Session auf und veranlasst das Programm zum Abbruch.
Sind alle Widgets definiert, befördert die show_all()-Methode
des Hauptfensters in Zeile 148 sie alle auf den Bildschirm.
Im Zustand yhoo_response springt der POE-Kernel die ab Zeile
152 definierte Funktion resp_handler an. Per Definition legt
POE::Component::Client::HTTP dabei ein Request- und Response-Paket
in ARG0 und ARG1 ab. POE nutzt ja diese seltsam anmutende
Parameterübergabe, nachdem es neue numerische Konstanten wie KERNEL,
HEAP, ARG0, ARG1 einführt und dann erwartet, dass der Programmierer
sie nutzt, um den Funktions-Parameter-Array @_ zu indizieren:
$_[KERNEL] gibt so zum Beispiel immer das Kernel-Objekt zurück.
Die erwähnten Request- bzw. Response-Pakete sind wiederum Referenzen auf
HTTP::Request bzw. HTTP::Response-Objekte, an denen wir eigentlich
interessiert sind, also extrahiert der map-Befehl in den Zeilen
154 und 155 diese nach $req und $resp.
Im Fall eines HTTP-Fehlers setzt Zeile 159 eine entsprechende Meldung im Status-Widget und kehrt sofort zurück. Andernfalls wird der globale zweidimensionale Array der Label-Widgets aufgefrischt, die für jede zu überwachende Aktie das Börsensymbol, den aktuellen Kurs und die prozentuale Veränderung anzeigen. Ist die Veränderung 0, wird sie weggelassen.
wake_up-Events im POE-Kernel lösen die ab Zeile 185 definierte
Routine wakeup_handler() aus. Sie ruft die ab Zeile 67 definierte
Funktion upd_quotes() auf, die ein HTTP::Request-Objekt definiert
und es per Event an die Komponente POE::Component::Client::HTTP
schickt. Als Zielzustand für den ticker
gibt sie dabei yhoo_response an.
Nachdem dies erledigt ist, setzt wakeup_handler() mit der
delay()-Methode des Kernels einen Weckruf, der nach der in
$UPD_INTERVAL definierten Sekundenzahl (60) einen wake_up-Event
der ticker-Session auslöst. So frischt der Ticker alle 60 Sekunden
seine Aktiendaten auf, auch wenn der Benutzer nicht den Update-Knopf drückt.
Die erforderlichen POE-Module POE und POE::Component::Client::HTTP
installiert man am besten mit einer CPAN-Shell.
Falls das Modul POE::Component::Client::DNS ebenfalls installiert ist,
werden sogar DNS-Anfragen asynchron bearbeitet, sonst kann das eingesetzte
gethostbyname() eine kleine Verzögerung verursachen.
Das Modul Gtk vom CPAN zieht noch einige
weitere Abhängkeiten herein und bereitete
bei meiner Installation einige Probleme. Aber ein
touch ./Gtk/build/perl-gtk-ref.pod
perl Makefile.PL --without-guessing
im Distributionsverzeichnis
mit anschließendem make install löste das Problem.
Und wie immer lässt sich die Geschwätzigkeit der Logs auf dem Terminal mit Log::Log4perl (ebenfalls vom CPAN) und in Zeile 22 einstellen.
Es ist schon faszinierend, wie glatt sich die Oberfläche bedienen lässt. Selbst wenn man während eines automatischen Auffrischvorgangs über ein langsames Netzwerk im Menü herumfuhrwerkt, kommt die Applikation nicht ins Schleudern. Die ideale Technologie für alle Arten von grafischen Client-Applikationen!
1 # ~/.gtkticker
2 TWX
3 MSFT
4 YHOO AMZN RHAT
5 DODGX
6 JNJ COKE IBM SUN
001 #!/usr/bin/perl
002 ###########################################
003 # gtkticker
004 # Mike Schilli, 2004 (m@perlmeister.com)
005 ###########################################
006 use warnings;
007 use strict;
008
009 my $YHOO_URL = "http://quote.yahoo.com/d?".
010 "f=sl1c1&s=";
011 my $RCFILE = "$ENV{HOME}/.gtkticker";
012 my @LABELS = ();
013 my $UPD_INTERVAL = 1800;
014 my @SYMBOLS;
015
016 use Gtk;
017 use POE qw(Component::Client::HTTP);
018 use HTTP::Request;
019 use Log::Log4perl qw(:easy);
020 use Data::Dumper;
021
022 Log::Log4perl->easy_init($DEBUG);
023
024 # Read config file
025 open FILE, "<$RCFILE" or
026 die "Cannot open $RCFILE";
027 while(<FILE>) {
028 next if /^\s*#/;
029 push @SYMBOLS, $_ for /(\S+)/g;
030 }
031 close FILE;
032
033 POE::Session->create(
034 inline_states => {
035 _start => \&start,
036 _stop => sub { INFO "Shutdown" },
037 yhoo_response => \&resp_handler,
038 wake_up => \&wake_up_handler,
039 }
040 );
041
042 my $STATUS;
043
044 $poe_kernel->post("ticker", "wake_up");
045 $poe_kernel->run();
046
047 ###########################################
048 sub start {
049 ###########################################
050
051 DEBUG "Starting up";
052
053 $poe_kernel->alias_set( 'ticker' );
054
055 my_gtk_init();
056
057 $STATUS->set("Starting up");
058
059 POE::Component::Client::HTTP->spawn(
060 Agent => 'gtkticker/0.01',
061 Alias => 'useragent',
062 Timeout => 60,
063 );
064 }
065
066 ###########################################
067 sub upd_quotes {
068 ###########################################
069
070 my $request = HTTP::Request->new(
071 GET => $YHOO_URL .
072 join ",", @SYMBOLS);
073
074 $STATUS->set("Fetching quotes");
075
076 $poe_kernel->post('useragent',
077 'request', 'yhoo_response',
078 $request);
079 }
080
081 #########################################
082 sub my_gtk_init {
083 #########################################
084
085 my $w = Gtk::Window->new();
086 $w->set_default_size(150,200);
087
088 # Create Menu
089 my $accel = Gtk::AccelGroup->new();
090 $accel->attach($w);
091 my $factory = Gtk::ItemFactory->new(
092 'Gtk::MenuBar', "<main>", $accel);
093
094 $factory->create_items(
095 { path => '/_File',
096 type => '<Branch>',
097 },
098 { path => '/_File/_Quit',
099 accelerator => '<control>Q',
100 callback =>
101 [sub { Gtk->exit(0) }],
102 });
103
104 my $vb = Gtk::VBox->new(0, 0);
105 my $upd = Gtk::Button->new(
106 'Update');
107
108 $vb->pack_start($factory->get_widget(
109 '<main>'), 0, 0, 0);
110
111 # Button at bottom
112 $vb->pack_end($upd, 0, 0, 0);
113
114 # Status line on top of buttons
115 $STATUS = Gtk::Label->new();
116 $STATUS->set_alignment(0.5, 0.5);
117 $vb->pack_end($STATUS, 0, 0, 0);
118
119 my $table = Gtk::Table->new(
120 scalar @SYMBOLS, 3);
121 $vb->pack_start($table, 1, 1, 0);
122
123 for my $row (0..@SYMBOLS-1) {
124
125 for my $col (0..2) {
126
127 my $label = Gtk::Label->new();
128 $label->set_alignment(0.0, 0.5);
129 push @{$LABELS[$row]}, $label;
130
131 $table->attach_defaults($label,
132 $col, $col+1, $row, $row+1);
133 }
134
135 }
136
137 $w->add($vb);
138
139 # Destroying window
140 $w->signal_connect('destroy',
141 sub {Gtk->exit(0)});
142
143 # Pressing update button
144 $upd->signal_connect('clicked',
145 sub { DEBUG "Sending wake_up";
146 $poe_kernel->post(
147 'ticker', 'wake_up')} );
148 $w->show_all();
149 }
150
151 ###########################################
152 sub resp_handler {
153 ###########################################
154 my ($req, $resp) =
155 map { $_->[0] } @_[ARG0, ARG1];
156
157 if($resp->is_error()) {
158 ERROR $resp->message();
159 $STATUS->set($resp->message());
160 return 1;
161 }
162
163 DEBUG "Response: ", $resp->content();
164
165 my $count = 0;
166
167 for(split /\n/, $resp->content()) {
168 my($symbol, $price, $change) =
169 split /,/, $_;
170 chop $change;
171 $change = "" if $change =~ /^0/;
172 $symbol =~ s/"//g;
173 $LABELS[$count][0]->set($symbol);
174 $LABELS[$count][1]->set($price);
175 $LABELS[$count][2]->set($change);
176 $count++;
177 }
178
179 $STATUS->set("");
180
181 1;
182 }
183
184 ###########################################
185 sub wake_up_handler {
186 ###########################################
187 DEBUG("waking up");
188
189 # Initiate update
190 upd_quotes();
191
192 # Re-enable timer
193 $poe_kernel->delay('wake_up',
194 $UPD_INTERVAL);
195 }
![]() |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |