iOS WebSocket長(zhǎng)鏈接(Swift)

WebSocket-Swift

Starscream的使用

WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更好的節(jié)省服務(wù)器資源和帶寬并達(dá)到實(shí)時(shí)通訊,它建立在 TCP 之上,同 HTTP 一樣通過(guò) TCP 來(lái)傳輸數(shù)據(jù),但是它和 HTTP 最大不同是:WebSocket 是一種雙向通信協(xié)議.

現(xiàn)在很多第三方比如:融云,環(huán)信,網(wǎng)易云,騰訊云等等,都可以滿(mǎn)足即時(shí)通訊的功能。而且技術(shù)成熟。但是當(dāng)由于特殊要求不能使用第三方時(shí),就可以使用WebSocket建立長(zhǎng)鏈接,實(shí)現(xiàn)即時(shí)通訊功能。

目前Swift版本的WebSocket最好用的框架就應(yīng)該是daltoniam的Starscream了。
Starscream庫(kù)下載地址:Starscream

OC最好用的算是Facebook的SocketRocket,使用說(shuō)明可以參考我的另一篇文章iOS WebSocket長(zhǎng)鏈接

使用

一般一個(gè)項(xiàng)目在啟動(dòng)后的某個(gè)時(shí)機(jī)會(huì)啟動(dòng)創(chuàng)建一個(gè)長(zhǎng)鏈接,如果多個(gè)地方需要,就可以封裝為一個(gè)單例,全局使用。
可以使用podpod管理庫(kù), 在podfile中加入
pod 'Starscream', '~> 4.0.0'

在使用命令行工具cd到當(dāng)前工程 安裝
pod install

導(dǎo)入頭文件import Starscream,即可使用。

1.首先創(chuàng)建一個(gè)名為WebSocketManager的單例類(lèi)

    static let shard = WebSocketManager()

可以使用單例,也可以使用[alloc]init 根據(jù)情況自己選擇

2.創(chuàng)建一個(gè)枚舉,分別表示W(wǎng)ebSocket的鏈接狀態(tài)

enum WebSocketConnectType {
    case closed       //初始狀態(tài),未連接
    case connect      //已連接
    case disconnect   //連接后斷開(kāi)
    case reconnecting //重連中...
}

3.創(chuàng)建連接

 // MARK: - 公開(kāi)方法,外部調(diào)用
    func connectSocket(_ paremeters: Any?) {

paremeters可以是外部傳入的參數(shù)。

4.接收消息
包括連接成功的消息,發(fā)送的文本消息,連接失敗的消息,等等。
新版的庫(kù),將所有的消息接收都封裝在了一個(gè)代理方法里面,老版的是分開(kāi)的。老版本的代理我寫(xiě)在了最下面,可以參考一下,有助于理解。
遵循代理,實(shí)現(xiàn)代理,接收消息:

   webSocket?.delegate = self

消息接收:

   func didReceive(event: WebSocketEvent, client: WebSocket) {
     //接收各種消息
   }

5.關(guān)閉連接

 /// 斷開(kāi)鏈接
 func disconnect()

6.為保持長(zhǎng)鏈接的連接狀態(tài),需要定時(shí)向后臺(tái)發(fā)送消息,就是俗稱(chēng)的:心跳包。
需要?jiǎng)?chuàng)建一個(gè)定時(shí)器,固定時(shí)間發(fā)送ping消息。

7.重新連接

/// 重新連接
func reConnectSocket()

8.鏈接斷開(kāi)情況處理:
首先判斷是否是主動(dòng)斷開(kāi),并且記錄這個(gè)操作狀態(tài):
如果是主動(dòng)斷開(kāi)就不作處理。
如果不是主動(dòng)斷開(kāi)鏈接,需要做重新連接的邏輯.

    /// 用于判斷是否主動(dòng)關(guān)閉長(zhǎng)連接,如果是主動(dòng)斷開(kāi)連接,連接失敗的代理中,就不用執(zhí)行 重新連接方法
    private var isActivelyClose:Bool = false

代碼如下:

//
//  WebSocketManager.swift
//  SwiftTools
//
//  Created by 網(wǎng)易詞典 on 2019/12/1.
//  Copyright ? 2019 xuanhe. All rights reserved.
//

import UIKit
import Starscream


