Windows 8-Netzwerke

Windows 8 und das WebSocket-Protokoll

Kenny Kerr

 

Das Ziel des WebSocket-Protokolls ist, bidirektionale Kommunikation bereitzustellen – in einer webgesättigten Welt voller Clients, die ausschließlich Verbindungen herstellen und Anforderungs-/Antwortpaare initiieren. Durch das WebSocket-Protokoll können die Anwendungen endlich viele weitere TCP-Vorteile nutzen, allerdings auf webfreundliche Weise. Wenn Sie bedenken, dass das WebSocket-Protokoll erst im Dezember 2011 von der Internet Engineering Task Force (IETF) standardisiert wurde – und vom World Wide Web Consortium (W3C) die Standardisierung noch erwogen wird, während ich dies schreibe – überrascht es vielleicht, wie umfassend diese neue Internettechnologie in Windows 8 integriert wurde.

In diesem Artikel zeige ich zunächst, wie das WebSocket-Protokoll funktioniert und in welcher Beziehung es zur umfangreicheren TCP/IP-Suite steht. Ich gehe anschließend auf die verschiedenen Methoden ein, mit denen die Programmierer die neue Technologie unter Windows 8 einfach innerhalb ihrer Anwendungen integrieren können.

Gründe für WebSocket

Das Protokoll soll hauptsächlich eine effiziente Standardmethode für browserbasierte Anwendungen bereitstellen, um außerhalb von Anforderungs-/Antwortpaaren frei kommunizieren zu können. Vor einigen Jahren diskutierten Webentwickler aufgeregt darüber, wie AJAX (Asynchronous JavaScript und XML) dynamische und interaktive Szenarios ermöglichte. Das stimmte zwar, aber all dies war vom XMLHttpRequest-Objekt inspiriert, mit dem die Browser weiterhin nur HTTP-Anforderungen ausführen konnten. Was ist, wenn der Server eine Out-of-Band-Nachricht an den Client senden möchte? An dieser Stelle kommt das WebSocket-Protokoll zum Tragen. Mit ihm kann der Server nicht nur Nachrichten an den Client senden, sondern sendet diese sogar ohne den Mehraufwand von HTTP. Das WebSocket-Protokoll stellt so bidirektionale Kommunikation bereit, die nahezu die Geschwindigkeit einer rohen TCP-Verbindung aufweist. Ohne das WebSocket-Protokoll mussten die Webentwickler HTTP zweckentfremden: Sie sendeten zur Aktualisierung Abfragen an den Server, setzten Programmiertechniken im Stil von Comet ein und verwendeten zahlreiche HTTP-Verbindungen mit viel Protokollaufwand, nur damit die Anwendungen aktuell blieben. Die Server werden überlastet, Bandbreite wird verschwendet und die Webanwendungen werden übermäßig kompliziert. Das WebSocket-Protokoll löst diese Probleme überraschend einfach und effizient. Aber bevor ich die Funktionsweise des Protokolls erläutere, möchte ich einigen grundlegenden und historischen Kontext darlegen.

TCP/IP-Suite

TCP/IP ist eine Protokollsuite oder Sammlung miteinander verwandter Protokolle, die die Internetarchitektur implementiert. Sie hat sich über viele Jahre zu ihrer derzeitigen Form entwickelt. Das Konzept der Paketvermittlungsnetzwerke begann sich in den 1960ern zu entwickeln. Seither hat sich die Welt stark verändert. Die Computer sind viel schneller, und die Software stellt viel höhere Anforderungen. Das Internet ist explodiert und stellt ein allumfassendes Web dar, das Informationen, Kommunikation und Interaktion bietet und die Basis von vielen heute beliebten Anwendungen ist.

Die TCP/IP-Suite besteht aus einer Anzahl von Schichten, die lose nach dem OSI-Schichtenmodell (Open System Interconnection) angeordnet sind. Obwohl die Protokolle auf den verschiedenen Schichten nicht besonders gut abgegrenzt sind, hat sich TCP/IP eindeutig als effektiv erwiesen, und die Schichtenprobleme wurden durch eine clevere Kombination von Hardware- und Softwareentwürfen überwunden. Die Aufteilung von TCP/IP in Schichten, so unpräzise sie auch sein mögen, hat die TCP/IP-Entwicklung im Lauf der Zeit gefördert, während Hardware und Technologie sich veränderten. Programmierer mit unterschiedlichen Fähigkeiten konnten dadurch auf verschiedenen Abstraktionsebenen arbeiten und entweder die Erstellung des Protokollstapels selbst unterstützen oder Anwendungen erstellen, die dessen verschiedene Funktionen nutzten.

Die untersten Schichten umfassen die physischen Protokolle, darunter z. B. die verdrahtete Medienzugriffssteuerung und Wi-Fi, um die physische Verbindung sowie die lokale Adressierung und Fehlererkennung bereitzustellen. Die meisten Programmierer schenken diesen Protokollen nicht viel Beachtung.

Eine Ebene weiter oben im Stapel befindet sich das Internetprotokoll (IP) selbst in der Netzwerkschicht und sorgt somit für die Interoperabilität von TCP/IP über die verschiedenen physischen Schichten. Es ordnet Computeradressen physischen Adressen zu und leitet Pakete von Computer zu Computer weiter.

Des Weiteren gibt es Zusatzprotokolle, bei denen wir darüber diskutieren könnten, auf welcher Schicht sie sich befinden, die aber wirklich eine notwendige unterstützende Rolle spielen, u. a. bei der automatischen Konfiguration, der Namensauflösung und Ermittlung, bei Optimierungen des Routings und bei der Diagnose.

