zurück zum Artikel

Patterns in der Softwarearchitektur: Das Reactor-Muster

Rainer Grimm
Zwei Verkehrsampeln vor wolkigem Abendhimmel.

(Bild: monticello/Shutterstock.com)

Ereignisgesteuerte Anwendungen wie GUIs oder Server verwenden oft das Architekturmuster Reactor.

Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung und Softwarearchitektur. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Ereignisgesteuerte Anwendungen wie Server oder GUIs verwenden oft das Architekturmuster Reactor. Dieser kann mehrere Anfragen gleichzeitig annehmen und sie auf verschiedene Handler verteilen.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Das Reactor-Pattern ist ein ereignisgesteuertes Framework, um Serviceanfragen zu demultiplexen und gleichzeitig an verschiedene Serviceanbieter zu verteilen. Die Anfragen werden synchron verarbeitet.

Reactor

Auch bekannt als

Problem

Ein Server soll

Die Anwendung sollte vor Multithreading und Synchronisierungsproblemen geschützt werden.

Lösung

Struktur

Handles

Synchonous Event Demultiplexer

Event Handler

Concrete Event Handler

Reactor

Der Reactor

Der Reactor (und nicht die Anwendung) wartet auf die Indikationsereignisse, um das Ereignis zu demultiplexen und zu versenden. Die konkreten Ereignis-Handler werden im Reactor registriert. Der Reactor kehrt den Kontrollfluss um. Diese Umkehrung der Kontrolle wird oft als Hollywood-Prinzip [6] bezeichnet.

Das dynamische Verhalten eines Reactor ist ziemlich interessant.

Die folgenden Punkte veranschaulichen den Kontrollfluss zwischen dem Reactor und dem Ereignis-Handler:

Schauen wir uns den Reactor in Aktion an.

In diesem Beispiel wird das POCO-Framework [7] verwendet. "The POCO C++ Libraries are powerful cross-platform C++ libraries for building network- and internet-based applications that run on desktop, server, mobile, IoT, and embedded systems."

// reactor.cpp

#include <fstream>
#include <string>

#include "Poco/Net/SocketReactor.h"
#include "Poco/Net/SocketAcceptor.h"
#include "Poco/Net/SocketNotification.h"
#include "Poco/Net/StreamSocket.h"
#include "Poco/Net/ServerSocket.h"
#include "Poco/Observer.h"
#include "Poco/Thread.h"
#include "Poco/Util/ServerApplication.h"

using Poco::Observer;
using Poco::Thread;

using Poco::Net::ReadableNotification;
using Poco::Net::ServerSocket;
using Poco::Net::ShutdownNotification;
using Poco::Net::SocketAcceptor;
using Poco::Net::SocketReactor;
using Poco::Net::StreamSocket;

using Poco::Util::Application;

class EchoHandler {
 public:
  EchoHandler(const StreamSocket& s, 
              SocketReactor& r): socket(s), reactor(r) { // (11)
    reactor.addEventHandler(socket, 
      Observer<EchoHandler, ReadableNotification>
        (*this, &EchoHandler::socketReadable));
  }

  void socketReadable(ReadableNotification*) {
    char buffer[8];
    int n = socket.receiveBytes(buffer, sizeof(buffer));
    if (n > 0) {
      socket.sendBytes(buffer, n);                       // (13)                                                    
    }
    else {
      reactor.removeEventHandler(socket,                 // (12)
	    Observer<EchoHandler, 
	             ReadableNotification>
	      (*this, &EchoHandler::socketReadable));
      delete this;
    }
  }

 private:
  StreamSocket socket;
  SocketReactor& reactor;
};

class DataHandler {
 public:

  DataHandler(StreamSocket& s, 
              SocketReactor& r):
    socket(s), reactor(r), outFile("reactorOutput.txt") {
    reactor.addEventHandler(socket,                      // (14) 
      Observer<DataHandler, 
               ReadableNotification>
        (*this, &DataHandler::socketReadable));
    reactor.addEventHandler(socket,                      // (15)
      Observer<DataHandler, 
               ShutdownNotification>
         (*this, &DataHandler::socketShutdown));
    socket.setBlocking(false);
  }

  ~DataHandler() {                                       // (16)
    reactor.removeEventHandler(socket, 
      Observer<DataHandler, 
               ReadableNotification>
         (*this, &DataHandler::socketReadable));
    reactor.removeEventHandler(socket, 
      Observer<DataHandler, 
               ShutdownNotification>
        (*this, &DataHandler::socketShutdown));
  }

