SSL/TLS-Netzwerkprogrammierung mit Boost.Asio, Teil 1: Grundlagen

Im Umfeld von C++ verspricht Boost.Asio, eine einfache, plattformübergreifende Möglichkeit zur Netzwerkprogrammierung zu sein. Dazu gehört auch das Absichern der Verbindungen mit SSL beziehungsweise TLS.

In Pocket speichern vorlesen Druckansicht 12 Kommentare lesen
Lesezeit: 20 Min.
Von
  • Oliver Müller
Inhaltsverzeichnis

Im Umfeld von C++ verspricht Boost.Asio, eine einfache, plattformübergreifende Möglichkeit zur Netzwerkprogrammierung zu sein. Dazu gehört auch das Absichern der Verbindungen mit SSL beziehungsweise TLS.

Seit der Artikelreihe auf heise Developer zur OpenSSL-Programmierung in C++ hat sich vieles getan. Die Sprache C++ und die verfügbaren Libraries wie Boost haben sich enorm weiterentwickelt.

Wie in der ursprünglichen Reihe zur OpenSSL-Programmierung kommt auch diesmal das WOPR-Beispiel zum Einsatz. Die Hommage an den Film "War Games" implementiert nun in Version 3.00 den mehr oder weniger logischen Dialog zwischen dem Terminal und dem Server aus dem Film. Damit lassen sich einfache Befehle an den Server übermitteln, der seinerseits darauf reagiert. Auf die Weise können Anwender die Kommunikation gestalten und per SSL beziehungsweise TLS absichern.

Mehr Infos

SSL/TLS-Netzwerkprogrammierung mit Boost.Asio

Das WOPR-Szenario ist gut geeignet, um die Fähigkeiten von Boost.Asio im Bereich der SSL/TLS-Netzwerkprogrammierung zu demonstrieren und auszuloten. Der erste Teil dieser Artikelreihe führt in die Grundlagen und Funktionsweise von Boost.Asio ein. Die folgenden Artikel gehen auf die Implementierung des Servers und des Clients sowie die Praxis der Fehlerbehandlung und Grenzen der Plattformunabhängigkeit ein.

Ursprünglich war Boost dazu gedacht, die neuen Vorschläge, die dem C++-Standardisierungskomitee vorlagen, auszuprobieren. Heute ist die Bibliothekssammlung jedoch weit mehr als eine Spielwiese der Standardisierung. Trotzdem fließt weiterhin vieles von ihr in neue C++-Sprachstandards ein.

2005 erschien Boost.Asio als Teil von Boost 1.35. Dabei handelt es sich um eine plattformübergreifende Bibliothek, die hauptsächlich Funktionen für die Netzwerkprogrammierung mitbringt. Zusätzlich bietet sie Möglichkeiten der Low-Level-I/O-Programmierung, beispielsweise für serielle Schnittstellen und Dateien. Ob Boost.Asio ein Socket, eine Hardware-Schnittstelle oder eine Datei anspricht, bleibt für den Programmierer unerheblich. Er kann die immer gleichen Routinen zum Lesen und Schreiben einsetzen. Die Bibliothek bietet jeweils die Möglichkeit zum synchronen oder asynchronen Zugriff.

Boost.Asio ist eine reine Header-Bibliothek. Sie benötigt also keine zusätzliche Library, gegen die der Objektcode des C++-Programms gelinkt werden müsste. Sobald Entwickler den Header boost/asio.hpp einbinden, steht Boost.Asio im Programm zur Verfügung. Ein weiteres #include <boost/ssl/asio.hpp> und die SSL-Funktionen stehen ebenfalls bereit.

Das ist zwar elegant, aber leider nur die halbe Wahrheit. Boost.Asio hängt von einigen Bibliotheken aus dem Boost-Fundus ab. So benötigt sie in jedem Fall Boost.System, welche die Unterstützung für das jeweilige Betriebssystem beisteuert. Je nachdem, welche Boost.Asio-Features das C++-Programm verwendet, sind noch Boost.Regex für reguläre Ausdrücke, Boost.DateTime für das Verarbeiten und Darstellen von Datums- und Zeitangaben sowie OpenSSL für die TLS/SSL-Unterstützung nötig. Ganz ohne zur Link-Zeit einzubindende Bibliotheken geht es dann eben doch nicht.

