SSL/TLS-Netzwerkprogrammierung mit Boost.Asio, Teil 3: Client-Programmierung und Fehlerbehandlung

Seite 3: Fehlerbehandlung

Inhaltsverzeichnis

Die Fehlerbehandlung erfolgt über einen Platzhalter vom Typ boost::system::error_code, den das Programm als Parameter des Handler beim boost::bind() gebunden hat. Das error_code-Objekt hält im Wesentlichen drei wichtige Methoden bereit:

  1. message() liefert eine aussagekräftige Fehlermeldung als std::string.
  2. category() gibt die Fehlerkategorie als Objekt der Klasse boost::system::error_category zurück. Die Kategorie gehört zum betriebssystemspezifischen Teil von Boost und dient zum näheren Eingrenzen der Fehlersituation. So vergleicht der WOPR-Client beispielsweise die Kategorie des aufgetretenen Fehlers mit boost::asio::error::get_ssl_category(), um SSL-spezifische Fehler zweifelsfrei identifizieren zu können.
  3. value() ermittelt den Fehlercode. Der Wert 0 bedeutet, dass kein Fehler aufgetreten ist. Alles andere lässt sich anhand der Kategorie zuverlässig zuordnen. Um die Werte einer nicht dokumentierten Fehlersituation zu ermitteln, genügt es, für einen unbekannten Fehler diesen Code auszuführen:
std::cerr << error.category().name() << ':' << error.value() << std::endl;

Wenn Entwickler das Programm starten und die Fehlersituation künstlich provozieren, erhalten sie den Namen der Kategorie und den Fehlerwert auf STDERR und können daraus den Fehler ableiten. Notfalls ist ein grep über die Header oder gar den Quelltext von Boost mit dem Kategorienamen notwendig.

Mit category() und value() lassen sich Fehler gut abfangen. Eine betriebssystemabhängige Unterscheidung mag bei portablen Programmen zusätzlich sinnvoll sein; beispielsweise über #ifdef-Konstrukte.

Den POSIX-Client implementiert die Klasse WoprTerminal in der Datei terminal.cpp. Der erste markante Unterschied zwischen den beiden Clients ist das Fehlen des blockierenden Aufrufs std::cin.getline() in der POSIX-Variante. An dessen Stelle treten unabhängige asynchrone Leseoperationen, die von STDIN beziehungsweise dem Terminal lesen. Auf diese Weise entkoppelt der POSIX-Client das Lesen der Befehle über das Terminal vom Lesen der Daten über den Socket.

Dafür nutzt WoprTerminal einen Stream vom Typ boost::asio::posix::stream_descriptor als Objektattribut input_. Der Konstruktor instanziiert das Objekt wie folgt:

input_(io_service, ::dup(STDIN_FILENO))

Der Stream ist mit dem io_service-Objekt verbunden. Als File Handle erhält er ein Duplikat von STDIN. Damit existiert nun ein Boost.Asio-kompatibler Stream – ähnlich dem bereits bekannten Socket – zum Lesen aus einer Datei; im vorliegenden Fall von STDIN.

Die weiteren Schritte und die ersten initiierten asynchronen Operationen ähneln denen des portablen Clients. Erst wenn die Verarbeitung im Handler handle_handshake() angekommen ist, unterscheiden sich die beiden WOPR-Clients deutlich. Die POSIX-Variante erzeugt zwei asynchrone Operationen:

boost::asio::async_read_until(input_, input_buffer_, '\n',
boost::bind(&WoprTerminal::handle_read_input, this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
socket_.async_read_some(boost::asio::buffer(reply_, max_length),
boost::bind(&WoprTerminal::listen_on_server, this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));

Der Aufruf von async_read_until erzeugt eine asynchrone Leseoperation vom Stream input_, also von STDIN. Wie das "read until" im Namen erahnen lässt, liest die Methode bis zu einem bestimmten Zeichen. Dass es sich dabei um ein Newline '\n' handelt, legt der dritte Parameter fest. Das Einlesen der Daten erfolgt in einen Stream-Buffer vom Typ boost::asio::streambuf, übergeben im zweiten Parameter. Der Rest ist das bekannte Boost.Asio-Binding mit handle_read_input() als Handler.

Das zweite Statement ist bereits vom portablen Client bekannt. Es erzeugt mittels der Methode async_read_some() des Socket eine Leseoperation. Der Handler heißt im POSIX-Client jedoch abweichend listen_on_server. Der Name ist Programm: Er dient ausschließlich zum Einlesen der Daten vom Server. Anschließend gibt der Handler sie auf STDOUT aus. Danach erzeugt er eine neue asynchrone Leseoperation mit sich selbst als neuen Handler. Es handelt sich sozusagen um ein reines Polling in einer quasi endlosen Folge asynchroner, gleichförmiger Leseoperationen, die erst beim Verbindungsende aufhört.

Der Handler des Streams handle_read_input() bereitet zunächst das eingegebene Kommando auf:

input_buffer_.sgetn(request_, length - 1);
input_buffer_.consume(1);
request_[length - 1] = '\0';

Der Inhalt des Stream-Buffers input_buffer_ geht in ein char-Array request_ ein – length - 1 stellt sicher, dass nur das eingegebene Kommando, aber nicht das abschließende Newline im char-Array landet.

Da der input_buffer_ nach dieser Operation immer noch ein '\n' enthält, ist der Methodenruf consume(1) unbedingt notwendig. Ein erneuter Aufruf von async_read_until() mit dem gleichen Stream-Buffer würde unweigerlich sofort abbrechen, da der Stream-Buffer ein nicht konsumiertes Newline enthält. Daher ist es zum Wiederverwenden des Stream-Buffers unbedingt zu entfernen. Andernfalls würde der Client in einer ewigen Schleife mit leeren Kommandos festhängen.

Das dritte Statement stellt sicher, dass das char-Array mit einem Null-Byte abgeschlossen ist. Damit können die folgenden Schritte davon ausgehen, dass request_ in jedem Fall ein gültiger ASCII-String ist und kein Pufferüberlauf stattfindet.

Nach einigen administrativen Code-Zeilen erzeugt der Handler eine neue asynchrone Schreiboperation, um den Inhalt von request_ an den Server zur Verarbeitung zu senden. Der entsprechende Aufruf von async_write() ist bekannt. Als Handler ist die Methode handle_write() festgelegt.

Sie erzeugt lediglich wieder eine neue asynchrone Leseoperation über dem Stream. Einzig, wenn ein Disconnect stattgefunden hat, erfolgt dieser Schritt nicht mehr. io_service::run() kann auf diese Weise in einem Fade-Out "aushungern".