Weiter oben im Stapel kommen wir zu den Transport- und Anwendungsprotokollen. Die Transportprotokolle erledigen das Multiplexing und Demultiplexing der Pakete von den unteren Schichten, sodass sich viele verschiedene Anwendungen den Kommunikationskanal teilen können, obwohl es möglicherweise nur eine einzelne physische Schicht und Netzwerkschicht gibt. Die Transportschicht stellt normalerweise auch eine weitere Fehlererkennung bereit, verlässliche Übermittlung und sogar leistungsbezogene Funktionen wie Überlastungssteuerung und Flusssteuerung. Auf der Anwendungsschicht waren traditionell Protokolle wie HTTP (durch Webbrowser und Server implementiert) und SMTP (durch E-Mail-Clients und Server implementiert) angesiedelt. Als Protokolle wie HTTP weltweit immer mehr an Bedeutung gewannen, wurde deren Implementierung in die Tiefen des Betriebssystems verlagert, um sowohl die Leistung zu verbessern als auch die Implementierung auf verschiedene Anwendungen aufzuteilen.

TCP und HTTP

Von den Protokollen in der TCP/IP-Suite sind wohl TCP und UDP (User Datagram Protocol) auf der Transportschicht die Protokolle, die den meisten Programmierern am besten bekannt sind. Beide definieren eine „Port“-Abstraktion, die von den Protokollen in Kombination mit IP-Adressen für das Multiplexing und Demultiplexing von Paketen genutzt wird, wenn die Pakete empfangen und gesendet werden.

Obwohl UDP häufig für andere TCP/IP-Protokolle verwendet wird, z. B. DHCP (Dynamic Host Configuration Protocol) und DNS, und verbreitet für private Netzwerkanwendungen übernommen wurde, ist es im Internet insgesamt nicht so weit verbreitet wie sein Gefährte. TCP hingegen ist durchgehend weit verbreitet, zum großen Teil dank HTTP. TCP ist zwar viel komplexer als UDP, aber viel von dieser Komplexität wird vor der Anwendungsebene verborgen, auf der die Anwendung von den TCP-Vorteilen profitiert, ohne die Komplexität zu erfahren.

TCP bietet einen verlässlichen Datenfluss zwischen Computern, dessen Implementierung sehr komplex ist. TCP behandelt die Paketfolge und Datenrekonstruktion, Fehlererkennung und Wiederherstellung, Überlastungssteuerung und Leistung, Timeouts, erneute Übertragungen und noch vieles mehr. Die Anwendung sieht dagegen nur eine bidirektionale Verbindung zwischen Ports und geht davon aus, dass die gesendeten und empfangenen Daten korrekt und in der richtigen Reihenfolge übermittelt werden.

Das heutige HTTP unterstellt ein verlässliches, verbindungsorientiertes Protokoll, und TCP ist eindeutig die offensichtliche und universelle Wahl. In diesem Modell funktioniert HTTP als ein Client/Server-Protokoll. Der Client öffnet eine TCP-Verbindung zu einem Server. Er sendet dann eine Anforderung, die vom Server evaluiert und beantwortet wird. Dies wiederholt sich weltweit ungezählte Male in jeder Sekunde an jedem Tag.

Natürlich ist das eine Vereinfachung oder Einschränkung der Funktionalität, die TCP bietet. Mit TCP können beide Parteien gleichzeitig Daten senden. Das eine Ende muss nicht darauf warten, dass das andere eine Anforderung sendet, bevor es antwortet. Diese Vereinfachung ermöglichte allerdings das serverseitige Zwischenspeichern von Antworten, mit großen Auswirkungen auf die Skalierfähigkeit des Web. Zur Beliebtheit von HTTP trug aber zweifellos dessen ursprüngliche Einfachheit bei. TCP stellt einen bidirektionalen Kanal für binäre Daten bereit, ein Paar von Strömen, wenn man so will. HTTP stellt dagegen eine Anforderungsnachricht bereit, die einer Antwortnachricht vorangeht, wobei beide aus ASCII-Zeichen bestehen, obwohl die Nachrichtentexte, falls vorhanden, auf andere Art codiert sein können. Ein einfache Anforderung kann zum Beispiel folgendermaßen aussehen:

GET /resource HTTP/1.1\r\n
host: example.com\r\n
\r\n

Jede Zeile endet mit einem Wagenrücklauf (\r) und einem Zeilenumbruch (\n). Die erste Zeile, Anforderungszeile genannt, gibt die Zugriffsmethode für eine Ressource an (in diesem Fall GET), den Ressourcenpfad und schließlich die HTTP-Version, die verwendet werden soll. Ähnlich wie bei den Protokollen der unteren Schichten stellt HTTP Multiplexing und Demultiplexing über diesen Ressourcenpfad bereit. Dieser Anforderungszeile folgen eine oder mehrere Headerzeilen. Header bestehen aus einem Namen und einem Wert, wie im vorhergehenden Beispiel gezeigt. Einige Header sind zwar erforderlich, wie der Hostheader, die meisten jedoch nicht. Sie dienen ausschließlich dazu, Browser und Server bei der effizienteren Kommunikation zu unterstützen oder Features und Funktionalität auszuhandeln.

Ein Antwort kann beispielsweise folgendermaßen aussehen:

HTTP/1.1 200 OK\r\n
content-type: text/html\r\n
content-length: 1307\r\n
\r\n
<!DOCTYPE HTML><html> ... </html>

