Turm von Babylon (Linux-Magazin, Juni 2017)

Schnell ein Skript geschrieben und schon steht der Programmierer vor dem typischen Problem: Wie ein HTTP-Dokument vom Web holen, welche Sprache macht's am einfachsten?

Kaum eine Programmieraufgabe zeigt die Unterschiede zwischen den gängigen Sprachen so deutlich wie das simple Einholen eines Dokuments vom Web. Sysadmins greifen bei Shell-Skripts oft auf die Utility curl zurück, die die Daten hinter einem URL ohne viel Federlesens überträgt und auf der Standardausgabe herausgibt.

Doch der Teufel steckt wie so oft im Detail, was ist, wenn der URL ins Nirwana zeigt? Der Server den Zugriff verweigert? Was passiert, falls der Server einen Redirect zurückgibt? So liefert curl http://google.com nicht die erwartete HTML-Seite mit dem berühmten Suchformular, sondern nur einen kurzen Hinweis, dass die wohl gewünschte Seite statt dessen auf www.google.com zu finden ist. Mit der Option -L folgt curl hingegen dem Verweis und pumpt dann die Daten aus der dort gefundenen Quelle. Oder was passiert bei einer Riesendatei wie einem 4K-Film, geht dem Prozess der RAM-Speicher aus, weil er sich alles auf einen Rutsch runterschlingt? Klappt bei einem https-URL die Verschlüsselung mit dem SSL-Protokoll automatisch, und prüft die Utility das Zertifikat des Servers ordnungsgemäß, damit sie nicht auf einen Man-in-the-Middle-Angriff hereinfällt? Wie das gute alte curl bieten populäre Programmiersprachen all dies, wenngleich auch oft nur als Zusatzpaket und oft mit eigenwilligem Ansatz.

Abbildung 1: Das simple curl-Kommando leistet Erstaunliches hinter den Kulissen.

Go, Hipster go!

Dem relativ neuen Go liegt der Web-Support schon von Haus aus mit dem Paket "net/http" bei, mustergültig inklusive SSL-Support. Go-Programmierer behandeln eventuell auftretende Fehler sofort nach Funktionsaufrufen, und können sich nicht darauf hinausreden, dass geworfene Exceptions schon irgendwo abgehandelt werden, und wenn es erst als schwer leserlicher Stack-Trace beim Abbruch des Programms ist. Zweifellos eine Philosophie-Frage mit ähnlicher Tragweite wie die Entscheidung für eine Religion, einen Ehepartner oder für einen der Editoren vi oder emacs: Es kann nur eine(n) geben.

Listing 1: http-get.go

    01 package main
    02 import "fmt"
    03 import "net/http"
    04 import "io/ioutil"
    05 
    06 func main() {
    07     resp, err := http.Get("http://google.com")
    08 
    09     if err != nil {
    10         fmt.Printf("%s\n", err)
    11         return
    12     }
    13 
    14     if resp.StatusCode != 200 {
    15         fmt.Printf("Status: %d\n", resp.StatusCode)
    16         return
    17     }
    18 
    19     defer resp.Body.Close()
    20     body, err := ioutil.ReadAll(resp.Body)
    21     if err != nil {
    22         fmt.Printf("I/O Error: %s\n", err)
    23         return
    24     }
    25 
    26     fmt.Printf("%s\n", body)
    27 }

Listing 1 zeigt gleich drei verschiedene Fehlerprüfungen, denn sowohl die Erzeugung des Requests kann schiefgehen (nicht unterstütztes Protokoll), der Server kann einen Fehlerstatus-Code liefern (404 bei nicht gefundenem Dokument) und schließlich kann beim Einholen der Daten über die verschlungenen Pfade des Internets ein Fehler auftreten, der zum Abbruch des Datenstromes führt. In Go geben Funktionen deswegen gern zwei Parameter zurück, ein Ergebnis und eine Fehler-Struktur, deren Wert auf nil gesetzt ist, falls alles glatt gegangen ist.

Interessant ist auch, dass das Paket "net/http" zuerst den Request ausführt, in Zeile 14 dann den Statuscode des Servers empfangen hat, sich aber mit dem Abpumpen der Body-Daten noch Zeit lässt. Diese schaufelt ReadAll() aus dem Paket io/ioutil erst später weg und die Close()-Methode auf das Body-Objekt zeigt abschließend an, dass die Request-Abarbeitung nun tatsächlich beendet ist und Go die Daten entsorgen kann. Das zugehörige Kommando steht schon in Zeile 19, wird aber mit dem super-eleganten Schlüsselwort defer bis zum Ende der gerade ausgeführten Funktion verzögert.