Eine Header-Only Library legt die berechtigte Vermutung nahe, dass über die Header viel Code in die eigenen C++-Quelltexte hineingeschoben wird. Das kann das Kompilieren deutlich in die Länge ziehen. Gerade während der Entwicklungsphase größerer Projekte kann sich das in unangenehm langen Turnaround-Zeiten äußern. Vorkompilierte Header, wie sie einige integrierte Entwicklungsumgebungen anbieten, schaffen hier Abhilfe. Allerdings hält Boost.Asio einen eigenen umgebungsunabhängigen Mechanismus bereit.

Das Rezept ist einfach: Der Entwickler fügt ins Projekt eine beliebige neue C++-Quelltextdatei -- beispielsweise PreCompBoostAsio.cpp -- mit folgender Code-Zeile ein:

#include <boost/asio/impl/src.hpp>

Die Verwendung von SSL erfordert zusätzlich folgende Zeile:

#include <boost/asio/ssl/impl/src.hpp>

Alle anderen C++-Quelltexte benötigen nun das gesetzte Makro BOOST_ASIO_SEPARATE_COMPILATION. Ein einfaches #define reicht aus. Alternativ erledigt das auch ein Compiler-Kommandozeilenschalter, in der Unix-Welt meist -D und /D für Microsofts CL.

Jetzt erfolgt das Kompilieren der Boost.Asio-Anteile ausschließlich und einmalig in PreCompBoostAsio.cpp. Das Kompilieren der anderen (eigenen) C++-Module erfolgt nun deutlich schneller. Entwickler dürfen schließlich nicht das Linken des Objektcodes von PreCompBoostAsio.cpp vergessen.

Die Ein-/Ausgabeverarbeitung kann synchron oder asynchron erfolgen. Synchrones Verarbeiten ist dasselbe wie blockierende Verarbeitung (blocking I/O). Auf ein Netzwerkprogramm übertragen heißt das, dass nach dem Schreiben von Daten ins Socket der weitere Programmfluss angehalten wird. Hierzu unterbricht das Programm sämtliche Verarbeitung und wartet, bis die erwarteten Daten der Antwort am Socket ankommen. Anschließend liest das Programm sie ein und verarbeitet sie. Danach schreibt es die Antwort ins Socket und setzt beim erneuten Warten wieder den Programmablauf aus.

Hingegen blockiert das asynchrone Verarbeiten nicht (non-blocking I/O). Statt auf Daten zum Verarbeiten zu warten, prüft das Programm regelmäßig, ob Daten zum Lesen vorhanden sind. Ist das nicht der Fall, nutzt es die Zeit bis zur nächsten Prüfung für andere Arbeiten. Ein typisches Beispiel für Nebenläufigkeit entsteht.

In der Regel übernimmt das Betriebssystem das Prüfen auf vorhandene Daten. Das geschieht auf POSIX- oder UNIX-konformen Systemen über den System-Call select(), auf POSIX.1-2001-konformen mit poll(), auf Linux über das ereignisorientierte epoll(), auf FreeBSD durch das ebenfalls ereignisorientierte kqueue-Verfahren oder auf Windows über die I/O Completion Ports. Um diese Vielfalt abzudecken, müssen portable Programmierer viel Eigenarbeit leisten. Der Wunsch nach einer plattformunabhängigen Library ist durchaus berechtigt.

Den select()-Ansatz nutzen die WOPR-Beispiele aus der vorangegangenen OpenSSL-Reihe. Die Programme waren damit zwar schlank, aber gleichzeitig auf POSIX- beziehungsweise UNIX-konforme Betriebs- und Subsysteme beschränkt. Mit Boost.Asio lässt sich das nun ändern.

Die komplette betriebssystemspezifische Kommunikation kapselt die Klasse boost::asio::io_service. Entwickler müssen sie nur instanziieren, um einheitlichen Zugriff auf die systemspezifischen I/O-Services zu erhalten. Für die meisten Programme genügt dabei ein einziges io_service-Objekt -- mehrere sind ebenfalls erlaubt.

Entwickler können nun in Boost.Asio I/O-Objekte wie Sockets und serielle Schnittstellen instanziieren und an ein io_service-Objekt binden. Sämtliche darüber ausgelöste I/O-Operationen führt das io_service-Objekt betriebssystemspezifisch aus.