Das Format ist im Grunde dasselbe, aber anstelle einer Anforderungszeile bestätigt eine Antwortzeile die zu verwendende HTTP-Version, einen Statuscode (200) und eine Beschreibung des Statuscodes. Der Statuscode 200 gibt für den Client an, dass die Anforderung erfolgreich verarbeitet wurde und alle etwaigen Ergebnisse sofort nach den gesamten Headerzeilen folgen. Der Server kann zum Beispiel angeben, dass die angeforderte Ressource nicht vorhanden ist, indem er den Statuscode 404 zurückgibt. Die Header haben dasselbe Format wie die in der Anforderung. In diesem Fall informiert der Inhaltstypheader den Browser, dass die angeforderte Ressource im Nachrichtentext als HTML zu interpretieren ist, und der Inhaltslängenheader teilt dem Browser mit, wie viele Bytes der Nachrichtentext umfasst. Eine wichtige Information, denn wie Sie sich erinnern werden, gehen HTTP-Nachrichten über TCP, und dies stellt keine Nachrichtengrenzen bereit. Ohne eine Inhaltslänge müssen HTTP-Anwendungen verschiedene Heuristiken verwenden, um die Länge eines Nachrichtentexts zu bestimmen.

Das ist alles recht einfach, ein Beweis für das geradlinige Design von HTTP. Aber HTTP ist nicht mehr einfach. Die heutigen Webbrowser und Server sind Programme auf dem aktuellen Stand der Technik mit Tausenden von miteinander verwandten Features, und HTTP ist das Arbeitspferd, das mit allem mithalten muss. Viel Komplexität entstand aus der Notwendigkeit von Geschwindigkeit. Es gibt inzwischen Header zum Aushandeln der Komprimierung des Nachrichtentexts, Zwischenspeicher- und Ablaufheader zur Vermeidung der Übermittlung eines Nachrichtentexts und vieles mehr. Es wurden Techniken entwickelt, um durch Kombinieren verschiedener Ressourcen die Anzahl der HTTP-Anforderungen zu reduzieren. Weltweit wurden Netzwerke für die Inhaltsübermittlung (Content Delivery Networks, CDNs) verteilt, um Ressourcen, auf die Webbrowser normalerweise zugreifen, näher bei diesen Webbrowsern zu hosten.

Trotz all dieser Fortschritte könnten viele Webanwendungen eine größere Skalierbarkeit und sogar Einfachheit erreichen, wenn es eine Methode gäbe, um gelegentlich aus HTTP auszubrechen und zum Streamingmodell von TCP zurückzukehren. Genau das bietet das WebSocket-Protokoll.

WebSocket-Handshake

Das WebSocket-Protokoll passt ordentlich über TCP und neben HTTP in die TCP/IP-Suite. Zu den Aufgaben bei der Einführung eines neuen Protokolls im Internet gehört, die zahllosen Router, Proxys und Firewalls glauben zu machen, es sei alles wie immer. Das WebSocket-Protokoll erreicht dieses Ziel, indem es sich als HTTP tarnt, bevor es auf derselben zugrunde liegenden TCP-Verbindung zur eigenen WebSocket-Datenübertragung wechselt. Auf diese Weise müssen viele ahnungslose Zwischenstellen nicht aktualisiert werden, um die WebSocket-Kommunikation auf ihren Netzwerkverbindungen passieren zu lassen. In der Praxis funktioniert das nicht immer so reibungslos, da einige überehrgeizige Router die HTTP-Anforderungen und -Antworten manipulieren, um sie für ihre Zwecke neu zu schreiben, zum Beispiel Proxyzwischenspeicherung, Adressübersetzung oder Ressourcenübersetzung. Eine schnelle, effektive Lösung ist die Verwendung des WebSocket-Protokolls über einen sicheren Kanal, Transport Layer Security (TLS), da dies die Manipulationen auf ein Minimum beschränkt.

Das WebSocket-Protokoll übernimmt Ideen von verschiedenen Quellen, darunter IP, UDP, TCP und HTTP, und stellt Webbrowsern und anderen Anwendungen diese Konzepte in vereinfachter Form zur Verfügung. Den Anfang macht ein Handshake, der genau wie ein HTTP-Anforderungs-/Antwortpaar aussehen und ausgeführt werden soll. Der Zweck dabei ist nicht, dass Clients oder Server sich gegenseitig dazu verleiten, WebSockets zu verwenden, sondern die verschiedenen Zwischenstellen sollen meinen, dass nur eine weitere TCP-Verbindung HTTP bereitstellt. Tatsächlich ist das WebSocket-Protokoll speziell dafür ausgelegt, zu verhindern, dass eine Partei eine Verbindung versehentlich annimmt. Zu Beginn sendet ein Client einen Handshake, der praktisch eine HTTP-Anforderung ist und folgendermaßen aussehen kann:

GET /resource HTTP/1.1\r\n
host: example.com\r\n
upgrade: websocket\r\n
connection: upgrade\r\n
sec-websocket-version: 13\r\n
sec-websocket-key: E4WSEcseoWr4csPLS2QJHA==\r\n
\r\n

Wie Sie sehen, gibt es nichts, wodurch dies keine absolut gültige HTTP-Anforderung sein kann. Eine ahnungslose Zwischenstelle sollte diese Anforderung einfach an den Server weitergeben, der sogar ein HTTP-Server sein kann, der als WebSocket-Server doppelt fungiert. Die Anforderungszeile in diesem Beispiel gibt eine GET-Standardanforderung an. Das bedeutet auch, dass ein WebSocket-Server zulassen kann, dass mehrere Endpunkte durch einen einzelnen Server bearbeitet werden, auf dieselbe Weise, wie das die meisten HTTP-Server ausführen. Der Hostheader ist für HTTP 1.1 erforderlich und dient demselben Zweck, nämlich der Einigung beider Parteien auf die Hostingdomäne in Szenarios mit gemeinsamem Hosting. Die Aktualisierungs- und Verbindungsheader sind ebenfalls HTTP-Standardheader, mit denen die Clients eine Aktualisierung des in der Verbindung genutzten Protokolls anfordern. Diese Technik wird manchmal von HTTP-Clients für den Übergang zu einer sicheren TLS-Verbindung verwendet, obgleich dies selten ist. Diese Header sind für das WebSocket-Protokoll erforderlich. Im Einzelnen gibt der Aktualisierungsheader an, dass die Verbindung auf das WebSocket-Protokoll aktualisiert werden muss, und der Verbindungsheader gibt an, dass dieser Aktualisierungsheader verbindungsspezifisch ist, was bedeutet, dass er nicht von Proxys über weitere Verbindungen kommuniziert werden soll.

