iOS Starscream实现Websocket通讯

封装WebSocket:

import Starscream
import SwiftyJSON

// MARK: - WebSocketManagerDelegate
protocol WebSocketManagerDelegate: class {
    /// 已建立连接,包括正常连接和自动重新连接两种情况。
    func webSocketManagerDidConnect(manager: WebSocketManager)
    /// 已断开连接,包括正常和非正常断开连接两种情况。参数 `isReconnecting` 表示是否处于等待重新连接状态。
    func webSocketManagerDidDisconnect(manager: WebSocketManager, error: Error?, isReconnecting: Bool)
}

// MARK: - WebSocketManager
class WebSocketManager {

    /// 收到新的打印异常
    var hasNewPrintException = false

    /// 尚未收到过打印异常
    var hasNotReceivedPrintException = true

    weak var delegate: WebSocketManagerDelegate?

    var enableLog = false

    private var isHeartbeatTimeout = false
    private var heartbeatInterval: TimeInterval = 5
    private var heartbeatTimeout: TimeInterval = 10
    private var heartbeatTimestamp: TimeInterval = 0 // 毫秒

    private var reconnectRetryCount = 0
    private var maxReconnectRetryCount = 3
    private var reconnectOperation: DispatchWorkItem?
    private var reconnectRetryInterval: TimeInterval = 5

    private var error: Error?

    private(set) var state = State.closed {
        didSet {
            stateChanged(oldState: oldValue)
        }
    }

    private var isHeartbeatTimerSuspended = true

    private lazy var heartbeatTimer: DispatchSourceTimer = {
        let timer = DispatchSource.makeTimerSource(queue: .main)
        timer.schedule(deadline: .now(), repeating: heartbeatInterval, leeway: .milliseconds(100))
        timer.setEventHandler { [weak self] in
            guard let `self` = self else { return }
            if !self.isHeartbeatTimerSuspended {
                self.sendHeartbeat()
            }
        }
        return timer
    }()

    private let url: URL

    private lazy var socket: WebSocket = {
        let socket = WebSocket(url: url)
        socket.delegate = self
        return socket
    }()

    deinit {
        disconnect()
        destroyHeartbeatTimer()
    }

    init(url: URL) {
        self.url = url
    }

}

// MARK: - 连接相关
extension WebSocketManager {

    enum State {
        /// 等待重连中
        case reconnecting
        /// 正在连接中
        case connecting
        /// 已连接
        case connected
        /// 断开连接中
        case closing
        /// 已断开
        case closed
    }

    /// 只在 `state` 值为 `closed` 时有效果。
    func connect() {
        if state == .closed {
            state = .connecting
        }
    }

    /// 只在 `state` 值为 `reconnecting` 或 `connecting` 或 `connected` 时有效果。
    func disconnect() {
        switch state {
        case .reconnecting, .connecting, .connected:
            state = .closing
        case .closing, .closed:
            break
        }
    }

    private func reconnect() {
        guard state == .reconnecting else { return }

        guard reconnectRetryCount < maxReconnectRetryCount else {
            state = .closed
            return
        }

        delegate?.webSocketManagerDidDisconnect(manager: self, error: error, isReconnecting: true)

        // webSocketManagerDidDisconnect 方法中可能会进行 disconnect 操作
        guard state == .reconnecting else { return }

        reconnectOperation = DispatchWorkItem { [weak self] in
            guard let `self` = self else { return }

            self.increaseReconnectRetryCount()
            self.clearReconnectOperation()
            self.state = .connecting
        }

        if reconnectRetryCount == 0 {
            reconnectOperation?.perform()
        } else {
            DispatchQueue.main.asyncAfter(deadline: .now() + reconnectRetryInterval, execute: reconnectOperation!)
        }
    }

    private func clearReconnectOperation() {
        reconnectOperation = nil
    }

    private func cancelAndClearReconnectOperation() {
        reconnectOperation?.cancel()
        clearReconnectOperation()
    }

    private func increaseReconnectRetryCount() {
        reconnectRetryCount = min(reconnectRetryCount + 1, maxReconnectRetryCount)
    }

    private func resetReconnectRetryCount() {
        reconnectRetryCount = 0
    }

    private func stateChanged(oldState: State) {
        switch state {

        case .connecting:
            handleConnectingState()

        case .connected:
            handleConnectedState()

        case .reconnecting:
            handleReconnectingState()

        case .closing:
            handleClosingState(oldState: oldState)

        case .closed:
            handleClosedState(oldState: oldState)
        }
    }

