PHPFlüsterer

WebSockets – Vom Client zum Server und zurück (Teil 1)

Dieser Artikel ist der Beginn einer kleinen Serie, die dir das Thema WebSockets und deren Einsatzmöglichkeiten ein wenig näher bringen soll. Bei WebSockets handelt es sich um eine der Neuerungen aus dem HTML5-Paket. WebSockets können ein mächtiges sowie einfach einzusetzendes Feature zugleich sein. Das die Möglichkeiten zum Einsatz vielfältig sind, steht wahrscheinlich außer Frage. Doch einfach einzusetzen? Diesen Eindruck könnte man haben, nachdem man sich die WebSocket-API Spezifikation durchgesehen hat und dabei das recht simple Interface näher betrachtet. Doch es sollte bei meinen Experimenten ganz anders kommen. Dieser Artikel soll dir helfen, nicht in die gleichen Fallen zu tappen wie ich, die folgenden Fragen beantworten:

  • Was sind WebSockets?
  • Wie setzt man WebSockets ein?
  • Was benötige ich um auf einen WebSocket zuzugreifen?
  • Wie sieht die Fallback-Variante für ältere Clients aus?
  • Wer nimmt die Daten entgegen bzw. wer antwortet mir?

und zusätzlich davor bewahren, unnötig viel Zeit bei der Abstimmung der beteiligten Komponenten zu verschwenden. Doch fangen wir vorne an …
 
 

Was ist ein WebSocket eigentlich?

Bevor wir tiefer in die Materie eintauchen, sollten wir vielleicht vorher mal kurz mit der Definition des WebSockets beginnen. Schauen wir doch dazu erstmal was Wikipedia uns verrät:

Das WebSocket-Protokoll ist ein auf TCP basierendes Netzwerkprotokoll, das entworfen wurde, um eine bidirektionale Verbindung zwischen einer Webanwendung und einem WebSocket-Server bzw. einem Web-Server, der auch WebSockets unterstützt, herzustellen.

Also haben wir es mit einer klassischen Socket-to-Socket Verbindung zu tun, so wie man sie auch von anderen Anwendungsfällen kennt. Beim Begriff WebSocket handelt es sich also mehr um ein Kunstwort als um eine vollständig neuartige Implementierung. Der eigentlich Socket wird lediglich durch ein paar Schichten abstrahiert und im Browser über eine API zur Verfügung gestellt. Von dort (Client) aus geht es zum Server (Server) der ebenfalls das WebSocket-Protokoll spricht. Mit diesem Server wird eine Verbindung aufgebaut. Das bedeutet, dass wir es mit mind. zwei Teilnehmern zu tun haben, dem Client und dem Server (WebSocket-Server / Webserver). Wir dürfen hier nur nicht eine entscheidende Komponente vergessen, die Middleware. Doch dazu kommen wir später. Kommen wir nun erst einmal zu Client und dem Server.
 
 

Client (Browser) support für WebSockets

Der aktuelle Status ist “working draft” in Version 17 und das Protokoll liegt in Version 13 vor. Details zur aktuellen Unterstützung durch die diversen Clients (Browser) können wir der folgenden Abbildung entnehmen:

WebSocket Unterstützung in Webbrowsern - Statistik von caniuse.com/WebSocket

WebSocket Unterstützung in Webbrowsern – Statistik von caniuse.com/WebSocket

Um im Browser via JavaScript auf das WebSocket-Interface zugreifen zu können, muss dieses ja in erster Linie existieren. Da wir aufgrund der vielfältigen Browser-Versionen nicht davon ausgehen können, dass jeder Client WebSockets unterstützt, kommen wir zur ersten Hürde die zu nehmen ist. Wir müssen also vor dem Zugriff wissen, ob der Client dieses Feature überhaupt unterstützt. Außerdem wollen wir via JavaScript auf die Schnittstelle zugreifen und eine akzeptable Fallback-Lösung haben. Bezüglich dieser Anforderungen war die Auswahl schon recht stark reduziert, zumal ich eine Lösung suchte, die zusätzlich auch noch auf jQuery basieren soll. Ich setze jQuery bei einer Vielzahl von Projekten ein und somit sollte hier nicht schon wieder eine vollständig neue Bibliothek eingesetzt werden müssen. Zusammengefasst haben folgende Voraussetzungen meine Entscheidung beeinflusst:

  • das Gewicht der Seite soll nicht unnötig steigen
  • das Gewicht der Seite soll nicht zu sehr steigen
  • aufrgunddessen möglichst auf eine auf jQuery-basierende Lösung zurückgegriffen wird
    (aufgrund der bestehenden XHR-Implementierung für die Fallback-Variante)
  • die Lösung muss stabil sein (reliable + rock solid)