// MARK: - WebSocket代理
//這里即設(shè)置代理,稍后還會(huì)發(fā)通知.使用情況不一樣.
protocol WebSocketManagerDelegate: class {
    /// 建立連接成功通知
    func webSocketManagerDidConnect(manager: WebSocketManager)
    /// 斷開(kāi)鏈接通知,參數(shù) `isReconnecting` 表示是否處于等待重新連接狀態(tài)。
    func webSocketManagerDidDisconnect(manager: WebSocketManager, error: Error?)
    /// 接收到消息后的回調(diào)(String)
    func webSocketManagerDidReceiveMessage(manager: WebSocketManager, text: String)
    /// 接收到消息后的回調(diào)(Data)
    func webSocketManagerDidReceiveData(manager: WebSocketManager, data: Data)
}

enum WebSocketConnectType {
    case closed       //初始狀態(tài),未連接
    case connect      //已連接
    case disconnect   //連接后斷開(kāi)
    case reconnecting //重連中...
}



class WebSocketManager: NSObject {
    /// 單例,可以使用單例,也可以使用[alloc]init 根據(jù)情況自己選擇
    static let shard = WebSocketManager()
    /// WebSocket對(duì)象
    private var webSocket : WebSocket?
    /// 是否連接
    var isConnected : Bool = false
    /// 代理
    weak var delegate: WebSocketManagerDelegate?

    private var heartbeatInterval: TimeInterval = 5
    
    /// 重連次數(shù)
    private var reConnectCount: Int = 0
    //存儲(chǔ)要發(fā)送給服務(wù)端的數(shù)據(jù),本案例不實(shí)現(xiàn)此功能,如有需求自行實(shí)現(xiàn)
    private var sendDataArray = [String]()
    

    ///心跳包定時(shí)器
    var heartBeatTimer: Timer?
    ///網(wǎng)絡(luò)監(jiān)聽(tīng)定時(shí)器
    var netWorkTimer:Timer?
    
    
    var connectType : WebSocketConnectType = .closed
    /// 用于判斷是否主動(dòng)關(guān)閉長(zhǎng)連接,如果是主動(dòng)斷開(kāi)連接,連接失敗的代理中,就不用執(zhí)行 重新連接方法
    private var isActivelyClose:Bool = false
    
    /// 當(dāng)前是否有網(wǎng)絡(luò),??????????應(yīng)該由各自項(xiàng)目提供,本處為了方便,簡(jiǎn)歷一個(gè)屬性作為臨時(shí)變量
    private var isHaveNet:Bool = true

    
    override init() {
        // webSocket.advancedDelegate = self

    }
    
