iOS WebSocket: URLSessionWebSocketTask & Starscream

Photo by Karl Anderson on Unsplash
Photo by Karl Anderson on Unsplash
WebSocket 近幾年越來越熱門,Apple 也在 WWDC 2019 上發表 iOS 13 內建 WebSocket 套件。本章將介紹如何使用 URLSessionWebSocketTask,以及如何使用第三方套件 Starsream,來實現 WebSocket 串接。

WebSocket 近幾年越來越熱門,Apple 也在 WWDC 2019 上發表 iOS 13 內建 WebSocket 套件。本章將介紹如何使用 URLSessionWebSocketTask,以及如何使用第三方套件 Starsream,來實現 WebSocket 串接。

完整程式碼可以在 下載。

URLSessionWebSocketTask

URLSessionWebSocketTask 是 iOS 13 之後才有,所以要先確定 Xcode 的 iOS Deployment Target 是設在 13 之後。

建立連線

宣告 URLRequest(url:),然後呼叫 URLSession.webSocketTask(with:)。它會回傳一個 URLSessionWebSocketTask,然後呼叫 Task 的 .resume() 來建立連線。是不是很簡單呢?

guard let url = URL(string: "wss://echo.websocket.org/") else {
    print("Error: can not create URL")
    return
}

let request = URLRequest(url: url)

let webSocketTask = URLSession.shared.webSocketTask(with: request)
webSocketTask.resume()

傳送訊息

把訊息字串放入 URLSessionWebSocketTask.Message 裡,然後呼叫 URLSessionWebSocketTask.send() 來傳送訊息。

let message = URLSessionWebSocketTask.Message.string("Hello WebSocket")
webSocketTask.send(message) { error in
    if let error = error {
	print(error)
    }
}

接收訊息

呼叫 URLSessionWebSocketTask.receive() 來接收訊息,並傳給它處理接受到的訊息的 Handler。收到的訊息也是 URLSessionWebSocketTask.Message 型態,要把訊息字串從裡面提出來。

要注意的是,當你呼叫一次 .receive(),它就只會接收一次訊息。所以在 Hanlder 處理完訊息後,離開之前,要記得再呼叫一次 .receive()

private func receive() {
    webSocketTask.receive { result in
        switch result {
        case .success(let message):
            switch message {
            case .string(let text):
                print("Received string: \(text)")
            case .data(let data):
                print("Received data: \(data)")
            @unknown default:
                fatalError()
            }

        case .failure(let error):
            print(error)
        }

        self.receive()
    }
}

斷開連線

呼叫 URLSessionWebSocketTask.cancel() 就可以斷開連線。

webSocketTask.cancel(with: .goingAway, reason: nil)

監聽連線狀態

如果你要監聽連線狀態的話,就要實作 URLSessionWebSocketDelegate。它包含了兩個監聽事件,一個是當連線建立時會被觸發,另外一個則是當連線斷開時會被觸發。

extension URLSessionWebSocket: URLSessionWebSocketDelegate {
    public func urlSession(_ session: URLSession,
                           webSocketTask: URLSessionWebSocketTask,
                           didOpenWithProtocol protocol: String?) {
        print("URLSessionWebSocketTask is connected")
    }

    public func urlSession(_ session: URLSession,
                           webSocketTask: URLSessionWebSocketTask,
                           didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
                           reason: Data?) {
        let reasonString: String
        if let reason = reason, let string = String(data: reason, encoding: .utf8) {
            reasonString = string
        } else {
            reasonString = ""
        }

        print("URLSessionWebSocketTask is closed: code=\(closeCode), reason=\(reasonString)")
    }
}

URLSessionWebSocketDelegate 必須一開始就設給 URLSession。所以建立連線時要自己宣告一個 URLSession,而不可以用 URLSession.defualt

let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
let request = URLRequest(url: url)

webSocketTask = urlSession.webSocketTask(with: request)
webSocketTask.resume()

