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

Seite 2: Ablauf

Inhaltsverzeichnis

Zunächst ermittelt der Client im Konstruktor von WoprPortableTerminal den Endpunkt, auf den er sich verbinden soll:

boost::asio::ip::tcp::endpoint endpoint = *endpoint_iterator;

Da er dafür den Iterator des Resolvers dereferenziert, kommt zunächst die erste verfügbare Adresse aus der Adressauflösung zum Einsatz. Der nächste wesentliche Schritt ist das Auslösen von Connect als asynchrone Operation. Als Handler dient die Methode handle_connect():

socket_.lowest_layer().async_connect(endpoint,
boost::bind(&WoprPortableTerminal::handle_connect, this,
boost::asio::placeholders::error, ++endpoint_iterator));

Danach terminiert der Konstruktor, wodurch die Hauptroutine [i]main() io_service.run() ausführen kann. Erreicht die Verarbeitung den Handler WoprPortableTerminal::handle_connect(), unterscheidet sie drei Fälle:

  1. Kein Fehler ist aufgetreten.
  2. Die Verbindung konnte nicht hergestellt werden.
  3. Ein anderer Fehler ist aufgetreten.

Im ersten Fall initiiert der Client eine asynchrone Operation für den SSL-Handshake:

socket_.async_handshake(boost::asio::ssl::stream_base::client,
boost::bind(&WoprPortableTerminal::handle_handshake, this,
boost::asio::placeholders::error));

Der einzige wesentliche Unterschied zur Handshake-Operation im Server ist das Setzen des Typs als client. Der Handler steckt in der Methode handle_handshake().

Tritt der zweite Fall ein, startet der Client einen erneuten Versuch für eine Verbindung auf den nächsten Endpunkt des Resolver-Iterators. Das geht solange bis entweder eine Verbindung auf einen Endpunkt erfolgreich ist, oder die Endpunkte aufgebraucht sind. Im letzteren Fall evaluiert der Ausdruck zu false:

endpoint_iterator != boost::asio::ip::tcp::resolver::iterator()

In diesem dritten Fall protokolliert der Client, dass die Verbindung nicht erfolgreich war und der Handler terminiert. Da sich in diesem Fall keine weitere asynchrone Operation in der Operation-Queue des io_service befindet, endet der Aufruf von run() in der Hauptroutine und daraufhin auch das Programm.

Waren sowohl der Verbindungsaufbau als auch der Handshake erfolgreich, tritt die Verarbeitung in handle_handshake() ein. Dieser Handler löst mit der bekannten Methode async_read_some() des Sockets eine asynchrone Leseoperation mit dem Handler handle_read() aus.

Hat das Programm Daten vom Server gelesen und handle_read() aufgerufen, gibt dieser zunächst die gelesenen Daten auf dem Standardausgabekanal (STDOUT) aus. Danach folgt das Lesen eines neuen Kommandos über

std::cin.getline(request_, max_length);

Den eingelesenen Befehl sendet der Client
daraufhin an den Server:

boost::asio::async_write(socket_,
boost::asio::buffer(request_, request_length),
boost::bind(&WoprPortableTerminal::handle_write, this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));

Der Aufruf von std::cin.getline() funktioniert unter jedem Betriebssystem, hat jedoch einen Nachteil: Er blockiert. Bei einem Client ist das auf den ersten Blick in Ordnung. Es entsteht eine vernünftige Abfolge zwischen Befehl lesen, an den Server senden, Antwort empfangen, Antwort ausgeben und von vorn beginnen.

Ein Nachteil ist, dass die Vorgehensweise immer nur eine Operation auf dem Server erzeugen und bis zu deren Abarbeitung keine weitere nachschieben kann. Damit ließe sich je nach Anwendungsszenario gut leben.

Ein anderes Problem stellt sich ein, wenn die Verbindung beim Lesen eines Befehls abbricht. Der Client – und damit auch der Benutzer – merkt das erst, nachdem ein Befehl mit der Return-Taste abgeschlossen wurde. Erst beim asynchronen Schreiben stellt der Client den Verbindungsabbruch fest. Das mag für den einen oder anderen Anwendungsfall irritierend wirken. Eine Lösung für beide Probleme skizziert der im späteren Artikelverlauf behandelte POSIX-Client.

Das asynchrone Schreiben erfolgt über den vom Server her bereits bekannten Aufruf von async_write(). Der zugehörige Handler erzeugt schlicht eine neue asynchrone Leseoperation – wiederum mit handle_read() als Handler.