Die Boost C++ Bibliotheken

Netzwerkprogrammierung

Auch wenn Boost.Asio eine Bibliothek ist, mit der beliebige Daten asynchron verarbeitet werden können, wird sie in der Praxis häufig zur Netzwerkprogrammierung eingesetzt. Boost.Asio unterstützte Netzwerkfunktionen zuerst, bevor im Laufe der Zeit neue I/O Objekte hinzukamen. Netzwerkfunktionen sind insofern ein gutes Beispiel für die asynchrone Datenverarbeitung, da Netzwerkverbindungen externe Ressourcen sind und eine Datenübertragung über Netzwerke unter Umständen einige Zeit dauern kann. Ergebnisse wie Empfangsbestätigungen oder Fehlermeldungen liegen nicht so schnell vor wie der Code der Funktionen, die Sie zum Datenversand oder -empfang in Ihrem Programm aufrufen, ausgeführt werden kann.

Boost.Asio bietet zahlreiche I/O Objekte, um Netzwerkanwendungen zu entwickeln. Im Beispiel 32.5 lernen Sie die Klasse boost::asio::ip::tcp::socket kennen, über die Sie eine Verbindung zu einem anderen Computer aufbauen können. Das Beispiel sendet einen HTTP-Request an einen Webserver, um die Homepage herunterzuladen.

Beispiel 32.5. Ein Webclient mit boost::asio::ip::tcp::socket
#include <boost/asio/io_service.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <array>
#include <string>
#include <iostream>

using namespace boost::asio;
using namespace boost::asio::ip;

io_service ioservice;
tcp::resolver resolv{ioservice};
tcp::socket tcp_socket{ioservice};
std::array<char, 4096> bytes;

void read_handler(const boost::system::error_code &ec,
  std::size_t bytes_transferred)
{
  if (!ec)
  {
    std::cout.write(bytes.data(), bytes_transferred);
    tcp_socket.async_read_some(buffer(bytes), read_handler);
  }
}

void connect_handler(const boost::system::error_code &ec)
{
  if (!ec)
  {
    std::string r =
      "GET / HTTP/1.1\r\nHost: theboostcpplibraries.com\r\n\r\n";
    write(tcp_socket, buffer(r));
    tcp_socket.async_read_some(buffer(bytes), read_handler);
  }
}

void resolve_handler(const boost::system::error_code &ec,
  tcp::resolver::iterator it)
{
  if (!ec)
    tcp_socket.async_connect(*it, connect_handler);
}

int main()
{
  tcp::resolver::query q{"theboostcpplibraries.com", "80"};
  resolv.async_resolve(q, resolve_handler);
  ioservice.run();
}

Im Beispiel 32.5 werden drei Handler verwendet: Die Funktionen connect_handler() und read_handler() werden aufgerufen, wenn die Verbindung erstellt wird und Daten empfangen werden. resolve_handler() wird zur Namensauflösung verwendet.

Weil der Empfang von Daten eine erfolgreiche Verbindungsaufnahme voraussetzt und eine Verbindungsaufnahme eine erfolgreiche Namensauflösung, werden verschiedene asynchrone Operationen in Handlern gestartet. So wird in der Funktion resolve_handler() auf das I/O Objekt tcp_socket zugegriffen, um mit Hilfe der aufgelösten Adresse, die über den Iterator it zur Verfügung steht, eine Verbindung aufzubauen. In der Funktion connect_handler() wird auf tcp_socket zugegriffen, um einen HTTP-Request zu senden und den Datenempfang zu starten. Weil es sich bei allen Funktionen um asynchrone Operationen handelt, werden jeweils die Namen der Handler als Parameter weitergegeben. Je nach Funktion sind zusätzliche Parameter notwendig wie beispielsweise der Iterator it, der auf die aufgelöste Adresse zeigt, oder das Array bytes, in dem empfangene Daten gespeichert werden.

