iOS 中使用 webSocket

是服务器和app之间的一种通信方式

 

webSocket 实现了服务端推机制(主动向客户端发送消息)。新的 web 浏览器全都支持 WebSocket,这使得它的使用超级简单。通过 WebSocket 能够打开持久连接,大部分网络都能轻松处理 WebSocket 连接。在 iOS 中使用 WebSocket 比较麻烦,你必须进行大量的设置,而且内置的 API 根本帮不上忙。这时 Starscream 出现了——这个小巧、易于使用的库让你所有的烦恼不翼而飞。 

 

Client1 ——->   cloud  ————>client2,3,4…

    <——-返回ack         <——-返回ack

一,基本使用

1根据url创建socket

var request = URLRequest(url: URL(string: "url")!)

            request.timeoutInterval = 5//超时时间

            socket = WebSocket(request: request)

            socket.delegate = self//接收到消息走代理方法

2。发送消息

socket.write(string: sendStr)

3.接收消息,在代理方法中

func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
//接收到字符串消息
}
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
        printLog(“\(data)”)//接收到data消息
    }

二 常见问题

1.如何确保client向特定的client发送消息

        “\(storeID!)-\(deviceNumber)-\(deviceGlobalID!)”.uppercased()    这些标志客户端的唯一性

发送消息时带着要发送给哪些client(唯一标识性数组)发送给cloud,cloud根据要发送给的client数组向相应的client发送消息

/// 发送一条消息到指定的多个设备
    ///
    /// - Parameters:
    ///   - deviceID: web socket 登陆名称数组
    ///   - text: 要发送的文本
    func sendTextTo(deviceIDs: [String], text: String) {
        if socket == nil  {
            return
        }
        if socket.isConnected == false {
            return
        }
        let cmdMessage = AldeloMessage(Type: 1, MsgGID: UUID().uuidString, Receivers: deviceIDs, Content: text, Time: nil, Publisher: nil, axOrderIDs: nil)
        if let sendStr = AldeloMessage.toJsonString(messages: [cmdMessage]) {
            socket.write(string: sendStr)
        }
    }

 

2.如何保持链接

十分钟发送一次心跳包,app进入前台时,app断网重连时,app失去web连接时,重新连接

NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
 
do {
            try reachability.startNotifier()
        } catch {
            print("Unable to start notifier")
        }
        
        reachability.whenReachable = { [weak self] reachability in
            self?.reconnectTimes = 10
            firstly {
                after(seconds: 3)
                }.done {
                    if self?.socket == nil {
                        return
                    }
                    self?.socket.connect()
            }
        }
        
        if #available(iOS 10.0, *) {
            timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { timer in
                let now = Date().timeIntervalSince1970
                let s = now - self.lastReceivedMessageTime
                if s >= 600 && s <= 660  {
                    self.sendHeartBeat()
                } else if s > 660 {
                    self.reconnect()
                }
            }
        } else {
            timer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: true)
        }
    @objc func handleTimer(timer: Timer) {
        let now = Date().timeIntervalSince1970
        let s = now - self.lastReceivedMessageTime
        if s >= 600 && s <= 660  {
            self.sendHeartBeat()
        } else if s > 660 {
            self.reconnect()
        }
    }
    @objc func appDidBecomeActive(_ application: UIApplication) {
        firstly {
            after(seconds: 3)
            }.done {
                if self.socket == nil {
                    return
                }
                if self.socket.isConnected == false && self.reachability.connection != .none {
                    self.socket.connect()
                }
        }
    }

3.如何保证消息送达

client到cloud:client中维护一个message数据表(包括字段是否发送成功sent)cloud收到消息之后向client返回ack,client收到ack后将该条message标记为sent=1已发送

             60秒client未收到ack,视为发送失败,从新发送

             cloud端message表中已经存在该条消息,则忽略,但是向客户端client发送ack

cloud到clinet:client收到消息后向cloud返回ack,cloud收到ack标记消息为已发送成功, 60秒cloud未收到ack,视为发送失败,从新发送

             client端message表中已经存在该条消息,则忽略,但是向客户端client发送ack