Starsream

URLSessionWebSocketTask 看似不錯使用,但是它有些問題。第一個是,它只有在 iOS 13 之後才支援。iOS 13 目前還是太新,我們希望還沒有升級的 iPhone 使用者也可以安裝我們的 App。另一個是,URLSessionWebSocketTask 在接收訊息和監聽事件的方式上,並不是很方便。Starsream 可以為我們解決上述的問題。

首先要先安裝 Starsram 套件,如果你是使用 CocoaPods 的話,將這行放入你的 Podfile 裡。

pod 'Starscream', '~> 4.0.0' 

如果你不熟悉 CocoaPods 操作的話,可以參考這篇。

建立連線

宣告一個 WebSocket 並傳入 URL,然後呼叫 .connect() 就可以建立連線了。

import Starscream

guard let url = URL(string: "wss://echo.websocket.org/") else {
    print("Error: can not create URL")
    return
}

let request = URLRequest(url: url)

webSocket = WebSocket(request: request)
webSocket.connect()

傳送訊息

呼叫 WebSocket.write(string:) 即可傳送訊息。這跟 URLSessionWebSocketTask 相比,真的簡單很多!

webSocket.write(string: message)

接收訊息和監聽連線狀態

Starscream 處理訊息接收和連線狀態,都是使用事件 (Event) 的方式。尤其是訊息接收,與 URLSessionWebSocketTask 相比,事件的方式的確比較直覺。

webSocket.delegate = self

extension StarscreamWebSocket: WebSocketDelegate {
    func didReceive(event: WebSocketEvent, client: WebSocket) {
        switch event {
        case .connected(_):
            print("WebSocket is connected")
        case .disconnected(let reason, let code):
            print("Disconnected: code=\(code), reason=\(reason)")
        case .text(let message):
            print("Received: \(message)")
        case .binary(_):
            break
        case .pong(_):
            break
        case .ping(_):
            break
        case .error(let error):
            print(error ?? "")
        case .viabilityChanged(_):
            break
        case .reconnectSuggested(_):
            break
        case .cancelled:
            print("WebSocket is cancelled")
        }
    }
}

斷開連線

呼叫 WebSocket.disconnect() 就可以斷開連線。

webSocket.disconnect()

結語

我們介紹了如何使用 URLSessionWebSocketTask 和 Starscream 來實現 WebSocket。這兩個方式相比起來,我們會推薦 Starscream。雖然 URLSessionWebSocketTask 是 iOS SDK 內建的,但是其使用的方式和普及性,遠遠不及 Starscream。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Alex Alvarez on Unsplash
Read More

Dispatch Queue 教學

GCD 提供有效率的並行處理,讓我們不需要直接管理多執行緒。它的 Dispatch Queues 可以循序地(serially)或是並行地(concurrently)執行任務。我們只需要將要並行的程式當作任務提交到 dispatch queues 就可以了。
Read More
Photo by Florinel Gorgan on Unsplash
Read More

如何製作一個 XCFramework

XCFramework 讓你可以將 iPhone、iPhone 模擬器等多的不同平台的二進位碼打包到一個可發佈的 .xcframework 檔。你只需要為你的 Framework 產生出一個 .xcframework 檔,就可以支援多種平台。
Read More
Photo by Fabian Gieske on Unsplash
Read More

SwiftUI @State & @Binding 教學

SwiftUI 推出了兩個 Property Wrapper – @State and @Binding。利用它們可以達到變數的 Two-way Binding 功能。也就是當變數的值改變時,它會重新被顯示。本章藉由製作一個 Custom View 來展示如何使用 @State 和 @Binding。
Read More
Photo by Svitlana on Unsplash
Read More

iOS:禁止螢幕截圖

基於一些理由,我們可能會想要禁止使用者對我們的 app 做螢幕截圖。然而,iOS 並沒有提供這樣的功能。所幸,我們可以利用 UITextField 來達到此效果。
Read More