Der sec-websocket-version-Header muss hinzugefügt werden, und sein Wert muss 13 sein. Wenn der Server ein WebSocket-Server ist, diese Version jedoch nicht unterstützt, bricht er den Handshake ab und gibt einen entsprechenden HTTP-Statuscode zurück. Wie Sie gleich sehen werden, ist der Client dazu ausgelegt, die Verbindung abzubrechen, sogar wenn der Server das WebSocket-Protokoll ignoriert und problemlos eine Antwort über den Erfolg zurückgibt.

Der sec-websocket-key-Header ist wirklich der Schlüssel für den WebSocket-Handshake. Die Entwickler des WebSocket-Protokolls wollten sicherstellen, dass ein Server keine Verbindung von einem Client akzeptieren kann, der tatsächlich kein WebSocket-Client ist. Sie wollten verhindern, dass ein schädliches Skript eine Formularübertragung erstellen oder das XMLHttpRequest-Objekt verwenden kann, um eine WebSocket-Verbindung durch Hinzufügen der sec-*-Header vorzutäuschen. Um beiden Parteien zu beweisen, dass eine rechtmäßige Verbindung erstellt wird, muss der sec-websocket-key-Header auch im Clienthandshake vorhanden sein. Der Wert muss eine zufällig ausgewählte (im Idealfall ein kryptografischer Zufallswert) 16-Byte-Zahl sein, im Sicherheitssprachgebrauch als Nonce bezeichnet, die dann für diesen Headerwert base64-codiert wird.

Nachdem der Clienthandshake gesendet wurde, wartet der Client auf eine Antwort, um zu überprüfen, dass der Server tatsächlich zum Herstellen einer WebSocket-Verbindung bereit und dazu fähig ist. Wenn wir davon ausgehen, dass der Server nicht ablehnt, könnte er folgenden Serverhandshake als HTTP-Antwort senden:

HTTP/1.1 101 OK
upgrade: websocket\r\n
connection: upgrade\r\n
sec-websocket-accept: 7eQChgCtQMnVILefJAO6dK5JwPc=\r\n
\r\n

Dies ist wiederum eine absolut gültige HTTP-Antwort. Die Antwortzeile umfasst die HTTP-Version, gefolgt vom Statuscode. Aber anstelle des normalen Codes 200 zum Angeben des Erfolgs muss der Server mit dem Standardcode 101 antworten, um anzugeben, dass der Server die Aktualisierungsanforderung versteht und bereit ist, die Protokolle zu wechseln. Die englische Beschreibung des Statuscodes macht dabei überhaupt keinen Unterschied. Sie kann „OK“ bedeuten oder „Wechsel zu WebSocket“ oder sogar ein zufällig ausgewähltes Zitat von Mark Twain enthalten. Wichtig ist der Statuscode, und der Client muss sicherstellen, dass er 101 lautet. Der Server könnte beispielsweise die Anforderung ablehnen und den Client durch den Statuscode 401 zur Authentifizierung auffordern, bevor er einen WebSocket-Clienthandshake akzeptiert. Eine erfolgreiche Antwort muss allerdings die Aktualisierungs- und Verbindungsheader enthalten, um zu bestätigen, dass der Statuscode 101 sich speziell auf einen Wechsel zum WebSocket-Protokoll bezieht. Auch das dient dazu, Täuschungen zu verhindern.

Schließlich muss der Client zum Überprüfen des Handshakes sicherstellen, dass der sec-websocket-accept-Header in der Antwort enthalten und sein Wert korrekt ist. Der Server muss den base64-codierten Wert, der vom Client gesendet wird, nicht decodieren. Er nimmt einfach diese Zeichenfolge, verkettet die Zeichenfolgendarstellung einer bekannten GUID und erstellt mit dem SHA-1-Algorithmus einen Hash der Kombination, um einen 20-Byte-Wert hervorzubringen, der nun base64-codiert und als Wert für den sec-websocket-accept-Header verwendet wird. Der Client kann dann einfach überprüfen, dass der Server tatsächlich die erforderliche Aktion ausgeführt hat. Anschließend gibt es keinen Zweifel mehr, dass beide Parteien in eine WebSocket-Verbindung einwilligen.

Wenn alles gut funktioniert, wird an dieser Stelle eine gültige WebSocket-Verbindung hergestellt, und beide Parteien können mithilfe von WebSocket-Datenrahmen frei und gleichzeitig in beide Richtungen kommunizieren. Betrachtet man das WebSocket-Protokoll, wird deutlich, dass es nach dem Unsicherheitsschock im Web entwickelt wurde. Anders als bei den meisten seiner Vorgänger war beim Entwurf des WebSocket-Protokolls der Sicherheitsaspekt präsent. Das Protokoll erfordert auch, dass der Client den Ursprungsheader enthält, wenn der Client tatsächlich ein Webbrowser ist. Dadurch können die Browser Schutz gegen ursprungsübergreifende Angriffe bereitstellen. Das macht natürlich nur im Kontext einer vertrauenswürdigen Hostingumgebung Sinn, wie der eines Browsers.