Wenn Sie das Beispiel ausführen, wird in der Funktion main() ein Objekt q vom Typ boost::asio::ip::tcp::resolver::query erstellt. Es handelt sich hierbei um den Typ für Anfragen an den Namensauflöser. Der Namensauflöser ist ein I/O Objekt vom Typ boost::asio::ip::tcp::resolver. Indem q an async_resolve() übergeben wird, wird eine asynchrone Operation zur Namensauflösung gestartet. Im Beispiel soll der Name theboostcpplibraries.com aufgelöst werden. Nachdem die asynchrone Operation gestartet wurde, wird run() für das I/O Serviceobjekt aufgerufen, um die Kontrolle über die asynchronen Operationen ans Betriebssystem zu übergeben.

Wurde der Name aufgelöst, wird der Handler resolve_handler() aufgerufen. In diesem wird überprüft, ob die Namensauflösung erfolgreich war. Ist sie das, ist das Objekt ec, das Fehlerarten repräsentiert, auf 0 gesetzt. Nur in diesem Fall wird auf den Socket zugegriffen und der Verbindungsaufbau initiiert. Die Adresse des Servers, zu dem die Verbindung aufgebaut werden soll, steht über den zweiten Funktionsparameter vom Typ boost::asio::ip::tcp::resolver::iterator zur Verfügung. Dies ist das Ergebnis der asynchronen Namensauflösung.

Dem Aufruf von async_connect() folgt ein Aufruf des Handlers connect_handler(). Dort wird ebenfalls ein Objekt ec ausgewertet, um zu überprüfen, ob der Verbindungsaufbau erfolgreich war. Ist dies der Fall, wird die Methode async_read_some() für den Socket aufgerufen. Mit diesem Methodenaufruf beginnt der Lesevorgang über die nun bestehende Verbindung. Empfangene Daten werden im Array bytes gespeichert, das als erster Parameter an async_read_some() übergeben wird.

Die Funktion read_handler() wird aufgerufen, wenn ein oder mehr Bytes empfangen und in bytes gespeichert wurden. Der Parameter bytes_transferred vom Typ std::size_t gibt an, wie viele Bytes empfangen wurden. Wie üblich sollte auch in diesem Handler zuerst der Parameter ec ausgewertet werden, um zu überprüfen, ob möglicherweise ein Empfangsfehler vorliegt. Nur wenn dies nicht der Fall ist, werden die empfangenen Daten auf die Standardausgabe ausgegeben.

Beachten Sie, dass innerhalb des Handlers read_handler() nach der Datenausgabe über std::cout async_read_some() erneut für den Socket aufgerufen wird. Das ist notwendig, da nicht davon ausgegangen werden kann, dass die gesamte Homepage über eine einzige asynchrone Operation empfangen werden kann und vollständig in bytes vorliegt. Der wiederholte Aufruf von async_read_some() gefolgt von einem wiederholten Aufruf des Handlers read_handler() endet erst dann, wenn die Verbindung unterbrochen wird. Dies geschieht, wenn der Webserver die Homepage komplett versendet hat. In diesem Fall wird im Handler read_handler() ein Fehler gemeldet, so dass keine Datenausgabe über std::cout erfolgt und async_read() für den Socket nicht mehr aufgerufen wird. Da es keine ausstehenden asynchronen Operationen mehr gibt, endet das Programm.

Beispiel 32.6. Ein Zeitserver mit boost::asio::ip::tcp::acceptor
#include <boost/asio/io_service.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <string>
#include <ctime>

using namespace boost::asio;
using namespace boost::asio::ip;

io_service ioservice;
tcp::endpoint tcp_endpoint{tcp::v4(), 2014};
tcp::acceptor tcp_acceptor{ioservice, tcp_endpoint};
tcp::socket tcp_socket{ioservice};
std::string data;

void write_handler(const boost::system::error_code &ec,
  std::size_t bytes_transferred)
{
  if (!ec)
    tcp_socket.shutdown(tcp::socket::shutdown_send);
}

void accept_handler(const boost::system::error_code &ec)
{
  if (!ec)
  {
    std::time_t now = std::time(nullptr);
    data = std::ctime(&now);
    async_write(tcp_socket, buffer(data), write_handler);
  }
}

int main()
{
  tcp_acceptor.listen();
  tcp_acceptor.async_accept(tcp_socket, accept_handler);
  ioservice.run();
}