Bei synchronen Operationen ist das Vorgehen recht simpel. Die Anwendung ruft eine Methode zum Lesen des Socket-Objekts auf, das wiederum die jeweilige Methode des io_service-Objekts aufruft. Dieser Callstack blockiert den Ablauf, bis Daten zum Lesen da sind. Dann liest io_service die Daten über das Socket ein und reicht sie seinerseits an die Anwendung weiter.

Asynchrone Operationen sind infolge der Nebenläufigkeit deutlich komplexer. Ein linearer Programmablauf existiert nicht mehr. Vielmehr sendet das Programm einen Operationswunsch und arbeitet danach an etwas anderem weiter. Ist die Operation abgeschlossen oder schlägt sie fehl, erhält das Programm eine Nachricht über eine Callback-Funktion. Konkret läuft das Socket-Beispiel folgendermaßen ab: Die Anwendung übergibt dem Socket-Objekt eine Leseoperation und eine Funktion oder Methode, die nach dem Abarbeiten der Operation aufgerufen werden soll, den sogenannten Handler. Da das Socket mit dem io_service-Objekt verbunden ist, landet die Operation über das Socket in dem io_service. Nach der Übergabe arbeitet das Programm weiter. Sind Daten zum Lesen da, ruft io_service den betreffenden Handler als Callback-Funktion auf. In ihr kann die Anwendung die gelesenen Daten verarbeiten oder entsprechend auf Fehler reagieren.

Damit io_service die übergebenen Operationen auch abarbeitet, ist die Methode run() des io_service aufzurufen. Diese Methode besteht aus einer Schleife, die auf die jeweiligen I/O-Ereignisse wie Lesen, Schreiben oder Connect für die übergebenen Operationen wartet. Tritt ein passendes Ereignis ein oder ein Fehler auf, entfernt run() die betreffende Operation aus der Liste der abzuarbeitenden Operationen und ruft den korrespondierenden Handler auf.

Die Methode run() verlässt diese Schleife und kehrt erst zurück, wenn alle Operationen abgearbeitet sind. Im Umkehrschluss heißt das, dass immer wenigstens eine Operation in der Queue sein muss, damit run() ständig weiterarbeitet.

Gerade bei einem Server hat die Applikation also dafür zu sorgen, dass run() mit genügend Operationen gefüttert wird. Normalerweise ist mindestens eine, die auf einen Connect wartet, in der Warteschlange. Bei Clients, die dauerhaft eine Verbindung offenhalten, steht in der Regel immer eine Operation zum Lesen von Daten in der Warteschlange der io_service-Methode run().

Der Aufruf eines Handlers tilgt zeitgleich die betreffende Operation aus der Warteschlange. Entwickler dürfen im Handler jedoch eine neue asynchrone Operation starten und somit sicherstellen, dass run() immer mit Arbeit versorgt ist. Initial können sie die Warteschlange mit Operationen vor dem Aufruf von io_service::run() befüllen. Damit findet run() bereits beim Start eine Reihe von Operationen in der Warteschlange vor. Das vermeidet, dass run() sofort nach dem Aufruf mangels Arbeit terminiert.

Boost.Asio nutzt für die Verarbeitung dieser nebenläufigen Operationen keine Threads. Stattdessen implementiert es das "Proactor Design Pattern", wie im Text "Proactor" und im Buch von Douglas C. Schmidt und anderen "Pattern Oriented Software Architecture, Volume 2" (Wiley, 2000) beschrieben. Abbildung 1 gibt die Implementierung in Boost.Asio als UML-Klassendiagramm wieder. Der Initiator ist die Anwendung selbst. Sie stellt über den Asynchronous Operation Processor asynchrone Operationen ein und übergibt den erzeugten Completion Handler. Sobald der Asynchronous Operation Processor die Operation ausgeführt hat, stellt er das Ergebnis als Event in die Warteschlange, die in dem Fall Completion Event Queue heißt. Das io_service-Objekt nimmt die Rolle des Proactors wahr. Es nutzt den sogenannten Asynchronous Event Demuxer, der die eingetroffenen Ereignisse aus der Completion Event Queue abruft. Anschließend führt der Proactor, also das io_service-Objekt, den Completion Handler aus.

Der Aufbau des Proactor-Pattern in Boost.Asio, das ohne Threads auskommt (Abb.1)