und so blieb letztlich nur ein Kandidat übrig:

Alle anderen Lösungen haben entweder nicht korrekt funktioniert, oder hatten keine solide Fallback-Variante implementiert. Als Fallback-Variante wollte ich eine auf AJAX-long-polling basierende Lösung. Dieses Verfahren ist, jedenfalls zum aktuellen Zeitpunkt noch, der Standard für das kontinuierliche Beziehen von Daten vom Server. Bei diesem Verfahren wird über das XHR-Interface des Browsers in der Regel in fest definierten Abständen ein Request abgesetzt und die Antwort vom Server auf neue Inhalte geprüft und entsprechend weiter bearbeitet. Dieses Verfahren hat sich bewährt, da es einfach zu implementieren ist und fast alle gängigen JavaScript-Bibliotheken vereinfachte Schnittstellen zum XmlHttpRequest-Objektes des Browsers zur Verfügung stellen. Wenn du dich fragst, warum ich nicht auf eine fertige Endpunkt-zu-Endpunkt Implementierung wie z. B. Socket.io zurückgegriffen habe, zumal ich gleich bei der Gegenstelle (Server) auf Node.js eingehen werde, der sei darauf hingewiesen, dass der Trade-off einfach zu groß wäre mit einer Client-Bibliothek (socket.io.js) die unkomprimiert 63 KB und minified immer noch 32 KB schwer ist. Das widerspricht (leider) den o. g. Voraussetzungen.
 
 

Gegenstelle – der Socket-Server

Wie wir vom Client aus einen Request absetzen können, ist jetzt (zumindest theoretisch) schon mal geklärt. Doch wer nimmt unsere Anfrage entgegen und antwortet uns auf diese? Meine Recherchen haben eine Vielzahl an möglichen Lösungen zu Tage gebracht. Diese reichten von simpel, properitär und experimentell bis zu stabil und einsetzbar. Die Server-Software die ich mir angeschaut habe, möchte ich der Vollständigkeit halber hier mal aufführen:

Es gibt also eine Vielzahl an möglicher Server-Software die wir einsetzen könnten. Hier bin ich in die erste größere Falle gelaufen. Achte bei deiner Wahl des Socket-Servers unbedingt auf die Unterstützung der aktuellsten Protokoll-Version. Aus Gründen der Sicherheit wurde das Handshake-Verfahren im Laufe der Protokoll-Evolution entscheidend verändert, so daß eine augenscheinlich gut geschrieben Open-Source-Lösung in PHP mich aufgrund der alten Protokoll-Implementierung fast zum Verzweifeln gebracht hat. Nach ausgiebigem Debugging, Fehler finden und beseitige, sowie anschließend korrekter Implementierung des Handshakes musste ich feststellen, dass PHP in Verbindung mit dem Apachen in Version 2.x einfach nicht das richtige Werkzeug zu sein scheint. Es performte einfach nicht so, wie ich es mir vorgestellt hatte. Somit habe ich mich umorientieren müssen. Aufgrund der Artikel und Bücher die ich in den letzten Monaten gelesen habe sowie den Erfahrungen die ich mit Node.js sammeln konnte, habe ich mich dann für den Einsatz von Node.js in Verbindung mit dem Paket WebSocket-Node entschieden. Node.js ist äußerst performant, skaliert hervorragend und es gibt eine Vielzahl von Paketen mit denen Node.js erweitert werden kann. So stehen uns z. B. mit Cradle ein Paket zum Zugriff auf CouchDB oder mit node-mysql ein Paket zum Zugriff auf MySQL-Datenbanken zur Verfügung. Ich werde mich in diesem HowTo, beim Einsatz des Socket-Servers, allerdings auf die Auslieferung von Dummy-Daten beschränken. Eine Datenbank im Backend anzubinden ist dann allerdings nur noch ein kleiner Schritt. Dieser kann in der Regel auf Basis der Informationen und mit Hilfe der HowTo’s auf den jeweiligen Projektseiten realisiert werden.
 
 

Die URI-Schemen – wie ist die Adresse?

Die WebSocket-Protokoll-Spezifikation definiert zwei neue URI-Schemen, ws: für unverschlüsselte, und wss: für verschlüsselte Verbindungen.
 
 

Das Protokoll – welche Sprache sprechen wir?