WebSocket-Datenübertragung

Beim WebSocket-Protokoll geht es darum, im Web zu einem Kommunikationsmodell mit relativ hoher Leistung und geringem Mehraufwand zurückzufinden, bereitgestellt durch IP und TCP. Es soll nicht noch komplexer werden und kein weiterer Mehraufwand entstehen. Aus diesem Grund wird der WebSocket-Mehraufwand auf ein Minimum reduziert, sobald der Handshake abgeschlossen ist. WebSocket stellt zusätzlich zu TCP einen Paketrahmenmechanismus bereit. Dieser erinnert an die IP-Paketierung, auf die TCP selbst aufgebaut ist und für die UDP so beliebt ist, aber ohne die Paketgrößenbeschränkungen, mit denen diese Protokolle belastet sind. Während TCP eine streambasierte Abstraktion zur Verfügung stellt, stellt WebSocket eine nachrichtenbasierte Abstraktion für die Anwendung bereit. Und während TCP-Datenströme über Segmente übertragen werden, werden WebSocket-Nachrichten als eine Sequenz von Frames übermittelt. Diese Frames werden über dieselbe TCP-Verbindung übertragen und setzen daher natürlich eine verlässliche und sequenzielle Übertragung voraus. Dieses Rahmenprotokoll ist ein wenig ausführlich, aber speziell so entworfen, dass es sehr klein ist und in vielen Fällen nur wenige zusätzliche Bytes Mehraufwand erforderlich sind. Wenn der Handshake zu Beginn abgeschlossen ist, können die Datenrahmen jederzeit sowohl vom Client als auch vom Server übertragen werden.

Jeder Frame enthält einen Opcode, der den Frametyp und die Größe der Nutzlast beschreibt. Diese Nutzlast stellt die eigentlichen Daten dar, die die Anwendung kommunizieren möchte, und alle im Voraus festgelegten Erweiterungsdaten. Interessanterweise lässt das Protokoll die Fragmentierung von Nachrichten zu. Wenn Sie sehr viel mit Netzwerken zu tun haben, erinnert Sie das vielleicht an die Leistungsauswirkungen der Fragmentierung auf IP-Ebene und den Aufwand, mit dem TCP die Fragmentierung vermeidet. Aber das WebSocket-Fragmentierungskonzept unterscheidet sich stark. Die Idee dabei ist, dass das WebSocket-Protokoll den Komfort von Netzwerkpaketen ohne die Größenbeschränkungen bereitstellen soll. Wenn der Sender die genaue Länge der gesendeten Nachricht nicht kennt, kann sie fragmentiert werden. Dabei gibt jeder Frame an, wie viele Daten er bereitstellt und ob es sich um das letzte Fragment handelt oder nicht. Darüber hinaus gibt der Frame nur an, ob er Binärdaten oder UTF-8-codierten Text enthält.

Auch Steuerungsframes werden definiert. Sie dienen hauptsächlich dazu, eine Verbindung zu schließen, können aber auch als Takt für einen Ping an den anderen Endpunkt verwendet werden, um sicherzustellen, dass dieser weiterhin antwortet oder zur Aufrechterhaltung der TCP-Verbindung beizutragen. Schließlich möchte ich noch erwähnen, dass Sie möglicherweise feststellen, dass die Datenrahmen codierte Daten zu enthalten scheinen, wenn Sie einen von einem Client gesendeten WebSocket-Frame mit einem Netzwerkprotokoll-Analyseprogramm wie Wireshark betrachten. Das WebSocket-Protokoll erfordert, dass alle Datenrahmen, die vom Client an den Server gesendet werden, maskiert sind. Die Maskierung enthält einen einfachen XOR-Algorithmus für die Datenbytes mithilfe eines Maskierungsschlüssels. Der Maskierungsschlüssel ist im Frame enthalten. Dies soll also kein unsinniges Sicherheitsfeature sein, obwohl es mit der Sicherheit zu tun hat. Wie bereits erwähnt, haben die Entwickler des WebSocket-Protokolls viel Mühe darauf verwandt, unterschiedliche Sicherheitsszenarios durchzugehen und die verschiedenen Methoden zu berücksichtigen, mit denen das Protokoll möglicherweise angegriffen werden kann. Zu einer der analysierten Angriffsmöglichkeiten gehörte ein indirektes Angreifen des WebSocket-Protokolls durch das Angreifen anderer Teile der Infrastruktur des Internets, in diesem Falle Proxyserver. Arglose Proxyserver, denen die Ähnlichkeit zwischen einem WebSocket-Handshake und einer GET-Anforderung vielleicht nicht bewusst ist, könnten getäuscht werden und Daten für eine gefälschte, von einem Angreifer initiierte GET-Anforderung zwischenspeichern. Dadurch würden sie den Zwischenspeicher für einige Benutzer vergiften. Die Maskierung jedes Frames mit einem neuen Schlüssel reduziert diese spezielle Bedrohung, da sie sicherstellt, dass die Frames nicht vorhersagbar sind und somit nicht beim Senden verfälscht werden können. Das ist nicht alles zu diesem Angriff, und zweifellos werden Experten mit der Zeit weitere mögliche Sicherheitslücken entdecken. Dennoch ist es beeindruckend, wie engagiert die Entwickler versucht haben, viele Angriffsformen vorherzusehen.

Windows 8 und das WebSocket-Protokoll

Ebenso nützlich wie eine gute Kenntnis des WebSocket-Protokolls ist eine Plattform mit solch einer umfassenden Unterstützung, und Windows 8 erfüllt zweifellos die Erwartungen. Lassen Sie uns einige Methoden ansehen, mit denen Sie das WebSocket-Protokoll verwenden können, ohne dass Sie das Protokoll an sich implementieren müssen.

