ChatViewController.swift Screenshot

Swift: Websockets in iOS über Starscream

08. Aug. 2017 // Moritz Kanzler //

In den Kapiteln Node.js: Eigener Websocket Server und Node.js: Skalierbarer Websocketserver mit Redis Pub/Sub habe ich beschrieben, wie ich die serverseitige Umsetzung eines Chatservers auf Websocketbasis implementiert habe. Der Chatserver bildet die Grundlage für die Implementierung auf Clientseite in diesem Artikel.


Starscream als Websocket Framework

Nachdem die Umsetzung des serverseitige Umsetzung des Websocket Chats fertiggestellt ist, muss die neue Architektur auch in der iOS App, welche ich in meinem Projekt umsetzen muss, eingebunden werden.
Zunächst ist dafür ein iOS Framework nötig, welches das Websocket Protokoll unterstützt. Natürlich kann hier auch eine eigene Implementation geschrieben werden, was aber in größeren Projekten keinen Sinn macht, da für eine vernünfitge Implementierung viel Zeit nötig ist und zusätzlich die Gefahr besteht, dass Fehler, die in anderen Projekten schon behoben worden sind, erneut gemacht werden können.

Auswahl von Starscream als Framework

Nach einiger Recherche habe ich mich für das Swift Framework Starscream entschieden. Es ist in Swift implementiert, bietet eine gute Dokumentationsgrundlage, wird regelmäßig gepflegt und hat in dem relativ jungen Bereich der Websocket Frameworks eine verhältnismäßige gute Reputation auf GitHub. Zusätzlich dazu bietet es über die Einbindung mit Delegate-Methoden eine ähnliche Funktionsstruktur wie die Eventbasierte Lösung in Node.js.
Das wichtigste Kriterium für mich war aber, dass Starscream eine einfache Möglichkeit bietet, die Header in den Websocket Paketen zu manipulieren oder zu erweitern. Da der Websocket Server die Authentifizierung und Legitimation von geschickten Nachrichten über den Authorization Header von HTTP macht, ist es für mich entscheidend gewesen, diesen mit den angemeldeten Nutzerinformationen aus der App zu bestücken.

Nutzung von Starscream in Swift

Starscream arbeitet grundsätzlich ineinem Hintergrundthread und liefert Events über einen Delegate-Handler. Bei der Nutzung von Starscream muss grundsätzlich zwischen Verbindungsaufbau, Nachrichtenempfang, Nachrichtensendung und Zusatzfunktionen unterschieden werden. Im Folgenden wir die Benutzung des Frameworks kurz angerissen und als Grundlage für die Code-Listings im nächsten Kapitel genutzt.

Verbindungsaufbau

Zunächst wird ein WebSocket Objekt mit der entsprechenden URL zum Websocket Server initalisiert. Danach kann dieses über den connect() Befehl mit dem Server verbunden werden:

Nachrichtenempfang & Verbindungsstatus

Der WebSocketDelegate Handler bietet unter anderem Events für den Nachrichtenempfang und die Statusänderungen der Verbindung. Hierzu werden folgende Methoden genutzt:

Nachrichtensendung

Um Nachrichten zu verschicken wird die    genutzt. Entsprechend der Empfangshandler können mit dieser auch Daten gesendet werden.

Zusatzfunktionen

Neben den oben beschriebenen Basisfunktionen kann der Websocketclient in Starscream auch noch weiter angepasst werden. Hierzu gehören verschiedene Optionen die den SSL Umgang angehen oder die Einstellungen von Threadqueues. In diesem Projekt hervorzuheben, ist das Ändern/Hinzufügen von Headerinformationen im HTTP Header:

Konzept

Die grundlegende Idee hinter dem Einsatz von Websockets und dem entsprechenden Chatserver ist, eine einfache Möglichkeit der Liveinteraktion zwischen zwei Benutzern zu ermöglichen und gleichzeitig Techniken wie Long-Polling oder ständige Refreshs zu vermeiden. Dabei sollen verschickte Nachrichten dennoch persistent gespeichert werden um nicht verloren zu gehen. Unter Rücksichtname der Spezifikation des Chatservers ergeben sich zwei Szenarien für den Umgang mit dem Chat in der App.

  • Livechat
  • Datenbankbasierter Chat

Livechat

Zunächst wird jede gesendete Nachricht an den Chatserver übermittelt. Dieser schickt ausnahmslos jede Nachricht auch weiter an den REST-Service der App, welcher die Nachricht dauerhaft in der Datenbank speichert. Danach versucht der Chatserver die Nachricht an den Empfänger weiter zu vermitteln. Ist dieser ebenfalls am Chatserver angemeldet, und hat den Chat mit dem Sender offen, empfängt er die Nachricht unmittelbar über die Websockets. So kann eine Echzeitkommunikation sichergestellt werden.