Schlankes Python

Ganz anders dagegen die Python3-Implementierung in Listing 2 mit der modernen Bibliothek requests: Sie kommt kompakter daher, weil Python Exceptions wirft, die der Entwickler später zentral prüfen kann. Ob der Request ein nicht existierendes Protokoll spezifiziert ("abc://"), die Serverzertifikatsprüfung bei SSL fehlschlägt, oder ein I/O-Fehler auftritt, alles löst eine entsprechende Exception aus, die der Code entweder gesondert oder wie in Zeile 11 gezeigt in einem Aufwasch prüft. Sicher bequemer, doch die eingesparte Tipparbeit kann sich später rächen, wenn eine Exception aus den Untiefen des Codes hochgespült wird, und keiner mehr weiß woher sie eigentlich kam. Auch die Lesbarkeit des Codes leidet, denn es ist nicht ganz klar, welche Zeile im try-Block die Exception eigentlich ausgelöst hat.

Listing 2: http-get.py

    01 #!/usr/bin/python
    02 import requests
    03 
    04 try:
    05     r = requests.get("http://google.de");
    06     if r.status_code == 200:
    07         print(r.text.encode('utf-8'))
    08     else:
    09         print(r.status_code)
    10 except Exception as exp:
    11     print("Error: " + str(exp))

Wie gesagt, das Thema hat schon zahllose schwer schlichtbare Endlosdiskussionen angezettelt. Lustig ist auch, dass r.encoding nach einem Request auf google.de angibt, dass die Seite in <ISO-8859-1> daherkommt. Erst ein manuelles Umschwenken auf utf-8 in Zeile 7 veranlasst das nachfolgende r.text, den Inhalt utf-8-kodiert auszugeben. Ein Status Code ungleich 200 wirft von Haus aus keine Exception und muss manuell geprüft werden. Wer's noch kompakter mag, kann aber mit der Methode raise_for_status() eine Exception werfen, falls der Server etwas anderes als 200 gemeldet hat.

Allesfänger Ruby

Auch Ruby kommt mit einem eingebauten HTTP-Modul namens net/http daher, allerdings muss es vor dem Absetzen des Requests den URL noch mit einer weiteren Klasse URL analysieren. Das ist für die Ruby-on-Rails-Hüpfer bereits zuviel Arbeit, es wurden deshalb schon einige Ruby-Gems geschaffen, die das Ganze in einem Aufwasch erledigen. Wie Python wirft Ruby Exceptions aus den Untiefen des Codes und deshalb kann der Entwickler auftretende Fehler zentral abfangen. Listing 3 zeigt ab Zeile 6 den Abfang-Block der mit begin los geht, ab rescue Fehler abfängt und mit end endet. Redirects folgt die Standard-Bibliothek in der Grundeinstellung nicht und die Seite von google.de interpretiert es als "ASCII-8BIT", was wohl der von Python erkannten ISO-8859-1-Kodierung entspricht.

Listing 3: http-get.rb

    01 #!/usr/bin/ruby
    02 require 'net/http'
    03 
    04 url = URI.parse('http://www.google.de')
    05 
    06 begin
    07   rsp = Net::HTTP.get(url)
    08   puts rsp.force_encoding('ISO-8859-1').encode('UTF-8')
    09 rescue Exception => e
    10   puts "Error: " + e.message
    11 end

Funktionales NodeJs

Wer in einem JavaScript-Snippet im Browser einen URL einholen oder ähnliches im NodeJS-Code auf der Server-Seite bewerkstelligen möchte, wie zum Beispiel auf einem Amazon-Lamda-Server ([2]), der muss sein Gehirn auf funktionalen Programmierstil umstellen. Event-basierte Systeme ticken ja nicht nach dem Motto "mach dies, warte bis es fertig ist, dann mach das", sondern wollen ihre Instruktionen in der Form "mach dies, dann das, dann das ... und los geht's" erhalten.