Beispiel 32.6 ist ein Zeitserver. Sie können mit einem Telnet-Client eine Verbindung aufbauen und die aktuelle Zeit erhalten. Anschließend wird der Zeitserver beendet.

Das I/O Objekt boost::asio::ip::tcp::acceptor wird verwendet, um auf einen Verbindungsaufbau ausgehend von einem anderen Programm zu warten. Dazu muss das entsprechende Objekt, im Beispiel 32.6 tcp_acceptor, derart initialisiert werden, dass es weiß, über welches Protokoll und über welchen Port ein möglicher Verbindungsaufbau erfolgt. Mit tcp_endpoint vom Typ boost::asio::ip::tcp::endpoint wird angegeben, dass der Acceptor auf Port 2014 auf eingehende Verbindungen vom Typ des Internet-Protokolls 4 warten soll.

Nachdem der Acceptor initialisiert wurde, wird in der Funktion main() listen() aufgerufen, um den Acceptor in den Empfangsmodus zu setzen. Anschließend wird mit async_accept() auf die erste Verbindungsaufnahme gewartet. Dazu muss ein Socket als erster Parameter an async_accept() übergeben werden, über den der Datenversand und -empfang erfolgen soll.

Nimmt ein anderes Programm Verbindung auf, wird die Funktion accept_handler() aufgerufen. War der Verbindungsaufbau erfolgreich, wird die aktuelle Zeit ermittelt und die freistehende Funktion boost::asio::async_write() aufgerufen. Diese Funktion wird verwendet, um sämtliche Daten in data über den Socket zu verschicken. Die Klasse boost::asio::ip::tcp::socket bietet auch eine Methode async_write_some() an. Bei dieser Methode wird ein Handler immer dann aufgerufen, wenn mindestens ein Byte gesendet wurde. Es müsste im Handler berechnet werden, wie viele Bytes noch gesendet werden müssen. Diese müssten über einen wiederholten Aufruf von async_write_some() verschickt werden. Berechnungen und wiederholte Aufrufe von async_write_some() lassen sich vermeiden, wenn boost::asio::async_write() verwendet wird, da diese asynchrone Operation erst als abgeschlossen gilt, wenn alle Daten in data gesendet wurden.

Nach dem Versand aller Daten wird die Funktion write_handler() aufgerufen. Diese Funktion greift auf den Socket zu und ruft shutdown() auf. Mit dem übergebenen Parameter boost::asio::ip::tcp::socket::shutdown_send wird angegeben, dass der Datenversand über den Socket abgeschlossen ist. Da es keine ausstehenden asynchronen Operationen gibt, endet Beispiel 32.6.

Beachten Sie, dass data keine lokale Variable ist. Obwohl data ausschließlich in accept_handler() verwendet wird, darf es sich bei diesem String nicht um eine lokale Variable handeln. Die Übergabe von data über boost::asio::buffer() an boost::asio::async_write() erfolgt per Referenz. Wenn boost::asio::async_write() und anschließend accept_handler() zurückkehrt, ist die asynchrone Operation initiiert, aber nicht beendet. data muss jedoch solange existieren, bis die asynchrone Operation beendet ist und die Variable nicht mehr benötigt wird. Indem data als globale Variable definiert ist, ist sichergestellt, dass sich der Gültigkeitsbereich der Variablen über die gesamte asynchrone Operation erstreckt.

Aufgabe

Entwickeln Sie einen Client und einen Server, mit denen eine Datei zwischen zwei Computern übertragen werden kann. Wird der Server gestartet, soll er die IP-Adressen aller lokalen Schnittstellen anzeigen und darauf warten, dass der Client eine Verbindung aufbaut. Wenn der Client gestartet wird, soll ihm eine der vom Server angebotenen IP-Adressen und der Name einer lokalen Datei als Kommandozeilenparameter übergeben werden. Der Client soll dann die Datei zum Server übertragen, wo sie gespeichert wird. Während der Datenübertragung soll der Client einen Verlauf anzeigen, so dass der Anwender weiß, dass die Datenübertragung momentan im Gange ist. Implementieren Sie den Client und Server mit Callbacks.