func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
        printLog("Web Socket receive \(text)")
        lastReceivedMessageTime = Date().timeIntervalSince1970
        
        
        if text == "$" {
            printLog("Received Heart Beat!!!!")
            return
        }
        
        guard let messageArray = AldeloMessage.from(jsonData: text.data(using: .utf8)!) else { return }
 
        for message in messageArray {
            if message.Type == 99 { //ACK
                DBPool.write { db in
                    try? db.execute("Update AldeloMessageRecord Set sent = 1 Where messageGID = '\(message.MsgGID)'")
                }
                continue
            }
 
            //收到消息后回复ACK,这样服务器会标记这条消息发送成功
            sendACK(message: message)
 
            //从收到的消息列表中对比msgid, 如果已经收到过,则忽略这条消息, 去重处理
            var shouldReturn = false
            DBQueue.inDatabase { db in
                do {
                    if let count = try Int.fetchOne(db, "Select count(*) from AldeloMessageRecord where messageGID = '\(message.MsgGID)'"), count > 0 {
                        //数据库里有这条消息,说明已经收到过,忽略掉
                        Log.shareInstance.log(message: "Websocket 收到重复消息,已忽略")
                        printLog("Websocket 收到重复消息,已忽略")
                        shouldReturn = true
                    }
                } catch {
                    Log.shareInstance.log(message: "读取数据库错误")
                    printLog("读取数据库错误")
                    self.createTable()
                }
            }
            
            if shouldReturn == true {
                return
            }
//            if let count = DatabaseOption().intForSql("Select count(*) from AldeloMessageRecord where messageGID = '\(message.MsgGID)'"), count > 0 {
//                //数据库里有这条消息,说明已经收到过,忽略掉
//                Log.shareInstance.log(message: "Websocket 收到重复消息,已忽略")
//                printLog("Websocket 收到重复消息,已忽略")
//                return
//            }
            
            //发完ACK将message存到数据库
            let aMessage = AldeloMessageRecord()
            aMessage.messageGID = message.MsgGID
            aMessage.type = message.Type
            aMessage.time = message.Time
            aMessage.publisher = message.Publisher
            
            if message.Type == 1 { //text
                guard let content = message.Content else { continue }
                aMessage.message = message.Content
                if content.hasPrefix("cmd::") {
                    let ar = content.components(separatedBy: "::")
                    var para: String? = nil
                    if ar.count == 3 {
                        para = ar[2]
                    }
                    let cmdString = "\(ar[0])::\(ar[1])".lowercased()
                    let command = AldeloCommand(rawValue: cmdString) ?? AldeloCommand.unknown
                    
                    if command == .clinePrint {
                        if let ar = para?.components(separatedBy: ","), ar.count == 2 {
                            if let orderID = Int64(ar[0]), orderID > 0 {
                                gotPrintCommandBlock?([orderID],ar[1].boolValue(),true, message)
                                delegate?.receivedPrintCommand(axOrderIDs: [orderID], packingPrint: ar[1].boolValue(), isClientWebSocket: true)
                            }
                        }
                    } else {
                        gotCommandBlock?(command,para, message)
                        delegate?.receivedCommand(cmd: command,parameter: para,  message: message)
                    }
 
                } else {
                    gotMessageBlock?(message)
                    delegate?.receivedMessage(message: message)
                }
            } else if message.Type == 2 { //print
                guard let orderIDs = message.axOrderIDs else { return }
                aMessage.message = "\(orderIDs)"
                gotPrintCommandBlock?(orderIDs,true,false,message)
                delegate?.receivedPrintCommand(axOrderIDs: orderIDs, packingPrint: true, isClientWebSocket: false)
            } else if message.Type == 3 { //QR payment
                guard let content = message.Content else { continue }
                aMessage.message = content
                gotQRPaymentBlock?(content)
                delegate?.receivedQRPayment(content: content)
            } else if message.Type == 4 { //cloud 强制反激活
                printLog("cloud 强制反激活 .....")
            }
            
            DBPool.write { db in
                try? aMessage.insert(db)
            }
        }
    }

starscream地址:https://github.com/daltoniam/starscream