Dabei kommt das System ohne das Erzeugen von Threads oder gar schwergewichtigen Prozessen aus. Das hat nicht nur Vorteile: Benötigt ein Handler lange für die Verarbeitung seiner Ergebnisse, blockiert das ganze System, wie ein Beispiel im nächsten Teil der Artikelreihe praktisch aufzeigen wird. Daher ist es sinnvoll, zeitintensive Verarbeitung aus dem Completion Handler eigenverantwortlich in Threads auszulagern. Das stellt sicher, dass io_service immer ansprechbar bleibt und auf Ereignisse rechtzeitig reagieren kann.

Die asynchrone Verarbeitung hat noch eine andere wichtige Konsequenz: Die Fehlerbehandlung ist mittels Exception-Handling nicht möglich, denn es gibt keine Methode, die Exceptions abfängt.

Zur Veranschaulichung soll folgendes Beispiel dienen: Die Applikation übergibt eine asynchrone Operation. io_service wertet das Ereignis aus. Im Fehlerfall wäre eine Exception wirkungslos. Sie müsste entweder innerhalb von io_service abgefangen werden oder außerhalb unter Terminierung der Operationsverarbeitung. Im ersten Fall könnte die Applikation keinerlei Fehlerbehandlung beisteuern, geschweige denn auf Fehler reagieren. Im zweiten Fall würde die Methode run() verlassen und damit die asynchrone Verarbeitung abgebrochen.

Daher erfolgt die Fehlerbehandlung über einen dem Handler mitgegebenen Statusparameter. Mit seiner Hilfe kann die Applikation dann aufgrund des Parameters entsprechend reagieren.

Wie bereits erwähnt, genügt in der Regel ein io_service pro Applikation. Die Methode run() des Objekts ist Thread-sicher und kann somit auch gefahrlos in mehreren Threads aufgerufen werden. Die einzelnen Instanzen von run() teilen sich eine gemeinsame -- ebenfalls Thread-sichere -- Event-Queue, da sie dem gleichen io_service-Objekt angehören. Damit können Entwickler die Verarbeitung von Ereignissen auf mehrere Threads verteilen und somit parallelisieren.

Ein Parallelisieren des Zugriffs auf einen Port durch mehrere io_service-Instanzen funktioniert nicht: Entwickler dürfen ein Socket-Objekt, das an eine Adresse und einen Port gebunden ist, nur einem einzigen io_service zuordnen. Weitere Dienstinstanzen können dasselbe Socket und damit auch die betreffende Adressen-Port-Kombination nicht bedienen. Mehrere io_service-Objekte sind also lediglich zum Trennen verschiedener Datenquellen wie weiterer Sockets, Zugriff auf Schnittstellen oder Dateien sinnvoll. Beispielsweise können Entwickler damit ein Benutzer- von einem Administrator-Interface logisch trennen.

Auch bei Boost.Asio gelten die allgemeinen Tabus der Netzwerkprogrammierung. So erzeugt beispielsweise ein paralleler Zugriff auf ein Socket aus unterschiedlichen Threads kein vorhersagbares Ergebnis. Die I/O-Objekte sind ebenso wenig Thread-sicher wie die betriebssystemspezifischen Pendants, die sie kapseln.

Die gesamten Quelltexte des WOPR-Projekts befinden sich im Tarball wopr-3.0.tar.bz2 . Das Konfigurieren des Build-Prozesses übernimmt das im Open-Source-Bereich bekannte configure-Script. Für Visual Studio 2013 und 2015 finden sich Solutions im Unterverzeichnis vs2013 beziehungsweise vs2015.

Das Kompilieren erfordert einen C++11-konformen Compiler wie GNU gcc ab 4.7, LLVM clang ab 3.3 oder Xcode ab Version 5. Trotz der fehlenden POSIX-Kompatibilität ist das Kompilieren des WOPR-Servers und des synchronen Terminals auch mit Visual Studio möglich (getestet mit VS 2013).

Einige Compiler ohne C++-11-Unterstützung funktionieren mit dem configure-Prozess ebenfalls. Dazu gehören Sun Solaris Studio beziehungsweise Sun C++ und IBM XL C/C++. Letzterer funktionierte auf z/Linux im Test bestens, scheiterte hingegen auf AIX. Eine Liste von getesteten Systemen befindet sich in der Datei COMPATIBILITY.txt im Wurzelverzeichnis des WOPR-Tarballs.