Grund dafür ist die Eventschleife, die immer nur kurze Callbacks ausführen kann, die Kontrolle zurück will und dann wieder vorbeischneit, falls langsam eintrudelnde Daten endlich von externen Schnittstellen eingetroffen sind. Diese Struktur erschwert die Lesbarkeit des Codes und erfordert viel Erfahrung beim Design von Software-Komponenten, damit diese gut wartbar zusammenspielen. Die gefürchtete "Pyramide der Verdammnis" ([3]) von verschachtelten Callbacks lässt sich mit einigen Konstrukten aufdröseln und Node 7.6 bringt neuerdings sogar Support für die Schlüsselworte async und await mit, die asynchronen Code zur optischen Aufhübschung in ein synchrones Korsett pressen ([4]).

Listing 4: http-get.js

    01 var http = require('http');
    02 
    03 http.get("http://google.com", function(res) {
    04     var content = '';
    05 
    06     res.on('data', function(data) {
    07       content += data;
    08     });
    09 
    10     res.on('error', function(err) {
    11         console.log( err );
    12     });
    13 
    14     res.on('end', function() {
    15       console.log( content );
    16     });
    17   });

Listing 4 zeigt einen get-Call des http-Moduls in NodeJS. Neben der URL auf das gewünschte Web-Dokument nimmt es eine Funktion entgegen. Diese wird später mit einem Response-Objekt aufgerufen und definiert eine Closure mit einer Variable (content) und drei Callbacks auf die Events "data", "error" und "end". Ersteren springt die Event-Schleife jedes Mal an, wenn ein Schub Daten vom Server zurückkommt. Er sammelt die Datenpakete der Reihe nach ein und hängt sie in der Variablen content aneinander. Im Falle eines Fehlers kommt der "error"-Callback an die Reihe und schreibt in Zeile 11 die Ursache ins Log. Signalisiert der Server das Ende der Übertragung, springt die Eventschleife den "end"-Callback an, der den Inhalt von content ausgibt, wo sich zu diesem Zeitpunkt die gesamten Body-Daten der HTTP-Antwort befinden. Redirects folgt die NodeJS-Bibiothek /http automatisch.

Gutes altes Perl

Listing 5: http-get.pl

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use LWP::UserAgent;
    04 
    05 my $ua = LWP::UserAgent->new;
    06 
    07 my $resp = $ua->get( "http://google.de" );
    08 if( $resp->is_error ) {
    09     die "Error: ", $ua->message;
    10 }
    11 binmode STDOUT, ":utf8";
    12 print $resp->decoded_content;

Das gute alte Perl holt Web-Dokumente traditionsgemäß mit dem CPAN-Modul LWP::UserAgent vom Netz. SSL-Support ist nicht automatisch dabei, kommt aber magisch hinzu, falls der Admin das CPAN-Modul LWP::Protocol::https nachinstalliert, das sich an eine verfügbare openssl-Installation und einer Liste von Root-Zertifikaten hängt. Listing 5 zeigt neben vorschriftsmäßiger Fehlerbehandlung auch noch eine Eigenheit: Wie manch andere hier vorgestellte Bibliothek folgt es redirects automatisch, erkennt ISO-8859-1 als Kodierung von google.de, aber liefert aus decoded_content() (im Gegensatz zu content()) einen utf8-String zurück. Das ist gut so, denn die Weiterverarbeitung der Daten im Programmcode setzt oft utf8 voraus und führt andersweitig zu unschönen Problemen.

Um aber einen utf8-String dann auch als solchen mit print unmodifiziert auszugeben, muss das Skript die Standardausgabe erst mit binmode auf den utf8-Modus einnorden. Dieses umständliche Gebahren ist der Kompatibilität geschuldet und stellt sicher, dass alte Skripts aus der Anfangszeit von Perls utf8-Support nicht mit neuen Perl-Versionen ausflippen. Jaja, das Alter, es ist kein Zuckerschlecken, wenn's in allen Gelenken knackt, und derweil die jungen Kerle wilde Kapriolen schlagen!

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2017/06/snapshot/

[2]

Michael Schilli, "Auf mehr als ein Wort": Linux-Magazin 04/17, S.96, <U>http://www.linux-magazin.de/Ausgaben/2017/04/Snapshot

[3]

Michael Schilli, "Pyramide der Verdammnis": Linux-Magazin 12/14, S.114, <U>http://www.linux-magazin.de/Ausgaben/2017/04/Snapshot

[4]

"Node 7.6 Brings Default Async/Await Support", Sergio De Simone https://www.infoq.com/news/2017/02/node-76-async-await

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.