Windows 8 stellt Microsoft .NET Framework bereit, unterstützt Clients über Windows-Runtime für systemeigenen und verwalteten Code, und Sie können mithilfe der Windows HTTP (WinHTTP)-Dienste-API WebSocket-Clients in C++ erstellen. Schließlich bietet IIS 8 ein systemeigenes WebSocket-Modul, und natürlich stellt Internet Explorer systemeigene Unterstützung für das WebSocket-Protokoll bereit. Das sind einige Umgebungen, aber vielleicht noch überraschender ist, dass Windows 8 nur eine einzelne WebSocket-Implementierung umfasst, die von allen Umgebungen gemeinsam verwendet wird. Die WebSocket Protocol Component-API implementiert alle Protokollregeln für den Handshake und die Frames, ohne je tatsächlich irgendeine Art von Netzwerkverbindung zu erstellen. Die verschiedenen Plattformen und Laufzeiten können dann diese gemeinsame Implementierung nutzen und in den Netzwerkstapel ihrer Wahl einbinden.

.NET-Clients und -Server

.NET Framework bietet Erweiterungen für ASP.NET und stellt „HttpListener“ für die Serverunterstützung des WebSocket-Protokolls bereit. „HttpListener“ basiert selbst auf der systemeigenen HTTP-Server-API, die von IIS verwendet wird. Im Fall von ASP.NET können Sie einfach einen HTTP-Handler schreiben, der die neue HttpContext.AcceptWebSocketRequest-Methode aufruft, um eine WebSocket-Anforderung auf einem bestimmten Endpunkt anzunehmen. Mithilfe der HttpContext.IsWebSocketRequest-Eigenschaft können Sie überprüfen, dass die Anforderung wirklich ein WebSocket-Clienthandshake ist. Außerhalb von ASP.NET können Sie einfach die HttpListener-Klasse verwenden, um einen WebSocket-Server zu hosten. Die Implementierung wird auch zumeist von den beiden gemeinsam verwendet. Abbildung 1 zeigt ein einfaches Beispiel eines solchen Servers.

Abbildung 1: WebSocket-Server, der „HttpListener“ verwendet

static async Task Run()
{
  HttpListener s = new HttpListener();
  s.Prefixes.Add("http://localhost:8000/ws/");
  s.Start();
  var hc = await s.GetContextAsync();
  if (!hc.Request.IsWebSocketRequest)
  {
    hc.Response.StatusCode = 400;
    hc.Response.Close();
    return;
  }
  var wsc = await hc.AcceptWebSocketAsync(null);
  var ws = wsc.WebSocket;
  for (int i = 0; i != 10; ++i)
  {
    await Task.Delay(2000);
    var time = DateTime.Now.ToLongTimeString();
    var buffer = Encoding.UTF8.GetBytes(time);
    var segment = new ArraySegment<byte>(buffer);
    await ws.SendAsync(segment, WebSocketMessageType.Text,
      true, CancellationToken.None);
  }
  await ws.CloseAsync(WebSocketCloseStatus.NormalClosure,
    "Done", CancellationToken.None);
}

Ich verwende hier eine async-Methode in C#, damit der Code sequenziell und kohärent bleibt, aber tatsächlich ist alles asynchron. Ich beginne mit dem Registrieren des Endpunkts und warte auf eine eingehende Anforderung. Ich prüfe dann, ob die Anforderung tatsächlich ein WebSocket-Handshake ist, und gebe andernfalls den ablehnenden Statuscode 400 zurück. Ich rufe anschließend „AcceptWebSocketAsync“ auf, um den Clienthandshake anzunehmen, und warte, bis der Handshake abgeschlossen ist. Nun kann ich mithilfe des WebSocket-Objekts frei kommunizieren. In diesem Beispiel sendet der Server nach einer kurzen Verzögerung 10 UTF-8-Frames. Jeder Frame wird mit der SendAsync-Methode asynchron gesendet. Diese Methode ist sehr leistungsstark und kann UTF-8-Frames oder binäre Frames als Ganzes oder in Fragmenten senden. Der dritte Parameter, in diesem Fall „true“, gibt an, ob dieser Aufruf von SendAsync das Ende der Nachricht darstellt. Sie können daher diese Methode wiederholt verwenden, um lange Nachrichten zu senden, die für Sie fragmentiert werden. Schließlich verwende ich die CloseAsync-Methode, um die WebSocket-Verbindung ordnungsgemäß zu schließen, durch Senden eines Steuerungsframes zum Schließen und Warten auf die Bestätigung des Clients durch einen eigenen Frame zum Schließen.

Auf der Clientseite verwendet die neue ClientWebSocket-Klasse intern ein HttpWebRequest-Objekt, damit eine Verbindung mit einem WebSocket-Server hergestellt werden kann. Abbildung 2 zeigt ein einfaches Beispiel für einen Client, der für eine Verbindung mit dem Server in Abbildung 1 verwendet werden kann.

Abbildung 2: WebSocket-Client, der „ClientWebSocket“ verwendet