Datenbankbasierter Chat

Wird eine Nachricht an einen Empfänger geschickt, welcher gerade nicht den Chat zu seinem gegenüber geöffnet hat, sorgt die automatische Datenbankspeicherung dafür, das die Nachricht zunächst nicht verloren geht.

Öffnet dieser Nutzer dann den Chat zu einem anderen Nutzer werden zunächst die letzten Nachrichten über den REST Service abgefragt und im Chatfenster dargestellt. So gehen keine Nachrichten verloren. Ab diesem Zeitpunkt ist der Nutzer gleichzeitig mit seinem Gegenüber über den (Websocket) Chatserver verbunden und kann so die Kommunikation in Echtzeit beginnen.

Implementierung

Dieses Kapitel beschreibt die Einbindung von Websocketsupport in iOS um einen Chat zu realisieren, welcher mit dem zuvor vorgestellten Websocket Chatserver interagieren kann. Zur Websocketeinbindung wird Starscream genutzt. Darüber hinaus werden aber auch die spezifischen Merkmale des Chatservers aufgegriffen und in der Implementierung berücksichtigt.

Installieren von Starscream

Wie jedes Framework kann auch Starscream auf verschiedene Methoden eingebunden werden. Ich nutze ich meinem Projekt CocoaPods. Zur Einbindung reicht hier die Podfile zu bearbeiten und Starscream mit in die Liste der benötigten Frameworks zu integrieren:

Danach werden die Abhängigkeiten neu geprüft und Starscream in das Projekt integiert:

Für die Einbindung von Starscream über andere Methoden findet man Informationen in der README.md auf Github.

Einbindung in die App

Um den Aufbau zum Websocket Chatserver strukturiert aufzubauen werden zunächst drei Dateien angelegt:

  • Message.swift: Modell, welches die Informationen einer einzelnen Nachricht darstellen.
  • ChatAPI.swift: Diese (statische) Klasse sorgt für den korrekten Socketaufbau mit entsprechender Nutzerkennung und stellt Methoden bereit, mit denen die Chatkonversation im JSON Format einfach geparst werden kann.
  • ChatViewController.swift: Dieser ViewController steuert die Darstellung eines Chats mit einer Person und die Interaktion des Nutzers mit dem Chatserver.

Message.swift: Das Nachrichten-Modell

Das Message Modell soll lediglich eine Klasse zum Speichern der Eigenschaften einer Nachricht bereitstellen. Hierbei werden die Datenbankfelder, die die Informationen aller Nachrichten enthalten, als Eigenschaften der Klasse Message dargestellt.

Der Chatserver liefert mit seinem JSON nicht alle Eigenschaften aus, da es sich um ein Livesystem handelt. Deshalb muss es auch möglich sein zu Kennzeichnen, dass bestimmte Nachrichten aus den Websockets gewonnen worden sind und nicht aus der Datenbank.

Die Klasse Message hat folgende Eigenschaften:

  • id (Int): Eindeutige Nachrichten ID aus Datenbank. Wird eine Nachricht vom Websocketserver empfangen, erhält diese die ID „-1“.
  • fromUser (String): Benutzername des Senders.
  • toUser (String): Benutzername des Empfängers.
  • message (String): Nachricht.
  • time (String): Absendezeitpunkt.
  • read (Bool): Nachricht auf dem Server als gelesen markiert?

Daraus resultiert folgender Code:

ChatAPI.swift: Allgemeine Hilfsfunktionen

Die ChatAPI soll als globale Hilfsklasse die einheitliche Verbindung zum Websocketserver garantieren und alle relevanten Daten hinterlegt haben. Dazu gehört die IP Adresse des Chatservers sowie die initial übermittelte Nutzerkennung zur Authentifizierung.

Darüber hinaus soll diese klasse (statische) Methoden bereitstellen um eine empfangene Nachricht vom Websocket Chatserver, welche in JSON kodiert ist, in das Message Modell umzuwandeln. Genau wie eine Methode welche eine abzusetzende Nachricht in das entsprechende Austauschformat des Chatservers verwandelt. Um mit JSON zu arbeiten wird außerdem das Framework SwiftyJSON benutzt.

Der entsprechende Code sieht wie folgt aus:

ChatViewController.swift: Der Chat

Im ChatViewController werden alle bisherigen Abläufe zu einem benutzbaren Chat zusammengezogen. Wichtig ist hierbei, dass der ChatViewController immer als Instanz eines Chats zu einer anderen Person gesehen werden muss. Die Auswahl der Chatpartner aus einer Liste wird an anderer Stelle vollzogen und hängt nicht zwingend mit dem Websocket Server zusammen.

