iOS WebSocket长链接(Swift)

WebSocket-Swift

Starscream的使用

WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:WebSocket 是一种双向通信协议.

现在很多第三方比如:融云,环信,网易云,腾讯云等等,都可以满足即时通讯的功能。而且技术成熟。但是当由于特殊要求不能使用第三方时,就可以使用WebSocket建立长链接,实现即时通讯功能。

目前Swift版本的WebSocket最好用的框架就应该是daltoniam的Starscream了。
Starscream库下载地址:Starscream

OC最好用的算是Facebook的SocketRocket,使用说明可以参考我的另一篇文章iOS WebSocket长链接

使用

一般一个项目在启动后的某个时机会启动创建一个长链接,如果多个地方需要,就可以封装为一个单例,全局使用。
可以使用podpod管理库, 在podfile中加入
pod 'Starscream', '~> 4.0.0'

在使用命令行工具cd到当前工程 安装
pod install

导入头文件import Starscream,即可使用。

1.首先创建一个名为WebSocketManager的单例类

    static let shard = WebSocketManager()

可以使用单例,也可以使用[alloc]init 根据情况自己选择

2.创建一个枚举,分别表示WebSocket的链接状态

enum WebSocketConnectType {
    case closed       //初始状态,未连接
    case connect      //已连接
    case disconnect   //连接后断开
    case reconnecting //重连中...
}

3.创建连接

 // MARK: - 公开方法,外部调用
    func connectSocket(_ paremeters: Any?) {

paremeters可以是外部传入的参数。

4.接收消息
包括连接成功的消息,发送的文本消息,连接失败的消息,等等。
新版的库,将所有的消息接收都封装在了一个代理方法里面,老版的是分开的。老版本的代理我写在了最下面,可以参考一下,有助于理解。
遵循代理,实现代理,接收消息:

   webSocket?.delegate = self

消息接收:

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

5.关闭连接

 /// 断开链接
 func disconnect()

6.为保持长链接的连接状态,需要定时向后台发送消息,就是俗称的:心跳包。
需要创建一个定时器,固定时间发送ping消息。

7.重新连接

/// 重新连接
func reConnectSocket()

8.链接断开情况处理:
首先判断是否是主动断开,并且记录这个操作状态:
如果是主动断开就不作处理。
如果不是主动断开链接,需要做重新连接的逻辑.

    /// 用于判断是否主动关闭长连接,如果是主动断开连接,连接失败的代理中,就不用执行 重新连接方法
    private var isActivelyClose:Bool = false

代码如下:

//
//  WebSocketManager.swift
//  SwiftTools
//
//  Created by 网易词典 on 2019/12/1.
//  Copyright © 2019 xuanhe. All rights reserved.
//

import UIKit
import Starscream


// MARK: - WebSocket代理
//这里即设置代理,稍后还会发通知.使用情况不一样.
protocol WebSocketManagerDelegate: class {
    /// 建立连接成功通知
    func webSocketManagerDidConnect(manager: WebSocketManager)
    /// 断开链接通知,参数 `isReconnecting` 表示是否处于等待重新连接状态。
    func webSocketManagerDidDisconnect(manager: WebSocketManager, error: Error?)
    /// 接收到消息后的回调(String)
    func webSocketManagerDidReceiveMessage(manager: WebSocketManager, text: String)
    /// 接收到消息后的回调(Data)
    func webSocketManagerDidReceiveData(manager: WebSocketManager, data: Data)
}

enum WebSocketConnectType {
    case closed       //初始状态,未连接
    case connect      //已连接
    case disconnect   //连接后断开
    case reconnecting //重连中...
}



class WebSocketManager: NSObject {
    /// 单例,可以使用单例,也可以使用[alloc]init 根据情况自己选择
    static let shard = WebSocketManager()
    /// WebSocket对象
    private var webSocket : WebSocket?
    /// 是否连接
    var isConnected : Bool = false
    /// 代理
    weak var delegate: WebSocketManagerDelegate?

    private var heartbeatInterval: TimeInterval = 5
    
    /// 重连次数
    private var reConnectCount: Int = 0
    //存储要发送给服务端的数据,本案例不实现此功能,如有需求自行实现
    private var sendDataArray = [String]()
    

    ///心跳包定时器
    var heartBeatTimer: Timer?
    ///网络监听定时器
    var netWorkTimer:Timer?
    
    
    var connectType : WebSocketConnectType = .closed
    /// 用于判断是否主动关闭长连接,如果是主动断开连接,连接失败的代理中,就不用执行 重新连接方法
    private var isActivelyClose:Bool = false
    
    /// 当前是否有网络,👇👇👇👇👇应该由各自项目提供,本处为了方便,简历一个属性作为临时变量
    private var isHaveNet:Bool = true

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

    }
    
    // MARK: - 公开方法,外部调用
    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()
        // 自定义队列,一般不需要设置,默认主队列
        //webSocket?.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")

    }
    /// 发送消息
    func sendMessage(_ text: String) {
        if self.isHaveNet {
            // 有网络直接发消息
            if self.connectType == .connect {  //已经连接
                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 {
            // 无网络的时候的操作
            //1.提示无网络
            //2.存储消息
            self.sendDataArray.append(text)
            //等待来网
            guard isActivelyClose else {
                initNetWorkTestingTimer()
                return
            }
        }
    }
    /// 断开链接
    func disconnect() {
        self.isActivelyClose = true
        self.connectType = .disconnect
        webSocket?.disconnect()
        destoryHeartBeat()
        destoryNetWorkStartTesting()
    }
    /// 重新连接
    func reConnectSocket() {
        if self.reConnectCount > 10 { //重连10次
            self.reConnectCount = 0;
            return
        }
        //重连10次,每两次间隔5s
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
            if self.connectType == .reconnecting {
                return
            }
            /// 连接
            self.connectSocket(nil)
            self.reConnectCount = self.reConnectCount + 1
        }
    }
    
    // MARK: - 网络监听
    func networkNotifation() {
        //外部最好也传进来一个网络变化的通知
        //当断开网络时候,不在进行重新连接
        
        //当网络恢复的时候,重新连接,根据自己业务进行更新.
        
        //更新网络状态
        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的内容,和服务器商定"
            if let data = text.data(using: String.Encoding.utf8) {
                webSocket?.write(ping: data)
            }
            
            // 我在网上查阅资料显示,也可以使用webSocket?.write(string: "")
            // 即: webSocket?.write(string: text)
            // write方法中ping和text是一样的,只是传入的枚举不一样,可以参考源代码
        }else{
            // 发现没有连接,根据需求做判断
        }
    }
    
    /// 没有网络的时候开始定时 -- 用于网络检测
    @objc private func noNetWorkStartTesting() {
        //有网络
        if isHaveNet {//这里可以根据业务需要修改
            //1.关闭网络监测定时器
            destoryNetWorkStartTesting()
            //2.重新连接
            reConnectSocket()
        }
    }
    
    //关闭心跳定时器
    private func destoryHeartBeat() {
        self.heartBeatTimer?.invalidate()
        self.heartBeatTimer = nil
    }
    
    //关闭网络监测定时器
    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)
                
                _ = "连接成功,在这里处理成功后的逻辑,比如将发送失败的消息重新发送等等..."
                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() //断开心跳定时器
                    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)
            
             //当全局都需要数据时,这里使用通知.
                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: - 老版本的代理,不要了
    /// 👇👇👇👇👇👇👇👇👇👇
    ///都是分开的,现在都合成一个了,就是上面的didReceive,但是可以参考一下,理解逻辑,.知道哪些是重要的
    /// 连接成功后的回调
    func websocketDidConnect(socket: WebSocketClient) {
        print(#function)
    }
    /// 断开连接后的回调
    func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
        print(#function)
    }
    
    /// 接收到消息后的回调(String)
    func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
        print(#function)
    }
    /// 接收到消息后的回调(Data)
    func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
        print(#function)
    }
    
    ///  👆👆👆👆👆👆👆👆👆👆👆
    
}