static async Task Client()
{
  ClientWebSocket ws = new ClientWebSocket();
  var uri = new Uri("ws://localhost:8000/ws/");
  await ws.ConnectAsync(uri, CancellationToken.None);
  var buffer = new byte[1024];
  while (true)
  {
    var segment = new ArraySegment<byte>(buffer);
    var result =
      await ws.ReceiveAsync(segment, CancellationToken.None);
    if (result.MessageType == WebSocketMessageType.Close)
    {
      await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "OK",
        CancellationToken.None);
      return;
    }
    if (result.MessageType == WebSocketMessageType.Binary)
    {
      await ws.CloseAsync(WebSocketCloseStatus.InvalidMessageType,
        "I don't do binary", CancellationToken.None);
      return;
    }
    int count = result.Count;
    while (!result.EndOfMessage)
    {
      if (count >= buffer.Length)
      {
        await ws.CloseAsync(WebSocketCloseStatus.InvalidPayloadData,
          "That's too long", CancellationToken.None);
        return;
      }
      segment =
        new ArraySegment<byte>(buffer, count, buffer.Length - count);
      result = await ws.ReceiveAsync(segment, CancellationToken.None);
      count += result.Count;
    }
    var message = Encoding.UTF8.GetString(buffer, 0, count);
    Console.WriteLine("> " + message);
  }
}

Hier stelle ich mit der ConnectAsync-Methode eine Verbindung her und führe den WebSocket-Handshake aus. Die URL verwendet das neue „ws“-URI-Schema, um dies als einen WebSocket-Endpunkt zu identifizieren. Wie bei HTTP ist der ws-Standardport ebenfalls 80. Das „wss“-Schema wird auch definiert, um eine sichere TLS-Verbindung darzustellen, und verwendet den entsprechenden Port 443. Der Client ruft dann „ReceiveAsync“ in einer Schleife auf, um so viele Frames zu empfangen, wie der Server sendet. Nach seinem Empfang wird der Frame zuerst daraufhin geprüft, ob er einen Steuerungsframe zum Schließen darstellt. In diesem Fall antwortet der Client, indem er seinen eigenen Schließframe sendet, woraufhin der Server die Verbindung sofort schließen kann. Der Client prüft als Nächstes, ob der Frame binäre Daten enthält. Wenn das der Fall ist, schließt er die Verbindung mit einem Fehler, der angibt, dass dieser Frametyp nicht unterstützt wird. Schließlich können die Framedaten gelesen werden. Um fragmentierte Nachrichten zu berücksichtigen, wartet eine while-Schleife, bis das letzte Fragment empfangen wird. Die neue ArraySegment-Struktur wird verwendet, um das Pufferoffset zu verwalten, damit die Fragmente korrekt wieder zusammengefügt werden.

WinRT-Client

Die Windows-Runtime-Unterstützung für das WebSocket-Protokoll ist etwas mehr eingeschränkt. Es werden nur Clients unterstützt, und fragmentierte UTF-8-Nachrichten müssen vollständig gepuffert sein, bevor sie gelesen werden können. Mit dieser API können nur binäre Nachrichten gestreamt werden. Abbildung 3 zeigt ein einfaches Beispiel für einen Client, der ebenfalls für eine Verbindung mit dem Server in Abbildung 1 verwendet werden kann.

Abbildung 3: WebSocket-Client, der Windows-Runtime verwendet

static async Task Client()
{
  MessageWebSocket ws = new MessageWebSocket();
  ws.Control.MessageType = SocketMessageType.Utf8;
  ws.MessageReceived += (sender, args) =>
  {
    var reader = args.GetDataReader();
    var message = reader.ReadString(reader.UnconsumedBufferLength);
    Debug.WriteLine(message);
  };
  ws.Closed += (sender, args) =>
  {
    ws.Dispose();
  };
  var uri = new Uri("ws://localhost:8000/ws/");
  await ws.ConnectAsync(uri);
}

Dieses Beispiel ist zwar in C# geschrieben, basiert aber zum größten Teil auf Ereignishandlern, und die async-Methode in C# hat nur den geringen Nutzen, dass sich das MessageWebSocket-Objekt asynchron verbinden kann. Der Code ist vielleicht etwas eigenartig, dafür aber recht einfach. Sobald die vollständige (möglicherweise fragmentierte) Nachricht empfangen wurde und zum Lesen bereit ist, wird der MessageReceived-Ereignishandler aufgerufen. Obwohl die gesamte Nachricht empfangen wurde und nur eine UTF-8-Zeichenfolge sein kann, ist sie in einem Datenstrom gespeichert, und zum Lesen des Inhalts und Zurückgeben einer Zeichenfolge muss ein DataReader-Objekt verwendet werden. Schließlich werden Sie durch einen Closed-Ereignishandler informiert, dass der Server einen Steuerungsframe zum Schließen gesendet hat. Aber wie bei der ClientWebSocket-Klasse in .NET sind Sie weiter dafür verantwortlich, einen Steuerungsframe zum Schließen zurück an den Server zu senden. Die MessageWebSocket-Klasse sendet diesen Frame allerdings genau bevor das Objekt selbst entfernt wird. Damit dies in C# umgehend ausgeführt wird, muss ich die Dispose-Methode aufrufen.

Prototyp eines JavaScript-Clients

Es gibt kaum einen Zweifel, dass das WebSocket-Protokoll in der JavaScript-Umgebung die größte Auswirkung haben wird, und die API ist beeindruckend einfach. Für die Verbindung zum Server in Abbildung 1 ist nur der folgende Code erforderlich:

var ws = new WebSocket("ws://localhost:8000/ws/");
ws.onmessage = function (args)
{
  var time = args.data;
  ...
};

Im Unterschied zu anderen APIs unter Windows übernimmt der Browser automatisch das Schließen der WebSocket-Verbindung, wenn er einen Steuerungsframe zum Schließen empfängt. Sie können natürlich eine Verbindung ausdrücklich schließen oder das onclose-Ereignis verarbeiten, aber zum Beenden des Handshakes zum Schließen müssen Sie nichts weiter tun.

WinHTTP-Client für C++

