Wie ihr vielleicht gemerkt habt gab es letze Woche wieder keinen Artikel. Ich möchte mich dafür entschuldigen und werde bis auf weiteres nur alle zwei Wochen einen neuen Artikel schreiben, da sich das zeitlich bei mir sonst kaum ausgeht.
Ruby/Eventmachine implementiert ereignisgesteuerte Ein- und Ausgabe für Netzwerkprogrammierung in Ruby. Eventmachine zielt dabei vor allem auf hohe Skalierbarkeit, Stabilität und Performanz, aber auch darauf eine API bereitzustellen die möglichst einfach zu verwenden ist. Eventmachine selber steht in 3 Varianten zur Verfügung: Purem Ruby, das ohne weitere Abhängigkeiten läuft, Ruby mit einer C++-Extension und einer Version für JRuby, die in Java geschrieben ist.
Eventmachine ist wie gewohnt per Rubygems verfügbar:
gem install eventmachine
Wenn man die C++-Extension verwenden möchte, muss ein C/C++-Compiler installiert sein, und Debian-User müssen wie gewohnt das ruby-dev-Paket installiert haben. Falls das nicht der Fall ist, oder das Kompilieren der Erweiterung fehlschlägt, steht nur die reine Ruby-Version zur Verfügung.
Ein einfacher Echo-Server (ein Server der alles zurückschickt was er empfängt) ist in Eventmachine schnell implementiert:
1 2 3 4 5 6 7 8 9 10 11 12 |
require 'rubygems' require 'eventmachine' module EchoServer def receive_data( data ) send_data data end end EventMachine.run { EventMachine.start_server "0.0.0.0", 8081, EchoServer } |
Hier sieht man auch die Grundstruktur von Eventmachine: Man require'd zuerst Eventmachine, und ruft dann EventMachine.run auf, welche einen Block erwartet. In diesem Block schreibt man den Code für die ereignisgesteuerte Ein/Ausgabe. In unserem Beispiel starten wir nur einen Server.
EventMachine erzeugt für jede Verbindung zum Server ein Connection-Objekt. Dieses Objekt inkludiert automatisch das Modul dass wir start_server mitgegeben haben (man kann aber auch eine Klasse erstellen die von EventMachine::Connection ableitet). EventMachine selber ruft dabei nur 3 Methoden auf: post_init (Die Verbindung wurde aufgebaut und initialisiert), receive_data (Es wurden Daten empfangen) und unbind (Die Verbindung wurde geschlossen). Die restlichen Methoden stellt EventMachine zu unserer Verfügung bereit.
Das EventMachine-Modul selber stellt ebenfalls einiges an Methoden bereit. Davon sind einige besonders von Interesse: run und stop_event_loop, connect und close_connection, add_timer und add_periodic_timer, defer sowie start_server und stop_server.
run kennen wir schon vom Echo-Server. stop_event_loop macht genau das was der Name verspricht, und kümmert sich darum dass alle Destruktoren ausgeführt werden.
connect und close_connection öffnen und schliessen eine Connection. Dazu ein (leicht modifiziertes) Beispiel aus der Dokumentation, welches auch gleich die oben erwähnten post_init und unbind-Methoden demonstriert:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
require 'rubygems' require 'eventmachine' module DumbHttpClient def post_init send_data "GET / HTTP/1.1\r\nHost: _\r\n\r\n" @data = "" end def receive_data data @data << data if @data =~ /[\n][\r]*[\n]/m puts "RECEIVED HTTP HEADER:" $`.each {|line| puts ">>> #{line}" } puts "Now we'll terminate the loop, which will also close the connection" EventMachine.stop_event_loop end end def unbind puts "A connection has terminated" end end # DumbHttpClient EventMachine.run { EventMachine.connect "forum.ruby-portal.de", 80, DumbHttpClient } puts "The event loop has ended" |
Für add_timer, add_periodic_timer, start_server und stop_server verweise ich einfach auf die Dokumentation, nachdem auch diese Methoden genau das machen was ihr Name aussagt.
Eine Methode ist noch übrig: defer. Um den Sinn hinter 'Deferring' zu verstehen muss man wissen dass ereignisgesteuerte Programme einen Nachteil haben: Solange wir auf ein Ereignis reagieren, kann EventMachine keine neuen Ereignisse signalisieren (wie z.B. eine neue Verbindung). Deshalb sind z.B. die Webserver Thin und Evented Mongrel, die beide auf EventMachine basieren (Ebb ist ebenfalls event-driven, basiert aber auf libev), in Vergleichstest mit Mongrel schneller solange ein Request sehr kurz ist, haben aber massive Performanzprobleme mit langen Requests (z.B. Fileuploads). Mongrel verwendet ein Threaded Networking Model, erzeugt also einen neuen Thread für jeden Request. Bei kurzen Request ist das ein relativ grosser Overhead, aber bei längeren Requests hat Mongrel keinerlei Probleme auf neue Verbindungen zu reagieren.
EventMachine bietet für dieses Problem zwei Lösungen an: die defer-Methode und das EventMachine::Deferrable-Modul.
Die defer-Methode wird am besten an einem Beispiel (aus der Dokumentation) demonstriert:
1 2 3 4 5 6 7 8 9 |
operation = proc {
# perform a long-running operation here, such as a database query.
"result" # as usual, the last expression evaluated in the block will be the return value.
}
callback = proc {|result|
# do something with result here, such as send it back to a network client.
}
EventMachine.defer( operation, callback ) |
Der callback-Parameter ist dabei optional. operation wird dabei in einem Threadpool gespeichert und dann asynchron zum Event-Loop ausgeführt. Die Methode eignet sich also um z.B. blockierende Operationen zu implementieren.
Wenn man schon mit Python's Twisted vertraut ist, dann wird EventMachine::Deferrable einem sehr bekannt vorkommen. Auch hier spricht ein Beispiel mehr als 1000 Worte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
require 'rubygems' require 'eventmachine' class MyClass include EM::Deferrable def print_value x puts "MyClass instance received #{x}" end end EM.run { df = MyClass.new df.callback {|x| df.print_value(x) EM.stop } EM::Timer.new(2) { df.set_deferred_status :succeeded, 100 } } |
Dieses Programm läuft für 2 Sekunden, gibt dann "MyClass instance received 100" aus, und stoppt dann den Event-Loop.
Auf den ersten Blick verhält sich das Deferrable-Modul also so ähnlich wie die defer-Methode. Ganz so einfach ist es aber nicht.
Ein Objekt welches Deferrable inkludiert ist wie jedes andere Ruby-Objekt, und kann auch genauso verwendet werden. Zusätzlich dazu können aber gewisse Callbacks und Errbacks (Callbacks für den Fall eines Misserfolgs) für dieses Objekt festgelegt werden, die irgendwann in der Zukunft ausgeführt werden wenn sich der Status des Objekts ändert.
Wozu der Aufwand? Durch dieses Pattern kann das Durchführen einer Operation (z.B. ein Request an einen anderen Server) von allen Operationen abgekapselt werden, die auf den Erfolg (oder Misserfolg) dieser Operation abhängig sind. Das kann man sich zunutze machen wenn man einen Http-Client implementiert (Beispiel aus der Dokumentation):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
require 'rubygems' require 'eventmachine' # ... stuff ... EM.run { df = EM::Protocols::HttpClient.request( :host=>"www.example.com", :request=>"/index.html" ) df.callback {|response| puts "Succeeded: #{response[:content]}" EM.stop } df.errback {|response| puts "ERROR: #{response[:status]}" EM.stop } } |
HttpClient.request liefert sofort ein Objekt zurück das Deferrable inkludiert. Im Hintergrund wird der HTTP-Request durchgeführt, während unser Event-Loop weiterläuft. Sobald der Request durchgeführt wurde, setzt das HttpClient-Objekt mittels set_deferred_status entweder auf :succeeded oder :failed, wodurch sofort die entsprechenden Callbacks aufgerufen werden.
Es ist dabei möglich mehrere Callbacks zu definieren (Beispiel aus der Dokumentation):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
require 'rubygems' require 'eventmachine' EM.run { df = EM::Protocols::HttpClient.request( :host=>"www.example.com", :request=>"/index.html" ) df.callback {|response| df.set_deferred_status :succeeded, response[:content] } df.callback {|string| puts "Succeeded: #{string}" EM.stop } df.errback {|response| puts "ERROR: #{response[:status]}" EM.stop } } |
Durch erneutes aufrufen von set_deferred_status kann man die Argumente verändern, die das nächste Callback übergeben bekommt. Den Status selbst kann man nicht mehr ändern.
EventMachine bietet noch einiges mehr als hier vorgestellt wurde, z.B. eingebauten Support für HTTP (EventMachine::Protocols::HttpClient) und SMTP (EventMachine::Protocols::SmtpServer und EventMachine::Protocols::SmtpClient).
Nächstes mal wirds um Datamapper gehen, einem O/R-Mapper.
Kommentare
Interessanter Artikel. Auf den Datamapper bin ich schon gespannt. Ist mein meinen Augen eigentlich das Gleiche wie ActiveRecord. Mal sehen welche Unterschiede du aufzeigst.