说明:

代理里面已经是全部的逻辑了,但是有一些细节需要特殊说明,也需要我们自行处理:

  1. 外部接收消息,是使用代理还是使用通知??
    当我们的长链接只需要在某一个页面使用的时候,我们创建了一个代理,在实现页面需要实现代理即可。
    当我们需要全局使用的时候,请使用通知,代理不准确。
    下面代码两种方法,请使用其中一个:
  delegate?.webSocketManagerDidReceiveMessage(manager: self, text: string)
            
  //当全局都需要数据时,这里使用通知.
  let dic = ["text" : string]
  NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveMessage"), object: dic)
  1. webSocket代理里面,我只写基本的处理逻辑,需要根据项目处理不同状态下的逻辑。
  2. 当消息发送失败的时候,消息处理逻辑我们没有实现完全。项目代码里面有写。存储要发送给服务端的数据,本案例不实现此功能,如有需求自行实现。做法就是我们把发送失败的消息存起来,链接成功后重新发送。
  3. 重连次数,我设置的是10次,可以自行修改
  4. 当前是否有网络的判断,我写了一个bool变量,需要替换成我们项目里面网络状态判断的相关代码。
  5. 可以自定义队列,一般不需要设置,默认主队列。我没有处理在其他队列情况的逻辑。

本文参考了文章:
SSRWebSocket

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

推荐阅读更多精彩内容