Webbasierte Software für für E-Sport und Business

Sichere Echtzeit-Anwendungen mit AutobahnJS und Thruway auf Basis von Symfony realisieren / Teil 1

Veröffentlicht von Henrik Braune am 24.08.2016

Henrik Braune

Moderne webbasierte Anwendungen werden von vielen Nutzern gleichzeitig genutzt, die mittelbar oder unmittelbar miteinander interagieren. HTTP und Ajax stoßen hier an ihre Grenzen. Die Websocket-Technologie schafft Abhilfe, indem Verbindungen zwischen Server und Client aufrecht erhalten werden, die für die bidirektionale Kommunikation genutzt werden können. Wir zeigen, wie sich Websockets auch auf Basis von PHP und Symfony realisieren lassen.

Was sind Websockets?

Klassische Anwendungen im Web nutzen das Netzwerkprotokoll HTTP, um Anfragen an den Server zu senden und darauf folgende Antworten zu verarbeiten. Dabei wurde stets die gesamte HTML-Seite vom Server geladen – mit der Folge, dass sich die Bedienung von Webanwendungen deutlich von Desktop-Anwendungen unterschied. Die AJAX-Technologie prägte als wichtige Basis vieler Frameworks (AngularJS, jQuery, etc.) in der Folge die Entwicklung moderner Anwendungen, indem es die asynchrone Datenübertragung und das Laden von Inhalten als Hintergrundprozess ermöglichte. Doch auch AJAX hat den Nachteil, dass der Datenfluss unidirektional vom Client zum Server gerichtet ist.

Heutige Anwendungen werden meist von vielen Nutzern gleichzeitig genutzt. Ihre Interaktionen in der Anwendung beeinflussen sich gegenseitig. Denken Sie an einen klassischen Support-Chat auf der Webseite Ihres Telekommunikationsanbieters: Sie befinden sich in einem Feed mit einem Mitarbeiter des Unternehmens und schreiben sich gegenseitig Nachrichten. Mit AJAX wäre es notwendig, immer wieder beim Server nachzufragen, ob neue Nachrichten des Mitarbeiters vorliegen. Das geht dann so:

  • Client an Server: "Hat der Mitarbeiter schon geantwortet?"
    Server an Client: "Nein"
    ... 2 Sekunden Pause

  • Client an Server: "Hat der Mitarbeiter schon geantwortet?"
    Server an Client: "Nein"
    ... 2 Sekunden Pause

So geht es also immer weiter. Bei steigender Nutzerzahl wird der Server immer weiter belastet. Hinzu kommt der Nachteil, dass die Antworten im schlechtesten Fall erst mit einer Verzögerung von zwei Sekunden beim Nutzer erscheint. Beide Probleme lösen Websockets: Sie erlauben dem Server, direkt Kontakt mit dem Client (Browser) aufzunehmen – die Verbindung ist birektional. Server und Client bleiben miteinander verbunden und können sich gegenseitig informieren, sofern Ereignisse der Applikation (z.B. eine neue Nachricht des Mitarbeiters) dies implizieren.

Websockets sind ein Standand-Protokoll, das von allen modernen Browsern unterstützt wird. Sockets werden auf Server-Seite von vielen Programmiersprachen und Frameworks supported. Der bekannteste Vertreter ist sicherlich Socket.IO für NodeJS. Sockets sind aber auch mit PHP möglich, z.B. über die Bibliotheken ReactPHP und Ratchet.

Websocket = Websocket?

Der Begriff "WebSocket" umfasst als ein auf TCP basierendes Protokoll zunächst einmal eine ganze Sammlung von Technologien, deren Implementierung keine einheitlichen Feature-Sets abbildet. Wichtige Features sind u.a. "PubSub" (Publish & Subscribe) sowie "RPC" (Remote Procedure Calls). Die Wahl der Technologie hängt daher von konkreten Projektanforderung ab und macht deutlich, dass hinter dem populären Begriff "Websockets" eine ganze Reihe unterschiedlicher Ansätze mit eigener Daseinsberechtigung stehen. Einen sehr guten Vergleich der einzelnen Technologien und Feature-Sets gibt es auf der Webseite des WAMP-Protokolls. WAMP steht für "Web Application Messaging Protocol" und definiert ein Subprotokoll, das in diversen Implementierungen eine breite Zustimmung gefunden.

Websockets und Symfony

In einem ERP-Projekt für einen unserer Kunden gab es die konkrete Anforderungen, dass Aktivitäten der Plattform in Echtzeit an alle Nutzer übermittelt werden und in Form eines kleines Popups über Neuerungen informieren (ähnlich wie die Funktion "Noch zwei Besucher sehen sich das Hotel an." von booking.com). Für die auf Symfony und AngularJS basierende API-Plattform implementierten wir einen Websocket-WAMP-Server auf Basis von PHP, mit dem sich die Anwendung beim Start verbinden kann. Das von uns gewählte Setup bestand aus folgenden Komponenten:

Frontend

AngularJS, Javascript MVC Framework
AutobahnJS, Javascript Implementierung des Web Application Messaging Protocol
AngularWAMP, Angular-Modul für die Integration von AutobahnJS