Die WebSocket-Client-API in WinRT kann natürlich auch in systemeigenem C++ verwendet werden, aber wenn Sie ein wenig mehr Steuerung möchten, ist WinHTTP genau das Richtige für Sie. Abbildung 4 zeigt ein einfaches Beispiel dafür, wie mit WinHTTP eine Verbindung mit dem Server in Abbildung 1 hergestellt wird. Der Kürze halber wird die WinHTTP-API im synchronen Modus verwendet, aber der Code würde asynchron genauso gut funktionieren.

Abbildung 4: WebSocket-Client, der WinHTTP verwendet

auto s = WinHttpOpen( ... );
auto c = WinHttpConnect(s, L"localhost", 8000, 0);
auto r = WinHttpOpenRequest(c, nullptr, L"/ws/", ... );
WinHttpSetOption(r, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullptr, 0);
WinHttpSendRequest(r, ... );
VERIFY(WinHttpReceiveResponse(r, nullptr));
DWORD status;
DWORD size = sizeof(DWORD);
WinHttpQueryHeaders(r,
  WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
  WINHTTP_HEADER_NAME_BY_INDEX,
  &status,
  &size,
  WINHTTP_NO_HEADER_INDEX);
ASSERT(HTTP_STATUS_SWITCH_PROTOCOLS == status);
auto ws = WinHttpWebSocketCompleteUpgrade(r, 0);
char buffer[1024];
DWORD count;
WINHTTP_WEB_SOCKET_BUFFER_TYPE type;
while (NO_ERROR ==
  WinHttpWebSocketReceive(ws, buffer, sizeof(buffer), &count, &type))
{
  if (WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE == type)
  {
    WinHttpWebSocketClose(
      ws, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0);
    break;
  }
  if (WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE == type ||
    WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE == type)
  {
    WinHttpWebSocketClose(
      ws, WINHTTP_WEB_SOCKET_INVALID_DATA_TYPE_CLOSE_STATUS, nullptr, 0);
    break;
  }
  std::string message(buffer, count);
  while (WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE == type)
  {
    WinHttpWebSocketReceive(ws, buffer, sizeof(buffer), &count, &type);
    message.append(buffer, count);
  }
  printf("> %s\n", message.c_str());
}

Wie bei allen WinHTTP-Clients müssen Sie eine WinHTTP-Sitzung, -Verbindung und ein WinHTTP-Anforderungsobjekt erstellen. Dabei gibt es nichts Neues, daher habe ich einige Details ausgelassen. Bevor Sie die Anforderung tatsächlich senden, müssen Sie die neue WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET-Option in der Anforderung festlegen, um WinHTTP anzuweisen, einen WebSocket-Handshake auszuführen. Die Anforderung kann dann mit der WinHttpSendRequest-Funktion gesendet werden. Anschließend wird die normale WinHttpReceiveResponse-Funktion zum Warten auf die Antwort verwendet, die in diesem Fall das Ergebnis des WebSocket-Handshakes enthält. Um wie immer das Ergebnis einer Anforderung zu bestimmen, wird die WinHttpQueryHeaders-Funktion aufgerufen, um speziell den vom Server zurückgegebenen Statuscode zu lesen. An dieser Stelle wurde die WebSocket-Verbindung hergestellt, und Sie können sie direkt verwenden. Die WinHTTP-API verarbeitet selbstverständlich die Frames, und diese Funktionalität wird über ein neues WinHTTP-WebSocket-Objekt zur Verfügung gestellt, dass durch den Aufruf der WinHttpWebSocketCompleteUpgrade-Funktion für das Anforderungsobjekt abgerufen wird.

Der Empfang der Nachrichten vom Server erfolgt – zumindest dem Konzept nach – fast genauso wie im Beispiel in Abbildung 2. Die WinHttpWebSocketReceive-Funktion wartet, um den nächsten Datenrahmen zu empfangen. Sie können damit auch Fragmente von jeder Art WebSocket-Nachricht lesen. Das Beispiel in Abbildung 4 zeigt, wie dies in einer Schleife ausgeführt werden kann. Wenn ein Steuerungsframe zum Schließen empfangen wird, erfolgt das Senden eines entsprechenden Schließframes an den Server mithilfe der WinHttpWebSocketClose-Funktion. Die Verbindung wird ähnlich geschlossen, wenn ein binärer Datenrahmen empfangen wird. Dadurch wird nur die WebSocket-Verbindung geschlossen. Sie müssen trotzdem noch „WinHttpCloseHandle“ aufrufen, um das WinHTTP-WebSocket-Objekt freizugeben, wie es für alle WinHTTP-Objekte in Ihrem Besitz erforderlich ist. Dazu können Sie eine Wrapperklasse für Handles verwenden, wie ich sie in meiner Kolumne „C++ und die Windows-API“ (msdn.microsoft.com/magazine/hh288076) vom Juli 2011 beschrieben habe.

Das WebSocket-Protokoll ist eine wichtige Innovation in der Welt der Webanwendungen, und trotz seiner relativen Einfachheit eine willkommene Ergänzung der umfassenderen TCP/IP-Protokollsuite. Ich habe wenig Zweifel daran, dass das WebSocket-Protokoll bald fast so universell wie HTTP selbst sein wird und Anwendungen und vernetzte Systeme aller Art dadurch einfacher und effizienter kommunizieren können. Windows 8 bietet einen umfassenden Satz APIs, um sowohl WebSocket-Clients als auch -Server zu erstellen.

Kenny Kerr ist Softwarespezialist mit einer Vorliebe für die systemeigene Windows-Entwicklung. Sie erreichen ihn unter kennykerr.ca.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Piotr Kulaga und Henri-Charles Machalani.