Für die Realisierung des ChatViewControllers werden einige Annahmen getroffen um die Erklärungen hier auf die Websocketverbindung und den Chatbetrieb zu beschränken:

  • Die einzelnen Chatnachrichten werden in einem UICollectionView als einzelne CollectionCells dargestellt.
  • Die Eigenschaft messages stellt ein Array aus Message Objekten dar. Dieses Array dient als Datengrundlage für den CollectionView.
  • Bei der Initalisierung des ViewControllers (viewWillAppear()) werden die bisherigen Nachrichten (Daten vom REST Service) asnychron geladen und dargestellt.
  • Ein Objekt der Klasse Person (friend) enthält alle relevanten Daten über das Chatgegenüber.
  • Bei neu darzustellenden Nachrichten wird zunächst ein entsprechendes Message Objekt in das messages Array eingefügt und dann der CollectionView mit der Methode reloadData() aktualisiert. Die Methode scrollDown() sorgt dafür das immer die neuste Nachricht unten im CollectionView präsentiert wird.
  • Es gibt ein Textfeld für eigene Nachrichten: messageField und einen Button (messageSend) sowie eine entsprechende Action-Methode welche den Nachrichtenversand durchführt: sendMessage().

Vorbereitung

Zunächst wird der ViewController für die Nutzung vorbereitet. Dazu wird:

  • ein messages Array angelegt,
  • ein Websocket Objekt vorbereitet und verbunden (mithilfe der ChatAPI),
  • die delegate Methoden für Starscream und den CollectionView zugeweisen,
  • und der Socket beim Verlassen des Views geschlossen.

Datenbankbasierte Abfrage der Nachrichten im Status Quo

Um nun alle bisher ausgetauschten Nachrichten, bis zum Verbinden mit dem Websocketserver, zu erhalten wird die API asynchron angefragt und die Nachrichten in das message Array gelegt:

Senden von eigenen Nachrichten an den Chatpartner

Das Senden eigener Nachrichten an den Chatpartner geschieht ausschließlich über den Websocketserver. Dazu kann eine Nachricht in das entsprechende messageField eingetragen werden und über den Druck auf den messageSend-Button wird die Action sendMessage()  geworfen.

Diese baut die zu verschickende Nachricht mithilfe der ChatAPI in eine qualifizierte Nachricht für den Server um und sendet diese über socket.write() ab. Dazu wird die eigene Nachricht auch noch „optisch“ in das Nachrichtenarray messages eingetragen.

WebSocketDelegate einbinden um auf eingehende Nachrichten zu reagieren

Bisher werden alle Nachrichten bis dato geladen und neue Nachrichten für den Chat können einfach über den Chatserver abgesetzt werden. Nun müssen noch die vom Chatserver eingehenden Nachrichten behandelt werden. Starscream bietet dazu schon verschiedene, oben genannte, Delegate Methoden an.

Da jedem User ein Message Channel innerhalb des Chatservers zusteht, können Nachrichten von verschiedenen Nutzern während eines Chats mit einer Person über den Websocket empfangen werden. Für die Darstellung des einzelnen Chats ist es deshalb wichtig nur solche Nachrichten darzustellen, welche auch vom entsprechenden Chatpartner stammen.

Zusammenfassung

Abschließend hier noch einmal das gesamte Codelisting:

Fazit

Wie durch die Implementierung gezeigt, ist die clientseitige Verbindung zu einem Websocketservice mit Starscream schnell und komfortabel zu realisieren.

Im gezeigten Chatkontext sorgt die Websocketunterstützung für eine einfache Umsetzung eines Livechats. Wichtig ist aber zu bedenken, dass in den meisten Einsatzszenarien eine reine Websocketumsetzung allerdings nicht ausreicht. Grund dafür ist beispielsweise der Wunsch eine Nachrichtenhistorie zu erstellen oder wichtiger noch: Nachrichtensendungen auch übermitteln zu können, wenn das Gegenüber nicht gleichzeitig am Websocketservice angemeldet ist.

In der hier gezeigten Umsetzung ist das durch eine zusätzliche Nutzung eines REST Service mit Datenbank realisiert worden. Dazu wurde eine Hybridansatz gewählt, welcher zu Beginn einer Konversation die bisherigen Daten aus dem REST Service gewinnt und danach nur noch die Anbindung an den Websocketservice nutzt.
Zusätzlich speichert der angeschlossene Websocketservice übermittelte Nachrichten auch persistent in der Datenbank.

Natürlich sind auch andere Verbindungen zwischen persistenter Datenspeicherung und Livebetrieb möglich, wichtig ist nur das man sich darüber Gedanken macht, welche Art von Service betrieben werden soll und wie man den Umgang mit den Daten angehen möchte.