WebSockets na iOS – komunikacja w czasie rzeczywistym, która nie spowalnia aplikacji
Kiedy chcesz pobrać dane do aplikacji, zwykle pewnie wykorzystujesz interfejs API RESTful. Wystarczy zapytać serwer o paczkę danych i gotowe. To dobra metoda, jeśli aplikacja nie potrzebuje stałego dostępu do nowych informacji. A co jeśli treści muszą się odświeżać w czasie rzeczywistym?
Wtedy opóźnienie może np. uniemożliwiać dokonanie zakupu albo postawienie zakładu sportowego. Na szczęście z pomocą przychodzą WebSockets na iOS. Sprawdź, jak je zaimplementować i dlaczego warto to zrobić.
Interfejs RESTful APIs – działanie i ograniczenia
Transmisja danych między serwerem a klientem to ważna kwestia w przypadku aplikacji mobilnych. Zazwyczaj doskonale sprawdza się komunikacja z wykorzystaniem interfejsu RESTful API. To styl architektoniczny interfejsu aplikacji (API), który bazuje na żądaniach HTTP, żeby uzyskać dostęp do danych i móc z nich korzystać.
W przypadku protokołu HTTP połączenie przebiega w trybie żądanie-odpowiedź według poniższego wzorca:
- Poproś o plik JSON z serwera.
- Odbierz i wykorzystaj dane.
To prawdopodobnie najprostszy i najbardziej znany sposób pozyskiwania aktualizacji z serwera. Sprawdza się wtedy, gdy częsta aktualizacja danych nie jest potrzebna. Dotyczy to np. informacji o produkcie w sklepie internetowym, pobierania danych użytkownika czy dokonywania zakupu w aplikacji.
A co w przypadku, kiedy treść trzeba zaktualizować natychmiast i konieczna jest komunikacja w czasie rzeczywistym? Dzieje się tak np. podczas obstawiania wyników wydarzeń sportowych, kupna akcji na giełdzie czy kryptowalut. Wtedy opóźnienia nie wchodzą w grę. Nikt nie chciałby np. obstawiać wyniku gema w tenisie, gdyby wiedział, że nie zna aktualnego wyniku, bo to z góry skazuje go na przegraną. Jak sobie poradzić z tym problemem?
RESTful APIs jako sposób na komunikację w czasie rzeczywistym
Pierwsze rozwiązanie, które przychodzi na myśl, to częstsze odpytywanie serwera. Nowe żądanie co minutę powinno wystarczyć – przecież w ciągu minuty nikt nie strzeli w meczu piłkarskim dwóch bramek. Ale co w przypadku tenisa ziemnego? W ciągu 60 sekund może się wiele wydarzyć, a użytkownik nie dowie się o tym w porę. W ten sposób utraci możliwość obstawiania wielu wyników.
W takim razie może żądanie co 10 sekund będzie lepsze? Bywa, że i to nie wystarcza. W tak krótkim czasie ktoś może np. wystawić ofertę sprzedaży kryptowaluty, a wtedy inna osoba ubiegnie użytkownika w zakupie. A żądanie co sekundę? To rozwiązanie wydaje się nie mieć słabych punktów, ale tylko pozornie. Tak częste odpytywanie serwera przez wielu użytkowników spowoduje jego obciążenie. To z kolei bezpośrednio przełoży się na dłuższy czas odpowiedzi, a także na odbiór samej aplikacji.
Zamiast korzystać z najświeższych danych, użytkownicy będą się zmagać z długim czasem ładowania lub w ogóle nie otrzymają treści. Sytuacja ta może również powodować szybkie zużycie baterii.
A co, jeśli to serwer mógłby powiedzieć coś Twojej aplikacji? Na platformie iOS jest to możliwe dzięki WebSockets.
Czym są WebSockets i jak działają?
Gniazda sieci Web, czyli WebSockets to protokoły komunikacyjne – otwarte połączenia, dzięki którym możliwa jest dwukierunkowa komunikacja między serwerem a klientem (duplex communication) przy użyciu pojedynczego gniazda.
WebSockets umożliwiają przesyłanie wiadomości. Najczęściej są one krótkie, żeby można było je jak najszybciej odbierać.
Wysyłanie małych paczek danych może wydawać się sprzeczne z tym, o czym była mowa wcześniej – przecież częste odpytywanie serwera powoduje jego obciążenie. Tyle, że korzystając z gniazd internetowych, nie wysyłasz żądań. Wysyłasz wiadomości.
Stale otwarte połączenie WebSocket powoduje, że nie obciążasz serwera w dodatkowy sposób. Pozwala przy tym na częste wysyłanie komunikatów z jak najmniejszym opóźnieniem. Implementacja gniazd sieciowych umożliwia wysyłanie treści tak szybko, jak to możliwe (przy czym ich waga powinna być jak najmniejsza) oraz odbieranie wielu wiadomości w krótkich odstępach czasu.
Korzystanie z gniazd opiera się głównie na 3 czynnościach:
- Podłączenie do gniazda sieciowego.
- Wysyłanie wiadomości
- Odbieranie wiadomości.
Ta wiedza pozwoli Ci ocenić, czy gniazda sieciowe sprawdzą się w Twoim projekcie.
Implementacja WebSockets krok po kroku
Pobranie biblioteki Starscream
Konfiguracja WebSockets na iOS nie jest prostym zadaniem, ponieważ nie ma wbudowanego interfejsu API, który byłby za to odpowiedzialny. Tutaj wsparciem okazuje się Starscream – biblioteka w języku Swift zgodna z WebSocket (RFC 6455).
Istnieją różne sposoby na dodanie Starscream do projektu. Dwa najpopularniejsze systemy do zarządzania zależnościami to Carthage i CocoaPods. W tym tutorialu korzystam z tego drugiego rozwiązania.
Co należy zrobić najpierw? Zainstaluj bibliotekę i zaimportuj ją w dowolnym pliku Swift. Warto też utworzyć SocketManagera, który będzie odpowiedzialny za obsługę gniazd.
import Starscream
final class SocketManager {}
Połączenie z WebSocket Server
Kolejnym krokiem jest utworzenie połączenia oraz ustanowienie delegata. Do tego służy poniższy kod:
func setupSocket() {
socket = WebSocket(url: URL(string: APIConstants.socketBaseURL)!)
socket.delegate = self
socket.connect()
}
Należy tutaj zwrócić uwagę na format adresu URL: static let socketBaseURL = “wss://…”. Jest to protokół WS, nie HTTPS czy HTTP.
Metody delegowania
Po nawiązaniu połączenia musisz zaimplementować kilka metod delegowania:
func websocketDidConnect(socket: WebSocket) {}
– wywoływana, gdy klient połączył się z serwerem.func websocketDidDisconnect(socket: WebSocket, error: NSError?) {}
– wywoływana w chwili, gdy klient zostaje odłączony od serwera.func websocketDidReceiveMessage(socket: WebSocket, text: String) {}
– wywoływana wtedy, gdy klient otrzymuje ramkę tekstową z połączenia.func websocketDidReceiveData(socket: WebSocket, data: Data) {}
– wywoływana wtedy, kiedy klient otrzymuje ramkę binarną z połączenia.
Wysyłanie wiadomości do serwera WebSocket
Teraz możesz nawiązać połączenie z serwerem. W zależności od jego implementacji, gdy tylko to nastąpi, konieczne może być przesłanie dodatkowych wiadomości. Służy do tego prosta metoda:
private func sendMessage(_ message: String) {
socket.write(string: message)
}
Uzgadnianie połączenia – handshake Message
Jednym z wymogów, z jakimi możesz się spotkać, jest konieczność wysłania tzw. handshake Message. Jest to most łączący HTTP z WebSockets. Pozwala on uzgodnić szczegóły połączenia. Serwer musi mieć pewność, że rozumie, o co prosi klient. W przeciwnym razie mogą wystąpić problemy z bezpieczeństwem.
func websocketDidConnect(socket: WebSocketClient) {
switch type {
case .trade:
tradesocketDidConnect()
}
}
private func tradesocketDidConnect() {
guard let handshakeMessage = Constants.handshakeDictionary.stringFromJSON() else { return }
sendMessage(handshakeMessage)
sendMessage(setupInitialState(for: .initAggregatedOffers))
sendMessage(handshakeMessage)
sendMessage(setupSubscribe(for: .aggregatedOffer))
}
Po ustaleniu szczegółów połączenia pobieranie danych z serwera stanie się możliwe.
Otrzymywanie treści z serwera
Kolejnym etapem jest implementacja metody websocketDidReceiveMessage, na przykład za pomocą takiego kodu:
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
switch type {
case .trade:
let model = tradesocketDidReceiveMessage(text: text)
viewDelegate?.tradesocketDidReceiveMessage(model: model)
}
}
Otrzymujesz tutaj zwykły tekst, który możesz przekonwertować na plik JSON.
private func tradesocketDidReceiveMessage(text: String) -> AggregatedOfferTouple? {
if let handshakeMessage = Constants.handshakeDictionary.stringFromJSON(), text == Constants.handshakeCid {
sendMessage(handshakeMessage)
return nil
} else if text.contains(Constants.initAggregatedOffersRid), let data = text.data(using: .utf8) {
return setupInitAggregatedOffers(with: data)
} else if text.contains(Constants.aggregatedOfferChannel), let data = text.data(using: .utf8) {
return setupAggregatedOffer(with: data)
} else {
return nil
}
}
W zależności od tego, jaki tekst otrzymasz, powinieneś wykonać różne działania. Na przykład, może się okazać, że po raz kolejny musisz uzgodnić z serwerem szczegóły połączenia. Aby uzyskać konkretne dane, najpierw powinieneś przekonwertować String do Data. Następnie, dzięki wykorzystaniu JSONDecoder, otrzymasz odpowiedni model danych:
guard let model = try? JSONDecoder().decode(Order.self, from: data) else { return nil }
Rozłączenie z serwerem
Warto też wiedzieć, jak przerwać połączenie, kiedy nie jest ono Ci już potrzebne. Może to mieć miejsce np. w ramach wywołania metody viewWillDisappear(_ animated: Bool)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
viewModel.disconnectSocket()
}
Rozłączanie odbywa się przy pomocy prostej metody:
func disconnectSocket() {
socket.disconnect()
}
Co jeśli serwer WebSockets nie działa?
W takim wypadku musisz sprawdzić jedną rzecz. Idź do info.plist
i zmień arbitrary loads na YES – info.plist -> App Transport Security Settings > Allow Arbitrary Loads
. To może rozwiązać problem, ponieważ Apple automatycznie blokuje wszystkie adresy URL bez HTTPS.
Przykładowy szablon dla SocketManager
Jeżeli rozwijasz komercyjną aplikację, która wymaga użycia WebSockets, wypróbuj to rozwiązanie i skorzystaj z kodu poniżej:
import Starscream
typealias ModelTouple = (buyOrders: [Model], sellOrders: [Model])
enum SocketViewType {
case type
}
protocol SocketManagerType {
var viewDelegate: SocketManagerDelegate? { get set }
func setupSocket()
func disconnectSocket()
}
protocol SocketManagerDelegate: class {
func socketDidReceiveMessage(model: ModelTouple?)
}
final class SocketManager {
weak var viewDelegate: SocketManagerDelegate?
private let type: SocketViewType
private var socket: WebSocket!
init(type: SocketViewType) {
self.type = type
}
private func sendMessage(_ message: String) {
socket.write(string: message)
}
private func sendData(_ data: Data) {
socket.write(data: data)
}
private func socketDidConnect() {}
private func socketDidReceiveMessage(text: String) -> ModelTouple? {}
}
extension SocketManager: SocketManagerType {
func setupSocket() {
socket = WebSocket(url: URL(string: APIConstants.socketBaseURL)!)
socket.delegate = self
socket.connect()
}
func disconnectSocket() {
socket.disconnect()
}
}
extension SocketManager: WebSocketDelegate {
func websocketDidConnect(socket: WebSocketClient) {
switch type {
case .type:
socketDidConnect()
}
}
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {}
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
switch type {
case .type:
let model = socketDidReceiveMessage(text: text)
viewDelegate?.socketDidReceiveMessage(model: model)
}
}
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {}
}
Komunikacja w czasie rzeczywistym – kiedy warto wykorzystać WebSockets na iOS?
Sprawna komunikacja klient-serwer odgrywa kluczową rolę w aplikacjach mobilnych. Użytkownicy nie lubią czekać aż treści się załadują. Im dłużej to trwa, tym większe szanse na to, że wyjdą z aplikacji i już do niej nie wrócą.
W wielu przypadkach wzorzec żądanie-odpowiedź (send and receive) wystarczy, żeby zapewnić odbiorcom jak najlepsze doświadczenia. WebSockets sprawdzą się jednak znacznie lepiej w scenariuszach z szybko lub często zmieniającymi się danymi.
Wyświetlanie wyników wydarzeń sportowych, obstawianie zakładów w czasie rzeczywistym, gra na giełdzie czy zakup kryptowalut to dobre przykłady użycia dla WebSockets.