Backend

Symfony, PHP MVC Framework
ReactPHP Socket und Ratchet, Socket Bibliotheken
ThruwayPHP WAMP Client und Router
ThruwayBundleSymfony Bundle als Wrapper für Thruway

Einer bestehenden Anwendung kann das ThruwayBundle über folgenden Befehl hinzugefügt werden:

php composer.phar require voryx/thruway-bundle

Anschließend muss das Paket im AppKernel registriert werden. Anschließend müssen die Parameter definiert werden. 

parameters:
    ...
    voryx_thruway.url: 'ws://127.0.0.1:2345'
    voryx_thruway.trusted_url: 'ws://127.0.0.1:2346'
    voryx_thruway.ip: 127.0.0.1
    voryx_thruway.port: '2345'
    voryx_thruway.trusted_port: '2346'

Die Ports können dabei frei gewählt werden, solange sie nicht in Konflikt mit anderen Anwendungen stehen. Thruway und der Websocket Server sind jetzt einsatzbereit. Über folgenden Befehl kann der Router gestartet werden:

php app/console thruway(router)start

Daraufhin startet ein dauerhaft laufender Prozess im Rahmen eines Event Loops, der in Produktionsumgebungen im Hintergrund ausgeführt werden sollte (der Start der Prozesse kann beispielsweise in systemd als Service registriert werden):

Making a go at starting the Thruway Router
2016-08-28T14(10)40.6081240 info [Customer\AppBundle\Authentication\WampAuthProvider 42821] New client created
...
2016-08-28T14(10)40.6292680 info [Thruway\Peer\Router 42821] Starting loop

In Javascript auf ein Event registrieren

Nachdem der Router gestartet wurde, können sich Clients mit dem Server verbinden. Um dabei auf bestimmte Ereignisse zu reagieren, können die Clients gemäß des PubSub-Pattern diese Nachrichten abonnieren (Subscribe). Folgender Ausschnitt aus dem AngularJS-Code zeigt das Vorgehen:

var chat = angular.module('chat', [
  'vxWamp'
]).config(function(
  $wampProvider
) {
  $wampProvider.init({
    url: 'ws://127.0.0.1:2345/',
    'realm': 'thruway.chat'
  });
});

chat.controller('ChatCtrl', ['$scope', '$wamp', '$rootScope', '$http', function($scope, $wamp, $rootScope, $http) {
  // Das Event "chat.new" wird abonniert.
  // Sofern eine neue Chatnachricht eingeht, wird diese der Scope-Variablen hinzugefügt.
  $wamp.subscribe('chat.new', function(args) {
    $scope.messages.push({text: args[0]});
  });
}]);

chat.run(function($wamp){
  // Die Verbindung zum WAMP-Server wird beim Start der Angular-Anwendung geöffnet.
  $wamp.open();
});

angular.bootstrap(document, ['chat']);

Innerhalb der Symfony Anwendung kann nun immer dann, wenn eine neue Nachricht gespeichert wird, eine Nachricht an alle über den WebSocket verbundenen Clients gesendet werden. Hierzu genügt es, über den Container den "thruway.client"-Service zu beziehen und die Publish-Methode im Sinne des PubSub-Patterns aufzurufen: 

/**
 * @Route("/chats/{chat}/messages")
 * @Method("POST")
 * @ParamConverter("chat", class="BrauneDigitalChatBundle:Chat", options={"id" = "chat"})
 */
public function writeMessageAction(Request $request, $chat) {
  $message = new Message();
  $messageText = $request->request->get('messageText');
  $message->setText($messageText);
  $message->setChat($chat);

  $user = $this->getUser();
  $message->setUser($user);

  $em = $this->getDoctrine()->getManager();
  $em->persist($message);
  $em->flush();

  // Benachrichtigung der verbundenen Clients und Senden der Nachricht
  $client = $this->container->get('thruway.client');
  $client->publish("chat.new", [$message->getText()], ['acknowledge' => true]);

  return $this->formatResponse($message);
}

Die Angular-App wird nun direkt benachrichtigt, ohne dass dazu ein klassischer AJAX-Call notwendig wäre. Das kleine Beispiel zeigt, dass sich Websockets sehr einfach in eine bestehende Symfony-Anwendung integrieren lassen und Applikationen um Echtzeit-Kommunikation erweitern können.

Ist die Websocket Verbindung sicher?

Herkömmliche Websocket-Verbindungen sind nicht verschlüsselt und daher zunächst einmal nicht sicher. Auch die Nutzer-Authentifizierung haben wir im oben aufgeführten Beispiel noch nicht berücksichtigt. Im zweiten Teil dieses Beitrags werden wir zeigen, wie man mit einem Websocket-Proxy über nginx mit einem bestehenden SSL-Zertifikat eine HTTPS-Verbindung aufbauen kann und darüber hinaus eine Nutzer-Authentifizierung in die Security-Komponente von Symfony integriert.  

© 2021, Braune Digital GmbH, Niemannsweg 69, 24105 Kiel, +49 (0) 431 55 68 63 59