Zu Beginn einer jeden Verbindung führen Server und Client einen sogenannten Handshake durch. Dieser ähnelt vom Aufbau her dem HTTP-Header und ist vollständig abwärtskompatibel zu diesem, was die Nutzung des Standard-HTTP-Ports “80” zugleich für normale HTTP-Kommunikation als auch für die Websocket-Nutzung ermöglicht. Der Handshake beinhaltet außerdem weitere Informationen (z. B. die verwendete Protokollversion).
 
 

Anfrage des Clients

Im Folgenden ist ein Beispielhandshake des siebzehnten Protokollentwurfs (17) dargestellt und erläutert:

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Achtung: Ältere Protokollentwürfe können sehr stark von dieser Version des Protokolls abweichen, da es aufgrund von Sicherheitsbedenken umgestaltet wurde!

Wie auch im HTTP-Protokoll gibt der Client an, auf welche Ressource (hier: /chat) und auf welchen Host (hier: server.example.com) er zugreifen möchte. Außerdem fordert der Client ein Upgrade auf das Websocket-Protokoll. Der zufällig generierte “Sec-WebSocket-Key” dient zur Überprüfung, ob der Server die Anfrage tatsächlich gelesen und verstanden hat (siehe Abschnitt Antwort des Servers). Unter “Sec-WebSocket-Protocol” hat der Client die Möglichkeit, auf das Websocket-Protokoll aufbauende Protokolle anzugeben, die die Clientanwendung unterstützt (hier: ein Chat-Protokoll). Selbsterklärend sollte unter “Sec-WebSocket-Version” die verwendete Protokollversion angegeben werden.
 
 

Antwort des Servers

Auf die obige Beispielanfrage könnte ein Websocket-Server beispielsweise wie folgt antworten:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Durch den HTTP-Statuscode 101 und die folgenden zwei Zeilen erklärt der Server, dass er mit dem Wechsel des Protokolls einverstanden ist.

Der zurückgesendete Schlüssel unter “Sec-WebSocket-Accept” dient der Verifikation, dass der Server die Anfrage des Clients gelesen hat. Er wird wie folgt erstellt: An den oben erwähnten, Base64-kodierten String, den der Client sendet (“Sec-WebSocket-Key”) wird der Globally Unique Identifier 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 angehängt. Daraufhin wird ein SHA1-Hash des entstandenen Schlüssels erstellt und Base64-kodiert. Anzumerken ist hierbei, dass der ursprünglich empfangene Schlüssel zwar Base64-kodiert ist, jedoch zu keinem Zeitpunkt dekodiert wird.

In diesem Beispiel gibt der Server zusätzlich an, dass er das angeforderte Protokoll “chat” kennt (“Sec-WebSocket-Protocol”).
 
 

Zusammenfassung

Fassen wir zusammen: Wir wissen jetzt was ein WebSocket ist, wie man auf diesen zugreifen und entsprechend nutzen kann (jedenfalls in der Theorie), an welchen Stellen wir besonders acht geben müssen und welche Software Client- und Server-seitig eingesetzt werden kann. Jetzt ist es an der Zeit die Informationen aus der Theorie in die Praxis zu überführen. Während ich an Teil 2 schreibe und die Codebeispiel vorbereite, empfehle ich dir, dich mit der angebotenen Software ein wenig näher zu befassen, da ich z. B. nicht im Detail auf die Funktionsweise von jQuery oder Node.js eingehen werden kann. Zu Node.js: Die Installation geht wirklich fix von der Hand und das experimentieren mit Node.js macht Spaß – so findet man am besten den Einstieg für die weitere Entwicklung mit Node.js.
 
 

Ausblick

In Teil 2 der Artikel-Serie werde ich Client- und Server-seitige Codebeispiele zeigen, erklären warum das Plugin jquery.gracefulWebSocket.js den Anforderungen vielleicht doch nicht reicht und warum wir ggf. mehr Komponenten benötigen als anfänglich gedacht (wie z. B. HAProxy).

Categories: Guides

jQuery-Plugin – jquery.ezy » « PHP Coding-Standards und Programmierrichtlinien

2 Comments

  1. Freu mich auf Teil 2!

  2. Ja, ich hätte mich auch gefreut, aber offenbar gibt es keinen Teil 2 mehr. Somit ist der Artikel ganz nett aber wenig hilfreich.

Leave a Reply

Your email address will not be published.

*

CAPTCHA Image

*

Copyright © 2016 PHPFlüsterer

Theme by Anders NorenUp ↑