Die Boost C++ Bibliotheken

Coroutinen

Seit der Version 1.54.0 unterstützt Boost.Asio Coroutinen. Während Sie die Bibliothek Boost.Coroutine direkt verwenden könnten, vereinfacht die explizite Unterstützung in Boost.Asio den Einsatz von Coroutinen.

Mit Coroutinen ist es möglich, auf Boost.Asio basierende Programme so zu strukturieren, dass Funktionsaufrufe die eigentliche Programmlogik widerspiegeln. Asynchrone Operationen verursachen keinen Bruch mehr, weil Handler definiert werden müssen, die Code beinhalten, der ausgeführt werden soll, wenn eine asynchrone Operation endet. Anstatt zahlreiche aufeinander zugreifende Handler zu definieren, erhält der Code seine ursprüngliche sequentielle Struktur wieder.

Beispiel 32.7. Coroutinen mit Boost.Asio
#include <boost/asio/io_service.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <list>
#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};
std::list<tcp::socket> tcp_sockets;

void do_write(tcp::socket &tcp_socket, yield_context yield)
{
  std::time_t now = std::time(nullptr);
  std::string data = std::ctime(&now);
  async_write(tcp_socket, buffer(data), yield);
  tcp_socket.shutdown(tcp::socket::shutdown_send);
}

void do_accept(yield_context yield)
{
  for (int i = 0; i < 2; ++i)
  {
    tcp_sockets.emplace_back(ioservice);
    tcp_acceptor.async_accept(tcp_sockets.back(), yield);
    spawn(ioservice, [](yield_context yield)
      { do_write(tcp_sockets.back(), yield); });
  }
}

int main()
{
  tcp_acceptor.listen();
  spawn(ioservice, do_accept);
  ioservice.run();
}

Die wichtigste Funktion für den Einsatz von Coroutinen mit Boost.Asio ist boost::asio::spawn(). Dieser Funktion muss als erster Parameter ein I/O Serviceobjekt übergeben werden. Der zweite Parameter ist eine Funktion, die als Coroutine dienen soll. Diese Funktion muss als einzigen Parameter ein Objekt vom Typ boost::asio::yield_context erwarten und darf keinen Rückgabewert haben. Im Beispiel 32.7 werden do_accept() und do_write() als Coroutinen verwendet. Ist die Signatur wie im Fall von do_write() verschieden, muss ein Adapter verwendet werden – zum Beispiel std::bind oder wie hier eine Lambda-Funktion.

Ein Objekt vom Typ boost::asio::yield_context kann anstatt eines Handlers an asynchrone Funktionen übergeben werden. So wird innerhalb von do_accept() der Parameter yield an async_accept() übergeben. In do_write() wird yield an async_write() übergeben. Diese Methoden rufen demnach keinen Handler auf, wenn die asynchronen Operationen beendet wurden. Stattdessen stellen sie den Kontext wieder her, in dem sie aufgerufen wurden. Dies bedeutet, dass das Programm nach Abschluß einer asynchronen Operation dort fortsetzt, wo die asynchrone Operation gestartet worden war.

do_accept() enthält eine for-Schleife. In dieser Schleife wird jeweils ein neuer Socket an async_accept() übergeben, um eine neue Verbindung zu akzeptieren. Hat ein Client Verbindung aufgenommen, wird über boost::asio::spawn() die Coroutine do_write() aufgerufen, um die aktuelle Uhrzeit an den Client zu senden.

Anhand des Schleifenkopfs ist erkennbar, dass das Programm zwei Verbindungen annehmen und die aktuelle Uhrzeit an zwei Clients senden kann, bevor es endet. Weil das Beispiel auf Coroutinen basiert, kann die wiederholte Ausführung einer Operation in einer herkömmlichen for-Schleife erfolgen. Dies kann das Verständnis des Codes erleichtern, da nicht mehr die potentielle Reihenfolge von Handler-Aufrufen durchdacht werden muss, um herauszufinden, wann die letzte asynchrone Operation ausgeführt wird. Soll der Zeitserver mehr als zwei Clients bedienen können, muss lediglich die for-Schleife angepasst werden.

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 Coroutinen.