OpenSSL ist für das weitere Vorgehen vorausgesetzt, da es den TLS/SSL-Teil abbildet. Auf aktuellen Unix-Systemen wie AIX, Solaris und den BSD-Varianten ist OpenSSL inklusive Header-Dateien und Entwicklungsbibliotheken meist bereits bei der Systeminstallation mit von der Partie. Auf Linux müssen die Entwicklungspakete installiert sein. Auf OS X kommen die OpenSSL-Header und Entwicklungsbibliotheken mit Xcode auf das System. Nähere Informationen zu den Unixoiden stehen in der Datei INSTALL im Tarball von WOPR 3.00.

Bei der Verwendung von Visual Studio 2013 oder 2015 bietet sich eine Binär-Distribution von OpenSSL an. Die Tests für diesen Artikel erfolgten mit der auf OpenSSL.org genannten Distribution von Shining Light Productions. Dort sind die 32-Bit- und 64-Bit-Versionen verfügbar. Sinnvoll ist die Installation der neuesten Version der Nicht-Light-Variante. Wer sowohl für 32 als auch für 64 Bit bauen möchte, installiert beide Versionen in separate Verzeichnisse.

Entwickler benötigen zum Bauen des Beispielsprojekts schließlich noch Boost. Viele Linux-Distributionen und auch Cygwin unter Windows liefern Lib- und Devel-Pakete mit zumindest recht aktuellen Versionen. Für Visual Studio bietet Boost selbst eine Binär-Distribution über Sourceforge zum Download an. OS-X-Nutzer finden Boost in den MacPorts. Auch die BSD-Varianten haben zumeist Binärpakete im Angebot. Ansonsten können BSD-Anwender pkgsrc bemühen, um Binärpakete zu bauen und zu installieren.

Die Mindestvoraussetzung für das WOPR-Beispiel ist Boost 1.40. Diese moderate Anforderung ergibt sich aus Boot.Program_Options, nicht aus Boost.Asio. Beim Schreiben des Artikels war bereits die Version 1.59 aktuell.

Wer keine fertige Boost-Distribution nutzen kann oder will, darf selbst kompilieren: Nach dem Entpacken des bei boost.org heruntergeladenen Archivs mit den Quelltexten startet der Nutzer dazu aus dem Boost-Verzeichnis auf Unix-like-Systemen mit

./bootstrap.sh

oder auf Windows in einem Visual-Studio-Commandline-Window mit

bootstrap.bat

das Konfigurieren des Boost-Builds. Compiler und Einstellungen erkennt der Bootstrap-Prozess in der Regel selbständig. Das Bauen der Bibliothekensammlung erfolgt unter Unix/Linux/Cygwin mit

./b2

und unter Windows mit Visual Studio mit

b2

Die Installation übernimmt unter Unix & Co. ein

./b2 install --prefix=Pfad

und unter Windows

b2 install --prefix=Pfad

"Pfad" steht dabei für den Pfad im System ein, in dem Boost installiert wird – beispielsweise /usr/local, /opt/local, /usr/local/boost-1.58 oder C:\local\boost\boost-1.58.

Diesen Pfad erhält anschließend der configure-Befehl von WOPR mittels --with-boost=Pfad. Beim späteren Ausführen der WOPR-Programme müssen die Nutzer gegebenenfalls das Verzeichnis Pfad/lib auf Unix/Linux in der Umgebungsvariablen LD_LIBRARY_PATH beziehungsweise LIBPATH (AIX) angeben. Auf Windows sollten die Boost-DLL-Verzeichnisse in der PATH-Variable eingetragen sein.

In der POSIX-Welt können Entwickler den Build-Prozess von WOPR für configure konfigurieren. Das funktioniert auf Unix/Linux-Systemen, OS X und auf Windows mit Cygwin. Das Entpacken des WOPR-Tarball wopr-3.0.tar.bz2 erfolgt mit

tar xjf wopr-3.00.tar.bz2

Nach dem Wechsel in das neu entstandene Verzeichnis mit

cd wopr-3.00

genügt ein

./configure

und ein anschließendes

make

für den Build von WOPR.

Zumeist lässt sich auch ein älterer gcc, der die Option --std=c++11 nicht unterstützt, über die Option --enable-legacy-gcc zur Zusammenarbeit bewegen: ./configure --enable-legacy-gcc