    // MARK: - 公開(kāi)方法,外部調(diào)用
    func connectSocket(_ paremeters: Any?) {
        guard let url = URL(string: "http://localhost:8888") else {
            return
        }

        self.isActivelyClose = false

        var request = URLRequest(url: url)
        request.timeoutInterval = 5
        
        //添加頭信息
        request.setValue("headers", forHTTPHeaderField: "Cookie")
        request.setValue("CustomeDeviceInfo", forHTTPHeaderField: "DeviceInfo")
        webSocket = WebSocket(request: request)
        webSocket?.delegate = self

        webSocket?.connect()
        // 自定義隊(duì)列,一般不需要設(shè)置,默認(rèn)主隊(duì)列
        //webSocket?.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")

    }
    /// 發(fā)送消息
    func sendMessage(_ text: String) {
        if self.isHaveNet {
            // 有網(wǎng)絡(luò)直接發(fā)消息
            if self.connectType == .connect {  //已經(jīng)連接
                self.webSocket?.write(string: text)
            }else if self.connectType == .reconnecting {
                self.sendDataArray.append(text)
            }else if self.connectType == .disconnect {
                reConnectSocket()
            }else{
                self.sendDataArray.append(text)
            }
            
        } else {
            // 無(wú)網(wǎng)絡(luò)的時(shí)候的操作
            //1.提示無(wú)網(wǎng)絡(luò)
            //2.存儲(chǔ)消息
            self.sendDataArray.append(text)
            //等待來(lái)網(wǎng)
            guard isActivelyClose else {
                initNetWorkTestingTimer()
                return
            }
        }
    }
    /// 斷開(kāi)鏈接
    func disconnect() {
        self.isActivelyClose = true
        self.connectType = .disconnect
        webSocket?.disconnect()
        destoryHeartBeat()
        destoryNetWorkStartTesting()
    }
    /// 重新連接
    func reConnectSocket() {
        if self.reConnectCount > 10 { //重連10次
            self.reConnectCount = 0;
            return
        }
        //重連10次,每?jī)纱伍g隔5s
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
            if self.connectType == .reconnecting {
                return
            }
            /// 連接
            self.connectSocket(nil)
            self.reConnectCount = self.reConnectCount + 1
        }
    }
    
    // MARK: - 網(wǎng)絡(luò)監(jiān)聽(tīng)
    func networkNotifation() {
        //外部最好也傳進(jìn)來(lái)一個(gè)網(wǎng)絡(luò)變化的通知
        //當(dāng)斷開(kāi)網(wǎng)絡(luò)時(shí)候,不在進(jìn)行重新連接
        
        //當(dāng)網(wǎng)絡(luò)恢復(fù)的時(shí)候,重新連接,根據(jù)自己業(yè)務(wù)進(jìn)行更新.
        
        //更新網(wǎng)絡(luò)狀態(tài)
        isHaveNet = false
    }
   
    
    // MARK: - 私有方法
    /// 初始化心跳
    private func initHeartBeat() {
        
        if self.heartBeatTimer != nil {
            return
        }
        self.heartBeatTimer = Timer(timeInterval: 1, target: self, selector: #selector(sendHeartBeat), userInfo: nil, repeats: true)
        RunLoop.current.add(self.heartBeatTimer!, forMode: RunLoop.Mode.common)
    }
    
    private func initNetWorkTestingTimer() {
        if self.netWorkTimer != nil {
            return
        }
        self.netWorkTimer = Timer(timeInterval: 5, target: self, selector: #selector(noNetWorkStartTesting), userInfo: nil, repeats: true)
        RunLoop.current.add(self.netWorkTimer!, forMode: RunLoop.Mode.common)
    }
    
    /// 心跳
    @objc private func sendHeartBeat() {
        if self.isConnected {
            let text = "ping的內(nèi)容,和服務(wù)器商定"
            if let data = text.data(using: String.Encoding.utf8) {
                webSocket?.write(ping: data)
            }
            
            // 我在網(wǎng)上查閱資料顯示,也可以使用webSocket?.write(string: "")
            // 即: webSocket?.write(string: text)
            // write方法中ping和text是一樣的,只是傳入的枚舉不一樣,可以參考源代碼
        }else{
            // 發(fā)現(xiàn)沒(méi)有連接,根據(jù)需求做判斷
        }
    }
    
    /// 沒(méi)有網(wǎng)絡(luò)的時(shí)候開(kāi)始定時(shí) -- 用于網(wǎng)絡(luò)檢測(cè)
    @objc private func noNetWorkStartTesting() {
        //有網(wǎng)絡(luò)
        if isHaveNet {//這里可以根據(jù)業(yè)務(wù)需要修改
            //1.關(guān)閉網(wǎng)絡(luò)監(jiān)測(cè)定時(shí)器
            destoryNetWorkStartTesting()
            //2.重新連接
            reConnectSocket()
        }
    }
    
    //關(guān)閉心跳定時(shí)器
    private func destoryHeartBeat() {
        self.heartBeatTimer?.invalidate()
        self.heartBeatTimer = nil
    }
    
    //關(guān)閉網(wǎng)絡(luò)監(jiān)測(cè)定時(shí)器
    private func destoryNetWorkStartTesting() {
        self.netWorkTimer?.invalidate()
        self.netWorkTimer = nil
    }
    
}

extension WebSocketManager: WebSocketDelegate{
    func didReceive(event: WebSocketEvent, client: WebSocket) {
        switch event {
            case .connected(let headers):
                isConnected = true
                delegate?.webSocketManagerDidConnect(manager: self)
                
                _ = "連接成功,在這里處理成功后的邏輯,比如將發(fā)送失敗的消息重新發(fā)送等等..."
                print("websocket is connected: \(headers)")
                break
            case .disconnected(let reason, let code):
                isConnected = false
                
                let error = NSError(domain: reason, code: Int(code), userInfo: nil) as Error
                    delegate?.webSocketManagerDidDisconnect(manager: self, error: error)
                
                self.connectType = .disconnect
                if self.isActivelyClose {
                    self.connectType = .closed
                } else {
                    self.connectType = .disconnect
                    destoryHeartBeat() //斷開(kāi)心跳定時(shí)器
                    if self.isHaveNet {
                        reConnectSocket()  //重新連接
                    } else {
                        initNetWorkTestingTimer()
                    }
                }
                print("websocket is disconnected: \(reason) with code: \(code)")
                break
            case .text(let string):
                delegate?.webSocketManagerDidReceiveMessage(manager: self, text: string)
            
             //當(dāng)全局都需要數(shù)據(jù)時(shí),這里使用通知.
                let dic = ["text" : string]
                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveMessage"), object: dic)
                print("Received text: \(string)")
                break
            case .binary(let data):
                print("Received data: \(data.count)")
                break
            case .ping(_):
                print("ping")
                break
            case .pong(_):
                print("pong")
                break
            case .viabilityChanged(_):
                break
            case .reconnectSuggested(_):
                break
            case .cancelled:
                isConnected = false
            case .error(let error):
                isConnected = false
                handleError(error)
        }
    }
    
    // custom
    func handleError(_ error: Error?) {
        if let e = error as? WSError {
            print("websocket encountered an error: \(e.message)")
        } else if let e = error {
            print("websocket encountered an error: \(e.localizedDescription)")
        } else {
            print("websocket encountered an error")
        }
    }
    
    
    // MARK: - 老版本的代理,不要了
    /// ????????????????????
    ///都是分開(kāi)的,現(xiàn)在都合成一個(gè)了,就是上面的didReceive,但是可以參考一下,理解邏輯,.知道哪些是重要的
    /// 連接成功后的回調(diào)
    func websocketDidConnect(socket: WebSocketClient) {
        print(#function)
    }
    /// 斷開(kāi)連接后的回調(diào)
    func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
        print(#function)
    }
    
    /// 接收到消息后的回調(diào)(String)
    func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
        print(#function)
    }
    /// 接收到消息后的回調(diào)(Data)
    func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
        print(#function)
    }
    
    ///  ??????????????????????
    
}

說(shuō)明:

代理里面已經(jīng)是全部的邏輯了,但是有一些細(xì)節(jié)需要特殊說(shuō)明,也需要我們自行處理:

  1. 外部接收消息,是使用代理還是使用通知??
    當(dāng)我們的長(zhǎng)鏈接只需要在某一個(gè)頁(yè)面使用的時(shí)候,我們創(chuàng)建了一個(gè)代理,在實(shí)現(xiàn)頁(yè)面需要實(shí)現(xiàn)代理即可。
    當(dāng)我們需要全局使用的時(shí)候,請(qǐng)使用通知,代理不準(zhǔn)確。
    下面代碼兩種方法,請(qǐng)使用其中一個(gè):
  delegate?.webSocketManagerDidReceiveMessage(manager: self, text: string)
            
  //當(dāng)全局都需要數(shù)據(jù)時(shí),這里使用通知.
  let dic = ["text" : string]
  NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveMessage"), object: dic)
  1. webSocket代理里面,我只寫(xiě)基本的處理邏輯,需要根據(jù)項(xiàng)目處理不同狀態(tài)下的邏輯。
  2. 當(dāng)消息發(fā)送失敗的時(shí)候,消息處理邏輯我們沒(méi)有實(shí)現(xiàn)完全。項(xiàng)目代碼里面有寫(xiě)。存儲(chǔ)要發(fā)送給服務(wù)端的數(shù)據(jù),本案例不實(shí)現(xiàn)此功能,如有需求自行實(shí)現(xiàn)。做法就是我們把發(fā)送失敗的消息存起來(lái),鏈接成功后重新發(fā)送。
  3. 重連次數(shù),我設(shè)置的是10次,可以自行修改
  4. 當(dāng)前是否有網(wǎng)絡(luò)的判斷,我寫(xiě)了一個(gè)bool變量,需要替換成我們項(xiàng)目里面網(wǎng)絡(luò)狀態(tài)判斷的相關(guān)代碼。
  5. 可以自定義隊(duì)列,一般不需要設(shè)置,默認(rèn)主隊(duì)列。我沒(méi)有處理在其他隊(duì)列情況的邏輯。

本文參考了文章:
SSRWebSocket

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容