    private func handleConnectingState() {
        socket.connect()
    }

    private func handleConnectedState() {
        resumeHeartbeatTimer()
        resetReconnectRetryCount()
        delegate?.webSocketManagerDidConnect(manager: self)
    }

    private func handleReconnectingState() {
        suspendHeartbeatTimer()
        reconnect()
    }

    private func handleClosingState(oldState: State) {
        switch oldState {

        case .connecting, .connected:
            suspendHeartbeatTimer()
            socket.disconnect()

        case .reconnecting:
            state = .closed

        case .closing, .closed:
            fatalError()
        }
    }

    private func handleClosedState(oldState: State) {
        switch oldState {

        case .closing: // 主动断开
            resetReconnectRetryCount()
            cancelAndClearReconnectOperation()

            if isHeartbeatTimeout {
                isHeartbeatTimeout = false
                state = .reconnecting // 心跳超时重连
            } else {
                delegate?.webSocketManagerDidDisconnect(
                    manager: self,
                    error: error,
                    isReconnecting: false)
            }

        case .reconnecting: // 重连次数上限
            resetReconnectRetryCount()
            delegate?.webSocketManagerDidDisconnect(
                manager: self,
                error: error,
                isReconnecting: false)

        case .connected, .connecting, .closed:
            fatalError()
        }
    }

}

// MARK: - 心跳相关
private extension WebSocketManager {

    func sendHeartbeat() {
        if socket.isConnected {
            updateHeartbeatTimestamp()
            socket.write(string: "\(heartbeatTimestamp)")
        }
    }

    func updateHeartbeatTimestamp() {
        heartbeatTimestamp = Date().timeIntervalSince1970 * 1_000
    }

    private func handleHeartbeatTimeout() {
        isHeartbeatTimeout = true
        disconnect()
    }

    func resumeHeartbeatTimer() {
        if isHeartbeatTimerSuspended {
            isHeartbeatTimerSuspended = false
            heartbeatTimer.resume()
        }
    }

    func suspendHeartbeatTimer() {
        if !isHeartbeatTimerSuspended {
            isHeartbeatTimerSuspended = true
            heartbeatTimer.suspend()
        }
    }

    func destroyHeartbeatTimer() {
        heartbeatTimer.cancel()
        resumeHeartbeatTimer()
    }

}

// MARK: - 消息处理
extension WebSocketManager {

    enum MessageType: String {

        case beat
        case print

        fileprivate func messageHandler(_ manager: WebSocketManager) -> (JSON) -> Void {
            switch self {
            case .beat:
                return WebSocketManager.handleBeatMessage(manager)
            case .print:
                return WebSocketManager.handlePrintMessage(manager)
            }
        }

    }

    private func handleBeatMessage(_ msg: JSON) {
        // {"msgType":"beat","ts":"1536134255660.87"}
        guard let ts = Double(msg["ts"].stringValue) else {
            return
        }

        // 超时重连
        if ts - heartbeatTimestamp > heartbeatTimeout {
            handleHeartbeatTimeout()
        }
    }

    private func handlePrintMessage(_ msg: JSON) {
        hasNewPrintException = true
        hasNotReceivedPrintException = false
        NotificationCenter.default.post(name: .PrintExceptionDidReceive, object: nil)
    }

}

// MARK: - WebSocketDelegate
extension WebSocketManager: WebSocketDelegate {
    
    func websocketDidConnect(socket: WebSocketClient) {
        log("websocketDidConnect")
        state = .connected
    }

    func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
        log("websocketDidDisconnect error: \(error as Any)")

        self.error = error
        
        switch state {

        case .connecting, .connected:
            state = .reconnecting // 断线重连

        case .closing:
            state = .closed // 主动关闭

        case .closed, .reconnecting:
            fatalError()
        }
    }

    func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
        log("websocketDidReceiveMessage text: \(text)")

        if state == .connected {
            let message = JSON(parseJSON: text)
            MessageType(rawValue: message["msgType"].stringValue)?.messageHandler(self)(message)
        }
    }

    func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
        log("websocketDidReceiveData \n \(String(data: data, encoding: .utf8)!)")
    }

}

extension WebSocketManager {

    func log(
        _ log: @autoclosure () -> String = "",
        file: String = #file,
        line: Int = #line,
        function: String = #function)
    {
        if enableLog {
            print("\(function) at \((file as NSString).lastPathComponent)[\(line)]", log())
        }
    }
}

参考链接:

https://www.jianshu.com/p/7879bd578db7

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357