Zusätzlich lässt das configure-Script auch die Optionen --with-openssl und --with-boost zu, um damit ein Verzeichnis anzugeben, in dem OpenSSL beziehungsweise Boost bereitgestellt sind -- beispielsweise mittels ./configure --with-boost=/opt/boost-1.58

Eine Installation der Beispielprogramme per make install ist in der Regel nicht notwendig. Der WOPR-Server wopr findet sich nach dem Build im gleichnamigen Verzeichnis. Das asynchrone Terminal-Programm findet sich als terminal und das synchrone als sterminal im Unterverzeichnis terminal. Wer es dennoch installieren will, sollte bei configure über die Option --prefix ein Zielverzeichnis angeben. Andernfalls erfolgt die Installation nach /usr/local.

Einen Beispielschlüssel und ein -zertifikat im Verzeichnis wopr erzeugt ein Aufruf von make server.crt. Daraufhin liegen dort für den Start von WOPR, aber auch für das Terminal (Option -c) server.key und server.crt bereit.

Windows-Entwickler können neben dem zuvor beschriebenen configure-Prozess unter Cygwin WOPR auch mit Visual Studio kompilieren. Dazu entpacken sie den WOPR-Tarball wopr-3.0.tar.bz2 mit WinZIP oder 7-Zip. In dem neu entstandenen Verzeichnis wopr-3.00 befindet sich im Unterverzeichnis vs2013 eine Solution für Visual Studio 2013 und in vs2015 eine für Visual Studio 2015.

Den Build-Prozess von WOPR steuert ein Eigenschaftsblatt, das in der Datei vs2013\ConfigSheet.props beziehungsweise vs2015\ConfigSheet.props hinterlegt ist. Entwickler können es sowohl mit einem Texteditor als auch mit dem Property Manager von Visual Studio bearbeiten.

Diese Nutzermakros steuern den Build-Prozess in Visual Studio 2013 und 2015 (Abb. 2)

Alle Konfigurationen für alle Plattformen binden dasselbe Eigenschaftenblatt ein. Sofern die oben genannten Distributionen von OpenSSL und Boost installiert sind, erfordern lediglich vier Makros eine Anpassung:

  • BoostVersion erfordert erwartungsgemäß die Version von Boost – mit Unterstrichen statt Punkten, also beispielsweise 1_58 für Boost 1.58.
  • BoostRootPath erwartet das Wurzelverzeichnis der Boost-Installation. Der Bezug auf die Version erfolgt durch $(BoostVersion).
  • OpenSSLPath gibt das Wurzelverzeichnis für 32-Bit-OpenSSL an.
  • OpenSSLPath64 gibt das Wurzelverzeichnis der 64-Bit-Variante von OpenSSL an.

Nach dem Anpassen ist der Bau von WOPR für Win32 und/oder x64 möglich. Die 64-Bit-Programme befinden sich in wopr-3.00\vs2013\x64\Release beziehungsweise wopr-3.00\vs2013\x64\Debug, die 32-Bit-Programme in wopr-3.00\vs2013\Release beziehungsweise wopr-3.00\vs2013\Debug. Für Visual Studio 2015 befinden sich die Pfade entsprechend in wopr-3.00\vs2015. Der Server hört auf den Namen wopr.exe, das synchrone Terminal auf sterminal.exe. Ein asynchrones Terminal ist infolge mangelnder POSIX-Unterstützung nicht verfügbar.

Ein Beispielschlüssel samt X.509-Zertifikat erstellen Entwickler mit dem PowerShell-Script Create-Server-Certificate.ps1. Dazu öffnen sie eine PowerShell und wechseln mit cd in das Verzeichnis wopr-3.00\Windows. Dort können sie eine Online-Hilfe für das Script mit

help .\Create-Server-Certificate.ps1

aufrufen. Das Script erwartet openssl.exe im Pfad der PATH-Variablen. Alternativ muss der vollständige Pfad des Programms in der Option -openssl angegeben sein.

Der nächste Teil des Artikels widmet sich dem WOPR-Server und dessen Programmierung. Dazu gehört eine Einführung in die Nebenläufigkeit und ein Blick auf die Plattformunabhängigkeit von Boost.

Oliver Müller
ist freiberuflicher IT-Berater und -Trainer mit den Schwerpunkten Software Engineering und Kryptographie. Er berät zu Java EE, Unix/Linux, Android und Mainframe-Systemen.
(rme)