  void socketReadable(ReadableNotification*) {
    char buffer[64];
    int n = 0;
    do {
      n = socket.receiveBytes(&buffer[0], sizeof(buffer));
      if (n > 0) {
        std::string s(buffer, n);
        outFile << s << std::flush;                     // (17)
      }
      else break;
    } while (true);
  }

  void socketShutdown(ShutdownNotification*) {
    delete this;
  }

 private:
  StreamSocket socket;
  SocketReactor& reactor;
  std::ofstream outFile;
};

class Server: public Poco::Util::ServerApplication {

 protected:
  void initialize(Application& self) {                    // (3)
    ServerApplication::initialize(self);
  }
		
  void uninitialize() {                                   // (4)
    ServerApplication::uninitialize();
  }

  int main(const std::vector<std::string>&) {             // (2)
		
    ServerSocket serverSocketEcho(4711);                  // (5)
    ServerSocket serverSocketData(4712);                  // (6)
    SocketReactor reactor;
    SocketAcceptor<EchoHandler> 
      acceptorEcho(serverSocketEcho, reactor);            // (7)
    SocketAcceptor<DataHandler> 
      acceptorData(serverSocketData, reactor);            // (8)
    Thread thread;
    thread.start(reactor);                                // (9)
    waitForTerminationRequest();
    reactor.stop();                                       // (10)
    thread.join();
        
    return Application::EXIT_OK;

  }

};

int main(int argc, char** argv) {

  Server app;                                             // (1)
  return app.run(argc, argv);

}

(1) erzeugt den TCP-Server. Dieser führt die main-Funktion (2) aus und wird in (3) initialisiert und in (4) deinitialisiert. Die main-Funktion des TCP-Servers erstellt zwei Server-Sockets, die auf Port 4711 5) und Port 4712 (6) lauschen. In den (7) und (5) werden die Server-Sockets mit dem EchoHandler und dem DataHandler erstellt. Der SocketAcceptor modelliert die Acceptor-Komponente des Accepter-Connector-Designmusters [8]. Der Reactor läuft in einem separaten Thread (9), bis er seine Abbruchanforderung erhält (10).

Der EchoHandler registriert sein Lese-Handle im Konstruktor (11) und hebt die Registrierung seines Lese-Handle in der Mitgliedsfunktion socketReadable auf (12). Er sendet die Nachricht des Kunden zurück (13). Im Gegensatz dazu ermöglicht der DataHandler einem Client, Daten an den Server zu übertragen. Der Handler registriert in seinem Konstruktor seine Aktion für Leseereignisse (14) und Shutdown-Ereignisse (Zeile 15). Beide Handler werden im Destruktor des DataHandler (16) wieder abgemeldet. Das Ergebnis der Datenübertragung wird direkt in das Dateihandle outFile geschrieben (17).

Die folgende Ausgabe zeigt auf der linken Seite den Server und auf der rechten Seite die beiden Clients. Eine Telnet-Sitzung dient als Client. Der erste Client verbindet sich mit Port 4711: telnet 127.0.0.1 4711. Dieser Client verbindet sich mit dem Echo-Server und zeigt daher seine Anfrage an. Der zweite Client stellt eine Verbindung zu Port 4712 her: telnet 127.0.0.1 4712. Die Ausgabe des Servers zeigt, dass die Daten des Clients an den Server übertragen werden.

Was sind die Vor- und Nachteile des Reactors?

Vorteile

Nachteile

Es gibt viele bewährte Muster, die im Bereich der Concurrency verwendet werden. Sie befassen sich mit Synchronisierungsproblemen wie Sharing und Mutation, aber auch mit nebenläufigen Architekturen. In meinem nächsten Beitrag beginne ich mit den Mustern, die sich auf die gemeinsame Nutzung von Daten konzentrieren. (rme [14])


URL dieses Artikels:
https://www.heise.de/-8973433

Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Select_(Unix)
[2] https://man7.org/linux/man-pages/man2/poll.2.html
[3] https://en.wikipedia.org/wiki/Epoll
[4] https://en.wikipedia.org/wiki/Kqueue
[5] https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects
[6] https://en.wikipedia.org/wiki/Inversion_of_control
[7] https://pocoproject.org/
[8] http://software-pattern.org/Book/29
[9] https://en.wikipedia.org/wiki/Select_(Unix)
[10] https://man7.org/linux/man-pages/man2/poll.2.html
[11] https://en.wikipedia.org/wiki/Epoll
[12] https://en.wikipedia.org/wiki/Kqueue
[13] https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects
[14] mailto:rme@ix.de